Empi 3 ruleset version (#1978)

* add rule version

* add rule version

* Rough in model for Golden Record.

* Test Link Rule Version

* add eid match boolean

* added new fields to EmpiLink to provide more information about how the link was created

* add logging to check an edge case

* all tests pass

* wip with failing tests

* tests pass

* FIXME

* optimize imports

* test score in provider output

* FIXME

* FIXME

* Fix jpa test app context

* fix migration string length

* review feedback param name

* review feedback javadoc

* review feedback javadoc

* bean config reorganization for cdr

* add more tests
This commit is contained in:
Ken Stevens 2020-07-17 08:31:15 -04:00 committed by GitHub
parent f5222f3105
commit ebd6ca4b64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 1167 additions and 488 deletions

View File

@ -34,6 +34,7 @@ import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@ -299,6 +300,20 @@ public class ParametersUtil {
addPart(theContext, theParameter, theName, value);
}
public static void addPartBoolean(FhirContext theContext, IBase theParameter, String theName, Boolean theValue) {
IPrimitiveType<Boolean> value = (IPrimitiveType<Boolean>) theContext.getElementDefinition("boolean").newInstance();
value.setValue(theValue);
addPart(theContext, theParameter, theName, value);
}
public static void addPartDecimal(FhirContext theContext, IBase theParameter, String theName, Double theValue) {
IPrimitiveType<BigDecimal> value = (IPrimitiveType<BigDecimal>) theContext.getElementDefinition("decimal").newInstance();
value.setValue(theValue == null ? null : new BigDecimal(theValue));
addPart(theContext, theParameter, theName, value);
}
public static void addPartCoding(FhirContext theContext, IBase theParameter, String theName, String theSystem, String theCode, String theDisplay) {
IBase coding = theContext.getElementDefinition("coding").newInstance();

View File

@ -52,10 +52,10 @@ Below are some simplifying principles HAPI EMPI enforces to reduce complexity an
1. HAPI EMPI stores these extra link details in a table called `MPI_LINK`.
1. Each record in the `MPI_LINK` table corresponds to a `link.target` entry on a Person resource unless it is a NO_MATCH record. HAPI EMPI uses the following convention for the Person.link.assurance level:
1. Level 1: not used
1. Level 2: POSSIBLE_MATCH
1. Level 3: AUTO MATCH
1. Level 4: MANUAL MATCH
1. Level 1: POSSIBLE_MATCH
1. Level 2: AUTO MATCH
1. Level 3: MANUAL MATCH
1. Level 4: GOLDEN RECORD
### Possible rule match outcomes:

View File

@ -92,6 +92,15 @@ This operation returns a `Parameters` resource that looks like the following:
}, {
"name": "linkSource",
"valueString": "AUTO"
}, {
"name": "eidMatch",
"valueBoolean": false
}, {
"name": "newPerson",
"valueBoolean": false
}, {
"name": "score",
"valueDecimal": 1.8
} ]
} ]
}

View File

@ -0,0 +1,33 @@
package ca.uhn.fhir.jpa.dao.empi;
import ca.uhn.fhir.jpa.dao.data.IEmpiLinkDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class EmpiLinkDeleteSvc {
private static final Logger ourLog = LoggerFactory.getLogger(EmpiLinkDeleteSvc.class);
@Autowired
private IEmpiLinkDao myEmpiLinkDao;
@Autowired
private IdHelperService myIdHelperService;
/**
* Delete all EmpiLink records with any reference to this resource. (Used by Expunge.)
* @param theResource
* @return the number of records deleted
*/
public int deleteWithAnyReferenceTo(IBaseResource theResource) {
Long pid = myIdHelperService.getPidOrThrowException(theResource.getIdElement(), null);
int removed = myEmpiLinkDao.deleteWithAnyReferenceToPid(pid);
if (removed > 0) {
ourLog.info("Removed {} EMPI links with references to {}", removed, theResource.getIdElement().toVersionless());
}
return removed;
}
}

View File

@ -24,7 +24,6 @@ import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import javax.persistence.Column;
import javax.persistence.Entity;
@ -49,6 +48,7 @@ import java.util.Date;
@UniqueConstraint(name = "IDX_EMPI_PERSON_TGT", columnNames = {"PERSON_PID", "TARGET_PID"}),
})
public class EmpiLink {
public static final int VERSION_LENGTH = 16;
private static final int MATCH_RESULT_LENGTH = 16;
private static final int LINK_SOURCE_LENGTH = 16;
@ -88,6 +88,29 @@ public class EmpiLink {
@Column(name = "UPDATED", nullable = false)
private Date myUpdated;
@Column(name = "VERSION", nullable = false, length = VERSION_LENGTH)
private String myVersion;
/** This link was created as a result of an eid match **/
@Column(name = "EID_MATCH")
private Boolean myEidMatch;
/** This link created a new person **/
@Column(name = "NEW_PERSON")
private Boolean myNewPerson;
@Column(name = "VECTOR")
private Long myVector;
@Column(name = "SCORE")
private Double myScore;
public EmpiLink() {}
public EmpiLink(String theVersion) {
myVersion = theVersion;
}
public Long getId() {
return myId;
}
@ -177,17 +200,6 @@ public class EmpiLink {
return myLinkSource == EmpiLinkSourceEnum.MANUAL;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("myId", myId)
.append("myPersonPid", myPersonPid)
.append("myTargetPid", myTargetPid)
.append("myMatchResult", myMatchResult)
.append("myLinkSource", myLinkSource)
.toString();
}
public Date getCreated() {
return myCreated;
}
@ -205,4 +217,70 @@ public class EmpiLink {
myUpdated = theUpdated;
return this;
}
public String getVersion() {
return myVersion;
}
public EmpiLink setVersion(String theVersion) {
myVersion = theVersion;
return this;
}
public Long getVector() {
return myVector;
}
public EmpiLink setVector(Long theVector) {
myVector = theVector;
return this;
}
public Double getScore() {
return myScore;
}
public EmpiLink setScore(Double theScore) {
myScore = theScore;
return this;
}
public Boolean getEidMatch() {
return myEidMatch;
}
public boolean isEidMatch() {
return myEidMatch != null && myEidMatch;
}
public EmpiLink setEidMatch(Boolean theEidMatch) {
myEidMatch = theEidMatch;
return this;
}
public Boolean getNewPerson() {
return myNewPerson;
}
public boolean isNewPerson() {
return myNewPerson != null && myNewPerson;
}
public EmpiLink setNewPerson(Boolean theNewPerson) {
myNewPerson = theNewPerson;
return this;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("myPersonPid", myPersonPid)
.append("myTargetPid", myTargetPid)
.append("myMatchResult", myMatchResult)
.append("myLinkSource", myLinkSource)
.append("myEidMatch", myEidMatch)
.append("myNewPerson", myNewPerson)
.append("myScore", myScore)
.toString();
}
}

View File

@ -24,7 +24,6 @@ import javax.persistence.EntityManagerFactory;
SubscriptionChannelConfig.class
})
public class TestJPAConfig {
@Bean
public DaoConfig daoConfig() {
return new DaoConfig();

View File

@ -33,45 +33,36 @@ import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator;
import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc;
import ca.uhn.fhir.empi.util.EIDHelper;
import ca.uhn.fhir.empi.util.PersonHelper;
import ca.uhn.fhir.jpa.dao.empi.EmpiLinkDeleteSvc;
import ca.uhn.fhir.jpa.empi.broker.EmpiMessageHandler;
import ca.uhn.fhir.jpa.empi.broker.EmpiQueueConsumerLoader;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkFactory;
import ca.uhn.fhir.jpa.empi.interceptor.EmpiStorageInterceptor;
import ca.uhn.fhir.jpa.empi.interceptor.IEmpiStorageInterceptor;
import ca.uhn.fhir.jpa.empi.svc.EmpiCandidateSearchCriteriaBuilderSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiCandidateSearchSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiEidUpdateService;
import ca.uhn.fhir.jpa.empi.svc.EmpiLinkQuerySvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiLinkSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiLinkUpdaterSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiMatchFinderSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiMatchLinkSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiPersonFindingSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiPersonMergerSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiResourceDaoSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiCandidateSearchCriteriaBuilderSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiCandidateSearchSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiPersonFindingSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.FindCandidateByEidSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.FindCandidateByLinkSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.FindCandidateByScoreSvc;
import ca.uhn.fhir.rest.server.util.ISearchParamRetriever;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import javax.annotation.PostConstruct;
@Configuration
public class EmpiConsumerConfig {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
IEmpiSettings myEmpiProperties;
@Autowired
EmpiProviderLoader myEmpiProviderLoader;
@Autowired
EmpiSubscriptionLoader myEmpiSubscriptionLoader;
@Autowired
EmpiSearchParameterLoader myEmpiSearchParameterLoader;
@Bean
IEmpiStorageInterceptor empiStorageInterceptor() {
return new EmpiStorageInterceptor();
@ -127,6 +118,21 @@ public class EmpiConsumerConfig {
return new EmpiPersonFindingSvc();
}
@Bean
FindCandidateByEidSvc findCandidateByEidSvc() {
return new FindCandidateByEidSvc();
}
@Bean
FindCandidateByLinkSvc findCandidateByLinkSvc() {
return new FindCandidateByLinkSvc();
}
@Bean
FindCandidateByScoreSvc findCandidateByScoreSvc() {
return new FindCandidateByScoreSvc();
}
@Bean
EmpiProviderLoader empiProviderLoader() {
return new EmpiProviderLoader();
@ -173,34 +179,28 @@ public class EmpiConsumerConfig {
return new EIDHelper(theFhirContext, theEmpiConfig);
}
@Bean
EmpiLinkDaoSvc empiLinkDaoSvc() {
return new EmpiLinkDaoSvc();
}
@Bean
EmpiLinkFactory empiLinkFactory(IEmpiSettings theEmpiSettings) {
return new EmpiLinkFactory(theEmpiSettings);
}
@Bean
IEmpiLinkUpdaterSvc manualLinkUpdaterSvc() {
return new EmpiLinkUpdaterSvcImpl();
}
@PostConstruct
public void registerInterceptorAndProvider() {
if (!myEmpiProperties.isEnabled()) {
return;
}
@Bean
EmpiLoader empiLoader() {
return new EmpiLoader();
}
@EventListener(classes = {ContextRefreshedEvent.class})
// This @Order is here to ensure that MatchingQueueSubscriberLoader has initialized before we initialize this.
// Otherwise the EMPI subscriptions won't get loaded into the SubscriptionRegistry
@Order
public void updateSubscriptions() {
if (!myEmpiProperties.isEnabled()) {
return;
}
myEmpiProviderLoader.loadProvider();
ourLog.info("EMPI provider registered");
myEmpiSubscriptionLoader.daoUpdateEmpiSubscriptions();
ourLog.info("EMPI subscriptions updated");
myEmpiSearchParameterLoader.daoUpdateEmpiSearchParameters();
ourLog.info("EMPI search parameters updated");
@Bean
EmpiLinkDeleteSvc empiLinkDeleteSvc() {
return new EmpiLinkDeleteSvc();
}
}

View File

@ -0,0 +1,44 @@
package ca.uhn.fhir.jpa.empi.config;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.provider.EmpiProviderLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
@Service
public class EmpiLoader {
private static final Logger ourLog = LoggerFactory.getLogger(EmpiLoader.class);
@Autowired
IEmpiSettings myEmpiProperties;
@Autowired
EmpiProviderLoader myEmpiProviderLoader;
@Autowired
EmpiSubscriptionLoader myEmpiSubscriptionLoader;
@Autowired
EmpiSearchParameterLoader myEmpiSearchParameterLoader;
@EventListener(classes = {ContextRefreshedEvent.class})
// This @Order is here to ensure that MatchingQueueSubscriberLoader has initialized before we initialize this.
// Otherwise the EMPI subscriptions won't get loaded into the SubscriptionRegistry
@Order
public void updateSubscriptions() {
if (!myEmpiProperties.isEnabled()) {
return;
}
myEmpiProviderLoader.loadProvider();
ourLog.info("EMPI provider registered");
myEmpiSubscriptionLoader.daoUpdateEmpiSubscriptions();
ourLog.info("EMPI subscriptions updated");
myEmpiSearchParameterLoader.daoUpdateEmpiSearchParameters();
ourLog.info("EMPI search parameters updated");
}
}

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.empi.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator;
import ca.uhn.fhir.jpa.dao.empi.EmpiLinkDeleteSvc;
import ca.uhn.fhir.jpa.empi.interceptor.EmpiSubmitterInterceptorLoader;
import ca.uhn.fhir.jpa.empi.svc.EmpiSearchParamSvc;
import org.springframework.context.annotation.Bean;
@ -44,4 +45,9 @@ public class EmpiSubmitterConfig {
EmpiRuleValidator empiRuleValidator(FhirContext theFhirContext) {
return new EmpiRuleValidator(theFhirContext, empiSearchParamSvc());
}
@Bean
EmpiLinkDeleteSvc empiLinkDeleteSvc() {
return new EmpiLinkDeleteSvc();
}
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.jpa.dao;
package ca.uhn.fhir.jpa.empi.dao;
/*-
* #%L
@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.dao;
*/
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
@ -32,7 +33,6 @@ import org.hl7.fhir.r4.model.Patient;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@ -43,25 +43,34 @@ import java.util.Date;
import java.util.List;
import java.util.Optional;
@Service
public class EmpiLinkDaoSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private IEmpiLinkDao myEmpiLinkDao;
@Autowired
private EmpiLinkFactory myEmpiLinkFactory;
@Autowired
private IdHelperService myIdHelperService;
@Transactional
public EmpiLink createOrUpdateLinkEntity(IBaseResource thePerson, IBaseResource theTarget, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource, @Nullable EmpiTransactionContext theEmpiTransactionContext) {
public EmpiLink createOrUpdateLinkEntity(IBaseResource thePerson, IBaseResource theTarget, EmpiMatchOutcome theMatchOutcome, EmpiLinkSourceEnum theLinkSource, @Nullable EmpiTransactionContext theEmpiTransactionContext) {
Long personPid = myIdHelperService.getPidOrNull(thePerson);
Long resourcePid = myIdHelperService.getPidOrNull(theTarget);
EmpiLink empiLink = getOrCreateEmpiLinkByPersonPidAndTargetPid(personPid, resourcePid);
empiLink.setLinkSource(theLinkSource);
empiLink.setMatchResult(theMatchResult);
empiLink.setMatchResult(theMatchOutcome.getMatchResultEnum());
// Preserve these flags for link updates
empiLink.setEidMatch(theMatchOutcome.isEidMatch() | empiLink.isEidMatch());
empiLink.setNewPerson(theMatchOutcome.isNewPerson() | empiLink.isNewPerson());
if (empiLink.getScore() != null) {
empiLink.setScore(Math.max(theMatchOutcome.score, empiLink.getScore()));
} else {
empiLink.setScore(theMatchOutcome.score);
}
String message = String.format("Creating EmpiLink from %s to %s -> %s", thePerson.getIdElement().toUnqualifiedVersionless(), theTarget.getIdElement().toUnqualifiedVersionless(), theMatchResult);
String message = String.format("Creating EmpiLink from %s to %s -> %s", thePerson.getIdElement().toUnqualifiedVersionless(), theTarget.getIdElement().toUnqualifiedVersionless(), theMatchOutcome);
theEmpiTransactionContext.addTransactionLogMessage(message);
ourLog.debug(message);
save(empiLink);
@ -75,7 +84,7 @@ public class EmpiLinkDaoSvc {
if (oExisting.isPresent()) {
return oExisting.get();
} else {
EmpiLink empiLink = new EmpiLink();
EmpiLink empiLink = myEmpiLinkFactory.newEmpiLink();
empiLink.setPersonPid(thePersonPid);
empiLink.setTargetPid(theResourcePid);
return empiLink;
@ -87,7 +96,7 @@ public class EmpiLinkDaoSvc {
if (theTargetPid == null || thePersonPid == null) {
return Optional.empty();
}
EmpiLink link = new EmpiLink();
EmpiLink link = myEmpiLinkFactory.newEmpiLink();
link.setTargetPid(theTargetPid);
link.setPersonPid(thePersonPid);
Example<EmpiLink> example = Example.of(link);
@ -95,7 +104,7 @@ public class EmpiLinkDaoSvc {
}
public List<EmpiLink> getEmpiLinksByTargetPidAndMatchResult(Long theTargetPid, EmpiMatchResultEnum theMatchResult) {
EmpiLink exampleLink = new EmpiLink();
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink();
exampleLink.setTargetPid(theTargetPid);
exampleLink.setMatchResult(theMatchResult);
Example<EmpiLink> example = Example.of(exampleLink);
@ -103,7 +112,7 @@ public class EmpiLinkDaoSvc {
}
public Optional<EmpiLink> getMatchedLinkForTargetPid(Long theTargetPid) {
EmpiLink exampleLink = new EmpiLink();
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink();
exampleLink.setTargetPid(theTargetPid);
exampleLink.setMatchResult(EmpiMatchResultEnum.MATCH);
Example<EmpiLink> example = Example.of(exampleLink);
@ -116,7 +125,7 @@ public class EmpiLinkDaoSvc {
return Optional.empty();
}
EmpiLink exampleLink = new EmpiLink();
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink();
exampleLink.setTargetPid(pid);
exampleLink.setMatchResult(EmpiMatchResultEnum.MATCH);
Example<EmpiLink> example = Example.of(exampleLink);
@ -124,7 +133,7 @@ public class EmpiLinkDaoSvc {
}
public Optional<EmpiLink> getEmpiLinksByPersonPidTargetPidAndMatchResult(Long thePersonPid, Long theTargetPid, EmpiMatchResultEnum theMatchResult) {
EmpiLink exampleLink = new EmpiLink();
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink();
exampleLink.setPersonPid(thePersonPid);
exampleLink.setTargetPid(theTargetPid);
exampleLink.setMatchResult(theMatchResult);
@ -138,7 +147,7 @@ public class EmpiLinkDaoSvc {
* @return A list of EmpiLinks that hold potential duplicate persons.
*/
public List<EmpiLink> getPossibleDuplicates() {
EmpiLink exampleLink = new EmpiLink();
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink();
exampleLink.setMatchResult(EmpiMatchResultEnum.POSSIBLE_DUPLICATE);
Example<EmpiLink> example = Example.of(exampleLink);
return myEmpiLinkDao.findAll(example);
@ -149,7 +158,7 @@ public class EmpiLinkDaoSvc {
if (pid == null) {
return Optional.empty();
}
EmpiLink empiLink = new EmpiLink().setTargetPid(pid);
EmpiLink empiLink = myEmpiLinkFactory.newEmpiLink().setTargetPid(pid);
Example<EmpiLink> example = Example.of(empiLink);
return myEmpiLinkDao.findOne(example);
}
@ -159,26 +168,12 @@ public class EmpiLinkDaoSvc {
myEmpiLinkDao.delete(theEmpiLink);
}
/**
* Delete all EmpiLink records with any reference to this resource. (Used by Expunge.)
* @param theResource
* @return the number of records deleted
*/
public int deleteWithAnyReferenceTo(IBaseResource theResource) {
Long pid = myIdHelperService.getPidOrThrowException(theResource.getIdElement(), null);
int removed = myEmpiLinkDao.deleteWithAnyReferenceToPid(pid);
if (removed > 0) {
ourLog.info("Removed {} EMPI links with references to {}", removed, theResource.getIdElement().toVersionless());
}
return removed;
}
public List<EmpiLink> findEmpiLinksByPersonId(IBaseResource thePersonResource) {
Long pid = myIdHelperService.getPidOrNull(thePersonResource);
if (pid == null) {
return Collections.emptyList();
}
EmpiLink empiLink = new EmpiLink().setPersonPid(pid);
EmpiLink empiLink = myEmpiLinkFactory.newEmpiLink().setPersonPid(pid);
Example<EmpiLink> example = Example.of(empiLink);
return myEmpiLinkDao.findAll(example);
}
@ -200,8 +195,12 @@ public class EmpiLinkDaoSvc {
if (pid == null) {
return Collections.emptyList();
}
EmpiLink empiLink = new EmpiLink().setTargetPid(pid);
EmpiLink empiLink = myEmpiLinkFactory.newEmpiLink().setTargetPid(pid);
Example<EmpiLink> example = Example.of(empiLink);
return myEmpiLinkDao.findAll(example);
}
public EmpiLink newEmpiLink() {
return myEmpiLinkFactory.newEmpiLink();
}
}

View File

@ -0,0 +1,18 @@
package ca.uhn.fhir.jpa.empi.dao;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import org.springframework.beans.factory.annotation.Autowired;
public class EmpiLinkFactory {
private final IEmpiSettings myEmpiSettings;
@Autowired
public EmpiLinkFactory(IEmpiSettings theEmpiSettings) {
myEmpiSettings = theEmpiSettings;
}
public EmpiLink newEmpiLink() {
return new EmpiLink(myEmpiSettings.getRuleVersion());
}
}

View File

@ -29,7 +29,7 @@ import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.empi.util.PersonHelper;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.dao.empi.EmpiLinkDeleteSvc;
import ca.uhn.fhir.jpa.dao.expunge.ExpungeEverythingService;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.api.server.RequestDetails;
@ -50,7 +50,7 @@ public class EmpiStorageInterceptor implements IEmpiStorageInterceptor {
@Autowired
private ExpungeEverythingService myExpungeEverythingService;
@Autowired
private EmpiLinkDaoSvc myEmpiLinkDaoSvc;
private EmpiLinkDeleteSvc myEmpiLinkDeleteSvc;
@Autowired
private FhirContext myFhirContext;
@Autowired
@ -87,7 +87,7 @@ public class EmpiStorageInterceptor implements IEmpiStorageInterceptor {
if (EmpiUtil.isEmpiManagedPerson(myFhirContext, theNewResource) &&
myPersonHelper.isDeactivated(theNewResource)) {
ourLog.debug("Deleting empi links to deactivated Person {}", theNewResource.getIdElement().toUnqualifiedVersionless());
myEmpiLinkDaoSvc.deleteWithAnyReferenceTo(theNewResource);
myEmpiLinkDeleteSvc.deleteWithAnyReferenceTo(theNewResource);
}
if (isInternalRequest(theRequestDetails)) {
@ -106,7 +106,7 @@ public class EmpiStorageInterceptor implements IEmpiStorageInterceptor {
if (!EmpiUtil.isEmpiResourceType(myFhirContext, theResource)) {
return;
}
myEmpiLinkDaoSvc.deleteWithAnyReferenceTo(theResource);
myEmpiLinkDeleteSvc.deleteWithAnyReferenceTo(theResource);
}
private void forbidIfModifyingExternalEidOnTarget(IBaseResource theNewResource, IBaseResource theOldResource) {
@ -181,6 +181,6 @@ public class EmpiStorageInterceptor implements IEmpiStorageInterceptor {
@Hook(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE)
public void expungeAllMatchedEmpiLinks(AtomicInteger theCounter, IBaseResource theResource) {
ourLog.debug("Expunging EmpiLink records with reference to {}", theResource.getIdElement());
theCounter.addAndGet(myEmpiLinkDaoSvc.deleteWithAnyReferenceTo(theResource));
theCounter.addAndGet(myEmpiLinkDeleteSvc.deleteWithAnyReferenceTo(theResource));
}
}

View File

@ -21,7 +21,7 @@ package ca.uhn.fhir.jpa.empi.svc;
*/
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.log.Logs;
@ -29,7 +29,9 @@ import ca.uhn.fhir.empi.model.CanonicalEID;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.util.EIDHelper;
import ca.uhn.fhir.empi.util.PersonHelper;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiPersonFindingSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.MatchedPersonCandidate;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import org.hl7.fhir.instance.model.api.IAnyResource;
@ -107,16 +109,16 @@ public class EmpiEidUpdateService {
private void createNewPersonAndFlagAsDuplicate(IAnyResource theResource, EmpiTransactionContext theEmpiTransactionContext, IAnyResource theOldPerson) {
log(theEmpiTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs.");
IAnyResource newPerson = myPersonHelper.createPersonFromEmpiTarget(theResource);
myEmpiLinkSvc.updateLink(newPerson, theResource, EmpiMatchResultEnum.MATCH, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
myEmpiLinkSvc.updateLink(newPerson, theOldPerson, EmpiMatchResultEnum.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
myEmpiLinkSvc.updateLink(newPerson, theResource, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
myEmpiLinkSvc.updateLink(newPerson, theOldPerson, EmpiMatchOutcome.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
}
private void linkToNewPersonAndFlagAsDuplicate(IAnyResource theResource, IAnyResource theOldPerson, IAnyResource theNewPerson, EmpiTransactionContext theEmpiTransactionContext) {
log(theEmpiTransactionContext, "Changing a match link!");
myEmpiLinkSvc.deleteLink(theOldPerson, theResource, theEmpiTransactionContext);
myEmpiLinkSvc.updateLink(theNewPerson, theResource, EmpiMatchResultEnum.MATCH, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
myEmpiLinkSvc.updateLink(theNewPerson, theResource, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
log(theEmpiTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs.");
myEmpiLinkSvc.updateLink(theNewPerson, theOldPerson, EmpiMatchResultEnum.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
myEmpiLinkSvc.updateLink(theNewPerson, theOldPerson, EmpiMatchOutcome.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
}
private void log(EmpiTransactionContext theEmpiTransactionContext, String theMessage) {

View File

@ -25,8 +25,8 @@ import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.util.ParametersUtil;
import org.hl7.fhir.instance.model.api.IBase;
@ -82,13 +82,16 @@ public class EmpiLinkQuerySvcImpl implements IEmpiLinkQuerySvc {
if (includeResultAndSource) {
ParametersUtil.addPartString(myFhirContext, resultPart, "matchResult", empiLink.getMatchResult().name());
ParametersUtil.addPartString(myFhirContext, resultPart, "linkSource", empiLink.getLinkSource().name());
ParametersUtil.addPartBoolean(myFhirContext, resultPart, "eidMatch", empiLink.getEidMatch());
ParametersUtil.addPartBoolean(myFhirContext, resultPart, "newPerson", empiLink.getNewPerson());
ParametersUtil.addPartDecimal(myFhirContext, resultPart, "score", empiLink.getScore());
}
}
return retval;
}
private Example<EmpiLink> exampleLinkFromParameters(IIdType thePersonId, IIdType theTargetId, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource) {
EmpiLink empiLink = new EmpiLink();
EmpiLink empiLink = myEmpiLinkDaoSvc.newEmpiLink();
if (thePersonId != null) {
empiLink.setPersonPid(myIdHelperService.getPidOrThrowException(thePersonId));
}

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.empi.svc;
*/
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.empi.log.Logs;
@ -28,8 +29,8 @@ import ca.uhn.fhir.empi.model.CanonicalIdentityAssuranceLevel;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.util.AssuranceLevelUtil;
import ca.uhn.fhir.empi.util.PersonHelper;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.instance.model.api.IAnyResource;
@ -63,24 +64,25 @@ public class EmpiLinkSvcImpl implements IEmpiLinkSvc {
@Override
@Transactional
public void updateLink(IAnyResource thePerson, IAnyResource theTarget, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiTransactionContext) {
public void updateLink(IAnyResource thePerson, IAnyResource theTarget, EmpiMatchOutcome theMatchOutcome, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiTransactionContext) {
IIdType resourceId = theTarget.getIdElement().toUnqualifiedVersionless();
if (theMatchResult == EmpiMatchResultEnum.POSSIBLE_DUPLICATE && personsLinkedAsNoMatch(thePerson, theTarget)) {
if (theMatchOutcome.isPossibleDuplicate() && personsLinkedAsNoMatch(thePerson, theTarget)) {
log(theEmpiTransactionContext, thePerson.getIdElement().toUnqualifiedVersionless() +
" is linked as NO_MATCH with " +
theTarget.getIdElement().toUnqualifiedVersionless() +
" not linking as POSSIBLE_DUPLICATE.");
return;
}
validateRequestIsLegal(thePerson, theTarget, theMatchResult, theLinkSource);
switch (theMatchResult) {
EmpiMatchResultEnum matchResultEnum = theMatchOutcome.getMatchResultEnum();
validateRequestIsLegal(thePerson, theTarget, matchResultEnum, theLinkSource);
switch (matchResultEnum) {
case MATCH:
myPersonHelper.addOrUpdateLink(thePerson, resourceId, AssuranceLevelUtil.getAssuranceLevel(theMatchResult, theLinkSource), theEmpiTransactionContext);
myPersonHelper.addOrUpdateLink(thePerson, resourceId, AssuranceLevelUtil.getAssuranceLevel(matchResultEnum, theLinkSource), theEmpiTransactionContext);
myEmpiResourceDaoSvc.updatePerson(thePerson);
break;
case POSSIBLE_MATCH:
myPersonHelper.addOrUpdateLink(thePerson, resourceId, AssuranceLevelUtil.getAssuranceLevel(theMatchResult, theLinkSource), theEmpiTransactionContext);
myPersonHelper.addOrUpdateLink(thePerson, resourceId, AssuranceLevelUtil.getAssuranceLevel(matchResultEnum, theLinkSource), theEmpiTransactionContext);
break;
case NO_MATCH:
myPersonHelper.removeLink(thePerson, resourceId, theEmpiTransactionContext);
@ -89,7 +91,7 @@ public class EmpiLinkSvcImpl implements IEmpiLinkSvc {
break;
}
myEmpiResourceDaoSvc.updatePerson(thePerson);
createOrUpdateLinkEntity(thePerson, theTarget, theMatchResult, theLinkSource, theEmpiTransactionContext);
createOrUpdateLinkEntity(thePerson, theTarget, theMatchOutcome, theLinkSource, theEmpiTransactionContext);
}
private boolean personsLinkedAsNoMatch(IAnyResource thePerson, IAnyResource theTarget) {
@ -175,8 +177,8 @@ public class EmpiLinkSvcImpl implements IEmpiLinkSvc {
}
}
private void createOrUpdateLinkEntity(IBaseResource thePerson, IBaseResource theResource, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiTransactionContext) {
myEmpiLinkDaoSvc.createOrUpdateLinkEntity(thePerson, theResource, theMatchResult, theLinkSource, theEmpiTransactionContext);
private void createOrUpdateLinkEntity(IBaseResource thePerson, IBaseResource theResource, EmpiMatchOutcome theMatchOutcome, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiTransactionContext) {
myEmpiLinkDaoSvc.createOrUpdateLinkEntity(thePerson, theResource, theMatchOutcome, theLinkSource, theEmpiTransactionContext);
}
private void log(EmpiTransactionContext theEmpiTransactionContext, String theMessage) {

View File

@ -29,8 +29,8 @@ import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;

View File

@ -23,6 +23,7 @@ package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.MatchedTarget;
import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiCandidateSearchSvc;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

View File

@ -21,20 +21,23 @@ package ca.uhn.fhir.jpa.empi.svc;
*/
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.empi.util.PersonHelper;
import ca.uhn.fhir.jpa.empi.svc.candidate.CandidateList;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiPersonFindingSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.MatchedPersonCandidate;
import ca.uhn.fhir.rest.server.TransactionLogMessages;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* EmpiMatchLinkSvc is the entrypoint for HAPI's EMPI system. An incoming resource can call
@ -72,46 +75,47 @@ public class EmpiMatchLinkSvc {
}
private EmpiTransactionContext doEmpiUpdate(IAnyResource theResource, EmpiTransactionContext theEmpiTransactionContext) {
List<MatchedPersonCandidate> personCandidates = myEmpiPersonFindingSvc.findPersonCandidates(theResource);
if (personCandidates.isEmpty()) {
CandidateList candidateList = myEmpiPersonFindingSvc.findPersonCandidates(theResource);
if (candidateList.isEmpty()) {
handleEmpiWithNoCandidates(theResource, theEmpiTransactionContext);
} else if (personCandidates.size() == 1) {
handleEmpiWithSingleCandidate(theResource, personCandidates, theEmpiTransactionContext);
} else if (candidateList.exactlyOneMatch()) {
handleEmpiWithSingleCandidate(theResource, candidateList.getOnlyMatch(), theEmpiTransactionContext);
} else {
handleEmpiWithMultipleCandidates(theResource, personCandidates, theEmpiTransactionContext);
handleEmpiWithMultipleCandidates(theResource, candidateList, theEmpiTransactionContext);
}
return theEmpiTransactionContext;
}
private void handleEmpiWithMultipleCandidates(IAnyResource theResource, List<MatchedPersonCandidate> thePersonCandidates, EmpiTransactionContext theEmpiTransactionContext) {
Long samplePersonPid = thePersonCandidates.get(0).getCandidatePersonPid().getIdAsLong();
boolean allSamePerson = thePersonCandidates.stream()
private void handleEmpiWithMultipleCandidates(IAnyResource theResource, CandidateList theCandidateList, EmpiTransactionContext theEmpiTransactionContext) {
MatchedPersonCandidate firstMatch = theCandidateList.getFirstMatch();
Long samplePersonPid = firstMatch.getCandidatePersonPid().getIdAsLong();
boolean allSamePerson = theCandidateList.stream()
.allMatch(candidate -> candidate.getCandidatePersonPid().getIdAsLong().equals(samplePersonPid));
if (allSamePerson) {
log(theEmpiTransactionContext, "EMPI received multiple match candidates, but they are all linked to the same person.");
handleEmpiWithSingleCandidate(theResource, thePersonCandidates, theEmpiTransactionContext);
handleEmpiWithSingleCandidate(theResource, firstMatch, theEmpiTransactionContext);
} else {
log(theEmpiTransactionContext, "EMPI received multiple match candidates, that were linked to different Persons. Setting POSSIBLE_DUPLICATES and POSSIBLE_MATCHES.");
//Set them all as POSSIBLE_MATCH
List<IAnyResource> persons = thePersonCandidates.stream().map((MatchedPersonCandidate matchedPersonCandidate) -> myEmpiPersonFindingSvc.getPersonFromMatchedPersonCandidate(matchedPersonCandidate)).collect(Collectors.toList());
persons.forEach(person -> {
myEmpiLinkSvc.updateLink(person, theResource, EmpiMatchResultEnum.POSSIBLE_MATCH, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
});
List<IAnyResource> persons = new ArrayList<>();
for (MatchedPersonCandidate matchedPersonCandidate : theCandidateList.getCandidates()) {
IAnyResource person = myEmpiPersonFindingSvc.getPersonFromMatchedPersonCandidate(matchedPersonCandidate);
myEmpiLinkSvc.updateLink(person, theResource, EmpiMatchOutcome.EID_POSSIBLE_MATCH, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
persons.add(person);
}
//Set all Persons as POSSIBLE_DUPLICATE of the first person.
IAnyResource samplePerson = persons.get(0);
persons.subList(1, persons.size()).stream()
.forEach(possibleDuplicatePerson -> {
myEmpiLinkSvc.updateLink(samplePerson, possibleDuplicatePerson, EmpiMatchResultEnum.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
});
//Set all Persons as POSSIBLE_DUPLICATE of the last person.
IAnyResource firstPerson = persons.get(0);
persons.subList(1, persons.size())
.forEach(possibleDuplicatePerson -> myEmpiLinkSvc.updateLink(firstPerson, possibleDuplicatePerson, EmpiMatchOutcome.EID_POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext));
}
}
private void handleEmpiWithNoCandidates(IAnyResource theResource, EmpiTransactionContext theEmpiTransactionContext) {
log(theEmpiTransactionContext, "There were no matched candidates for EMPI, creating a new Person.");
IAnyResource newPerson = myPersonHelper.createPersonFromEmpiTarget(theResource);
myEmpiLinkSvc.updateLink(newPerson, theResource, EmpiMatchResultEnum.MATCH, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
myEmpiLinkSvc.updateLink(newPerson, theResource, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
}
private void handleEmpiCreate(IAnyResource theResource, MatchedPersonCandidate thePersonCandidate, EmpiTransactionContext theEmpiTransactionContext) {
@ -120,8 +124,8 @@ public class EmpiMatchLinkSvc {
if (myPersonHelper.isPotentialDuplicate(person, theResource)) {
log(theEmpiTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs.");
IAnyResource newPerson = myPersonHelper.createPersonFromEmpiTarget(theResource);
myEmpiLinkSvc.updateLink(newPerson, theResource, EmpiMatchResultEnum.MATCH, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
myEmpiLinkSvc.updateLink(newPerson, person, EmpiMatchResultEnum.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
myEmpiLinkSvc.updateLink(newPerson, theResource, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
myEmpiLinkSvc.updateLink(newPerson, person, EmpiMatchOutcome.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
} else {
if (thePersonCandidate.isMatch()) {
myPersonHelper.handleExternalEidAddition(person, theResource, theEmpiTransactionContext);
@ -131,13 +135,12 @@ public class EmpiMatchLinkSvc {
}
}
private void handleEmpiWithSingleCandidate(IAnyResource theResource, List<MatchedPersonCandidate> thePersonCandidates, EmpiTransactionContext theEmpiTransactionContext) {
private void handleEmpiWithSingleCandidate(IAnyResource theResource, MatchedPersonCandidate thePersonCandidate, EmpiTransactionContext theEmpiTransactionContext) {
log(theEmpiTransactionContext, "EMPI has narrowed down to one candidate for matching.");
MatchedPersonCandidate matchedPersonCandidate = thePersonCandidates.get(0);
if (theEmpiTransactionContext.getRestOperation().equals(EmpiTransactionContext.OperationType.UPDATE)) {
myEidUpdateService.handleEmpiUpdate(theResource, matchedPersonCandidate, theEmpiTransactionContext);
myEidUpdateService.handleEmpiUpdate(theResource, thePersonCandidate, theEmpiTransactionContext);
} else {
handleEmpiCreate(theResource, matchedPersonCandidate, theEmpiTransactionContext);
handleEmpiCreate(theResource, thePersonCandidate, theEmpiTransactionContext);
}
}

View File

@ -1,183 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* 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%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.MatchedTarget;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.model.CanonicalEID;
import ca.uhn.fhir.empi.util.EIDHelper;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class EmpiPersonFindingSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private FhirContext myFhirContext;
@Autowired
IdHelperService myIdHelperService;
@Autowired
private EmpiLinkDaoSvc myEmpiLinkDaoSvc;
@Autowired
private EmpiResourceDaoSvc myEmpiResourceDaoSvc;
@Autowired
private IEmpiMatchFinderSvc myEmpiMatchFinderSvc;
@Autowired
private EIDHelper myEIDHelper;
/**
* Given an incoming IBaseResource, limited to Patient/Practitioner, return a list of {@link MatchedPersonCandidate}
* indicating possible candidates for a matching Person. Uses several separate methods for finding candidates:
* <p>
* 0. First, check the incoming Resource for an EID. If it is present, and we can find a Person with this EID, it automatically matches.
* 1. First, check link table for any entries where this baseresource is the target of a person. If found, return.
* 2. If none are found, attempt to find Person Resources which link to this theResource.
* 3. If none are found, attempt to find Persons similar to our incoming resource based on the EMPI rules and similarity metrics.
* 4. If none are found, attempt to find Persons that are linked to Patients/Practitioners that are similar to our incoming resource based on the EMPI rules and
* similarity metrics.
*
* @param theResource the {@link IBaseResource} we are attempting to find matching candidate Persons for.
* @return A list of {@link MatchedPersonCandidate} indicating all potential Person matches.
*/
public List<MatchedPersonCandidate> findPersonCandidates(IAnyResource theResource) {
List<MatchedPersonCandidate> matchedPersonCandidates = attemptToFindPersonCandidateFromIncomingEID(theResource);
if (matchedPersonCandidates.isEmpty()) {
matchedPersonCandidates = attemptToFindPersonCandidateFromEmpiLinkTable(theResource);
}
if (matchedPersonCandidates.isEmpty()) {
//OK, so we have not found any links in the EmpiLink table with us as a target. Next, let's find possible Patient/Practitioner
//matches by following EMPI rules.
matchedPersonCandidates = attemptToFindPersonCandidateFromSimilarTargetResource(theResource);
}
return matchedPersonCandidates;
}
private List<MatchedPersonCandidate> attemptToFindPersonCandidateFromIncomingEID(IAnyResource theBaseResource) {
List<MatchedPersonCandidate> retval = new ArrayList<>();
List<CanonicalEID> eidFromResource = myEIDHelper.getExternalEid(theBaseResource);
if (!eidFromResource.isEmpty()) {
for (CanonicalEID eid : eidFromResource) {
Optional<IAnyResource> oFoundPerson = myEmpiResourceDaoSvc.searchPersonByEid(eid.getValue());
if (oFoundPerson.isPresent()) {
IAnyResource foundPerson = oFoundPerson.get();
Long pidOrNull = myIdHelperService.getPidOrNull(foundPerson);
MatchedPersonCandidate mpc = new MatchedPersonCandidate(new ResourcePersistentId(pidOrNull), EmpiMatchResultEnum.MATCH);
ourLog.debug("Matched {} by EID {}", foundPerson.getIdElement(), eid);
retval.add(mpc);
}
}
}
return retval;
}
/**
* Attempt to find a currently matching Person, based on the presence of an {@link EmpiLink} entity.
*
* @param theBaseResource the {@link IAnyResource} that we want to find candidate Persons for.
* @return an Optional list of {@link MatchedPersonCandidate} indicating matches.
*/
private List<MatchedPersonCandidate> attemptToFindPersonCandidateFromEmpiLinkTable(IAnyResource theBaseResource) {
List<MatchedPersonCandidate> retval = new ArrayList<>();
Long targetPid = myIdHelperService.getPidOrNull(theBaseResource);
if (targetPid != null) {
Optional<EmpiLink> oLink = myEmpiLinkDaoSvc.getMatchedLinkForTargetPid(targetPid);
if (oLink.isPresent()) {
ResourcePersistentId personPid = new ResourcePersistentId(oLink.get().getPersonPid());
ourLog.debug("Resource previously linked. Using existing link.");
retval.add(new MatchedPersonCandidate(personPid, oLink.get().getMatchResult()));
}
}
return retval;
}
/**
* Attempt to find matching Persons by resolving them from similar Matching target resources, where target resource
* can be either Patient or Practitioner. Runs EMPI logic over the existing Patient/Practitioners, then finds their
* entries in the EmpiLink table, and returns all the matches found therein.
*
* @param theBaseResource the {@link IBaseResource} which we want to find candidate Persons for.
* @return an Optional list of {@link MatchedPersonCandidate} indicating matches.
*/
private List<MatchedPersonCandidate> attemptToFindPersonCandidateFromSimilarTargetResource(IAnyResource theBaseResource) {
List<MatchedPersonCandidate> retval = new ArrayList<>();
List<Long> personPidsToExclude = getNoMatchPersonPids(theBaseResource);
List<MatchedTarget> matchedCandidates = myEmpiMatchFinderSvc.getMatchedTargets(myFhirContext.getResourceType(theBaseResource), theBaseResource);
//Convert all possible match targets to their equivalent Persons by looking up in the EmpiLink table,
//while ensuring that the matches aren't in our NO_MATCH list.
// The data flow is as follows ->
// MatchedTargetCandidate -> Person -> EmpiLink -> MatchedPersonCandidate
matchedCandidates = matchedCandidates.stream().filter(mc -> mc.isMatch() || mc.isPossibleMatch()).collect(Collectors.toList());
for (MatchedTarget match : matchedCandidates) {
Optional<EmpiLink> optMatchEmpiLink = myEmpiLinkDaoSvc.getMatchedLinkForTargetPid(myIdHelperService.getPidOrNull(match.getTarget()));
if (!optMatchEmpiLink.isPresent()) {
continue;
}
EmpiLink matchEmpiLink = optMatchEmpiLink.get();
if (personPidsToExclude.contains(matchEmpiLink.getPersonPid())) {
ourLog.info("Skipping EMPI on candidate person with PID {} due to manual NO_MATCH", matchEmpiLink.getPersonPid());
continue;
}
MatchedPersonCandidate candidate = new MatchedPersonCandidate(getResourcePersistentId(matchEmpiLink.getPersonPid()), match.getMatchResult());
retval.add(candidate);
}
return retval;
}
private List<Long> getNoMatchPersonPids(IBaseResource theBaseResource) {
Long targetPid = myIdHelperService.getPidOrNull(theBaseResource);
return myEmpiLinkDaoSvc.getEmpiLinksByTargetPidAndMatchResult(targetPid, EmpiMatchResultEnum.NO_MATCH)
.stream()
.map(EmpiLink::getPersonPid)
.collect(Collectors.toList());
}
private ResourcePersistentId getResourcePersistentId(Long thePersonPid) {
return new ResourcePersistentId(thePersonPid);
}
public IAnyResource getPersonFromMatchedPersonCandidate(MatchedPersonCandidate theMatchedPersonCandidate) {
ResourcePersistentId personPid = theMatchedPersonCandidate.getCandidatePersonPid();
return myEmpiResourceDaoSvc.readPersonByPid(personPid);
}
}

View File

@ -27,8 +27,8 @@ import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.util.PersonHelper;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IAnyResource;
@ -76,7 +76,7 @@ public class EmpiPersonMergerSvcImpl implements IEmpiPersonMergerSvc {
}
private void addMergeLink(Long theFromPersonPid, Long theToPersonPid) {
EmpiLink empiLink = new EmpiLink()
EmpiLink empiLink = myEmpiLinkDaoSvc.newEmpiLink()
.setPersonPid(theFromPersonPid)
.setTargetPid(theToPersonPid)
.setMatchResult(EmpiMatchResultEnum.MATCH)

View File

@ -0,0 +1,25 @@
package ca.uhn.fhir.jpa.empi.svc.candidate;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
public abstract class BaseCandidateFinder {
@Autowired
IdHelperService myIdHelperService;
@Autowired
EmpiLinkDaoSvc myEmpiLinkDaoSvc;
CandidateList findCandidates(IAnyResource theTarget) {
CandidateList candidateList = new CandidateList(getStrategy());
candidateList.addAll(findMatchPersonCandidates(theTarget));
return candidateList;
}
protected abstract List<MatchedPersonCandidate> findMatchPersonCandidates(IAnyResource theTarget);
protected abstract CandidateStrategyEnum getStrategy();
}

View File

@ -0,0 +1,46 @@
package ca.uhn.fhir.jpa.empi.svc.candidate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
public class CandidateList {
private final CandidateStrategyEnum mySource;
private final List<MatchedPersonCandidate> myList = new ArrayList<>();
public CandidateList(CandidateStrategyEnum theSource) {
mySource = theSource;
}
public CandidateStrategyEnum getSource() {
return mySource;
}
public boolean isEmpty() {
return myList.isEmpty();
}
public void addAll(List<MatchedPersonCandidate> theList) { myList.addAll(theList); }
public MatchedPersonCandidate getOnlyMatch() {
assert myList.size() == 1;
return myList.get(0);
}
public boolean exactlyOneMatch() {
return myList.size()== 1;
}
public Stream<MatchedPersonCandidate> stream() {
return myList.stream();
}
public List<MatchedPersonCandidate> getCandidates() {
return Collections.unmodifiableList(myList);
}
public MatchedPersonCandidate getFirstMatch() {
return myList.get(0);
}
}

View File

@ -0,0 +1,10 @@
package ca.uhn.fhir.jpa.empi.svc.candidate;
public enum CandidateStrategyEnum {
/** Find Person candidates based on matching EID */
EID,
/** Find Person candidates based on a link already existing for the target resource */
LINK,
/** Find Person candidates based on other targets that match the incoming target using the EMPI Matching rules */
SCORE
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.jpa.empi.svc;
package ca.uhn.fhir.jpa.empi.svc.candidate;
/*-
* #%L
@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.empi.svc;
*/
import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson;
import ca.uhn.fhir.jpa.empi.svc.EmpiSearchParamSvc;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.jpa.empi.svc;
package ca.uhn.fhir.jpa.empi.svc.candidate;
/*-
* #%L
@ -27,6 +27,7 @@ import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.svc.EmpiSearchParamSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IAnyResource;

View File

@ -0,0 +1,79 @@
package ca.uhn.fhir.jpa.empi.svc.candidate;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* 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%
*/
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.jpa.empi.svc.EmpiResourceDaoSvc;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class EmpiPersonFindingSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private EmpiResourceDaoSvc myEmpiResourceDaoSvc;
@Autowired
private FindCandidateByEidSvc myFindCandidateByEidSvc;
@Autowired
private FindCandidateByLinkSvc myFindCandidateByLinkSvc;
@Autowired
private FindCandidateByScoreSvc myFindCandidateByScoreSvc;
/**
* Given an incoming IBaseResource, limited to Patient/Practitioner, return a list of {@link MatchedPersonCandidate}
* indicating possible candidates for a matching Person. Uses several separate methods for finding candidates:
* <p>
* 0. First, check the incoming Resource for an EID. If it is present, and we can find a Person with this EID, it automatically matches.
* 1. First, check link table for any entries where this baseresource is the target of a person. If found, return.
* 2. If none are found, attempt to find Person Resources which link to this theResource.
* 3. If none are found, attempt to find Persons similar to our incoming resource based on the EMPI rules and similarity metrics.
* 4. If none are found, attempt to find Persons that are linked to Patients/Practitioners that are similar to our incoming resource based on the EMPI rules and
* similarity metrics.
*
* @param theResource the {@link IBaseResource} we are attempting to find matching candidate Persons for.
* @return A list of {@link MatchedPersonCandidate} indicating all potential Person matches.
*/
public CandidateList findPersonCandidates(IAnyResource theResource) {
CandidateList matchedPersonCandidates = myFindCandidateByEidSvc.findCandidates(theResource);
if (matchedPersonCandidates.isEmpty()) {
matchedPersonCandidates = myFindCandidateByLinkSvc.findCandidates(theResource);
}
if (matchedPersonCandidates.isEmpty()) {
//OK, so we have not found any links in the EmpiLink table with us as a target. Next, let's find possible Patient/Practitioner
//matches by following EMPI rules.
matchedPersonCandidates = myFindCandidateByScoreSvc.findCandidates(theResource);
}
return matchedPersonCandidates;
}
public IAnyResource getPersonFromMatchedPersonCandidate(MatchedPersonCandidate theMatchedPersonCandidate) {
ResourcePersistentId personPid = theMatchedPersonCandidate.getCandidatePersonPid();
return myEmpiResourceDaoSvc.readPersonByPid(personPid);
}
}

View File

@ -0,0 +1,50 @@
package ca.uhn.fhir.jpa.empi.svc.candidate;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.model.CanonicalEID;
import ca.uhn.fhir.empi.util.EIDHelper;
import ca.uhn.fhir.jpa.empi.svc.EmpiResourceDaoSvc;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class FindCandidateByEidSvc extends BaseCandidateFinder {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private EIDHelper myEIDHelper;
@Autowired
private EmpiResourceDaoSvc myEmpiResourceDaoSvc;
protected List<MatchedPersonCandidate> findMatchPersonCandidates(IAnyResource theBaseResource) {
List<MatchedPersonCandidate> retval = new ArrayList<>();
List<CanonicalEID> eidFromResource = myEIDHelper.getExternalEid(theBaseResource);
if (!eidFromResource.isEmpty()) {
for (CanonicalEID eid : eidFromResource) {
Optional<IAnyResource> oFoundPerson = myEmpiResourceDaoSvc.searchPersonByEid(eid.getValue());
if (oFoundPerson.isPresent()) {
IAnyResource foundPerson = oFoundPerson.get();
Long pidOrNull = myIdHelperService.getPidOrNull(foundPerson);
MatchedPersonCandidate mpc = new MatchedPersonCandidate(new ResourcePersistentId(pidOrNull), EmpiMatchOutcome.EID_MATCH);
ourLog.debug("Matched {} by EID {}", foundPerson.getIdElement(), eid);
retval.add(mpc);
}
}
}
return retval;
}
@Override
protected CandidateStrategyEnum getStrategy() {
return CandidateStrategyEnum.EID;
}
}

View File

@ -0,0 +1,44 @@
package ca.uhn.fhir.jpa.empi.svc.candidate;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class FindCandidateByLinkSvc extends BaseCandidateFinder {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
/**
* Attempt to find a currently matching Person, based on the presence of an {@link EmpiLink} entity.
*
* @param theTarget the {@link IAnyResource} that we want to find candidate Persons for.
* @return an Optional list of {@link MatchedPersonCandidate} indicating matches.
*/
@Override
protected List<MatchedPersonCandidate> findMatchPersonCandidates(IAnyResource theTarget) {
List<MatchedPersonCandidate> retval = new ArrayList<>();
Long targetPid = myIdHelperService.getPidOrNull(theTarget);
if (targetPid != null) {
Optional<EmpiLink> oLink = myEmpiLinkDaoSvc.getMatchedLinkForTargetPid(targetPid);
if (oLink.isPresent()) {
ResourcePersistentId personPid = new ResourcePersistentId(oLink.get().getPersonPid());
ourLog.debug("Resource previously linked. Using existing link.");
retval.add(new MatchedPersonCandidate(personPid, oLink.get()));
}
}
return retval;
}
@Override
protected CandidateStrategyEnum getStrategy() {
return CandidateStrategyEnum.LINK;
}
}

View File

@ -0,0 +1,90 @@
package ca.uhn.fhir.jpa.empi.svc.candidate;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.MatchedTarget;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class FindCandidateByScoreSvc extends BaseCandidateFinder {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private FhirContext myFhirContext;
@Autowired
IdHelperService myIdHelperService;
@Autowired
private EmpiLinkDaoSvc myEmpiLinkDaoSvc;
@Autowired
private IEmpiMatchFinderSvc myEmpiMatchFinderSvc;
/**
* Attempt to find matching Persons by resolving them from similar Matching target resources, where target resource
* can be either Patient or Practitioner. Runs EMPI logic over the existing Patient/Practitioners, then finds their
* entries in the EmpiLink table, and returns all the matches found therein.
*
* @param theTarget the {@link IBaseResource} which we want to find candidate Persons for.
* @return an Optional list of {@link MatchedPersonCandidate} indicating matches.
*/
@Override
protected List<MatchedPersonCandidate> findMatchPersonCandidates(IAnyResource theTarget) {
List<MatchedPersonCandidate> retval = new ArrayList<>();
List<Long> personPidsToExclude = getNoMatchPersonPids(theTarget);
List<MatchedTarget> matchedCandidates = myEmpiMatchFinderSvc.getMatchedTargets(myFhirContext.getResourceType(theTarget), theTarget);
//Convert all possible match targets to their equivalent Persons by looking up in the EmpiLink table,
//while ensuring that the matches aren't in our NO_MATCH list.
// The data flow is as follows ->
// MatchedTargetCandidate -> Person -> EmpiLink -> MatchedPersonCandidate
matchedCandidates = matchedCandidates.stream().filter(mc -> mc.isMatch() || mc.isPossibleMatch()).collect(Collectors.toList());
for (MatchedTarget match : matchedCandidates) {
Optional<EmpiLink> optMatchEmpiLink = myEmpiLinkDaoSvc.getMatchedLinkForTargetPid(myIdHelperService.getPidOrNull(match.getTarget()));
if (!optMatchEmpiLink.isPresent()) {
continue;
}
EmpiLink matchEmpiLink = optMatchEmpiLink.get();
if (personPidsToExclude.contains(matchEmpiLink.getPersonPid())) {
ourLog.info("Skipping EMPI on candidate person with PID {} due to manual NO_MATCH", matchEmpiLink.getPersonPid());
continue;
}
MatchedPersonCandidate candidate = new MatchedPersonCandidate(getResourcePersistentId(matchEmpiLink.getPersonPid()), match.getMatchResult());
retval.add(candidate);
}
return retval;
}
private List<Long> getNoMatchPersonPids(IBaseResource theBaseResource) {
Long targetPid = myIdHelperService.getPidOrNull(theBaseResource);
return myEmpiLinkDaoSvc.getEmpiLinksByTargetPidAndMatchResult(targetPid, EmpiMatchResultEnum.NO_MATCH)
.stream()
.map(EmpiLink::getPersonPid)
.collect(Collectors.toList());
}
private ResourcePersistentId getResourcePersistentId(Long thePersonPid) {
return new ResourcePersistentId(thePersonPid);
}
@Override
protected CandidateStrategyEnum getStrategy() {
return CandidateStrategyEnum.SCORE;
}
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.jpa.empi.svc;
package ca.uhn.fhir.jpa.empi.svc.candidate;
/*-
* #%L
@ -20,28 +20,33 @@ package ca.uhn.fhir.jpa.empi.svc;
* #L%
*/
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
public class MatchedPersonCandidate {
private final ResourcePersistentId myCandidatePersonPid;
private final EmpiMatchResultEnum myEmpiMatchResult;
private final EmpiMatchOutcome myEmpiMatchOutcome;
public MatchedPersonCandidate(ResourcePersistentId theCandidate, EmpiMatchResultEnum theEmpiMatchResult) {
public MatchedPersonCandidate(ResourcePersistentId theCandidate, EmpiMatchOutcome theEmpiMatchOutcome) {
myCandidatePersonPid = theCandidate;
myEmpiMatchResult = theEmpiMatchResult;
myEmpiMatchOutcome = theEmpiMatchOutcome;
}
public MatchedPersonCandidate(ResourcePersistentId thePersonPid, EmpiLink theEmpiLink) {
myCandidatePersonPid = thePersonPid;
myEmpiMatchOutcome = new EmpiMatchOutcome(theEmpiLink.getVector(), theEmpiLink.getScore()).setMatchResultEnum(theEmpiLink.getMatchResult());
}
public ResourcePersistentId getCandidatePersonPid() {
return myCandidatePersonPid;
}
public EmpiMatchResultEnum getMatchResult() {
return myEmpiMatchResult;
public EmpiMatchOutcome getMatchResult() {
return myEmpiMatchOutcome;
}
public boolean isMatch() {
return myEmpiMatchResult == EmpiMatchResultEnum.MATCH;
return myEmpiMatchOutcome.isMatch();
}
}

View File

@ -10,13 +10,13 @@ import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc;
import ca.uhn.fhir.empi.util.EIDHelper;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.dao.data.IEmpiLinkDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.config.EmpiConsumerConfig;
import ca.uhn.fhir.jpa.empi.config.EmpiSearchParameterLoader;
import ca.uhn.fhir.jpa.empi.config.EmpiSubmitterConfig;
import ca.uhn.fhir.jpa.empi.config.TestEmpiConfigR4;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.empi.matcher.IsLinkedTo;
import ca.uhn.fhir.jpa.empi.matcher.IsMatchedToAPerson;
import ca.uhn.fhir.jpa.empi.matcher.IsPossibleDuplicateOf;
@ -61,13 +61,14 @@ import static org.slf4j.LoggerFactory.getLogger;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {EmpiSubmitterConfig.class, EmpiConsumerConfig.class, TestEmpiConfigR4.class, SubscriptionProcessorConfig.class})
abstract public class BaseEmpiR4Test extends BaseJpaR4Test {
private static final Logger ourLog = getLogger(BaseEmpiR4Test.class);
public static final String NAME_GIVEN_JANE = "Jane";
public static final String NAME_GIVEN_PAUL = "Paul";
public static final String TEST_NAME_FAMILY = "Doe";
protected static final String TEST_ID_SYSTEM = "http://a.tv/";
protected static final String JANE_ID = "ID.JANE.123";
protected static final String PAUL_ID = "ID.PAUL.456";
private static final Logger ourLog = getLogger(BaseEmpiR4Test.class);
private static final ContactPoint TEST_TELECOM = new ContactPoint()
.setSystem(ContactPoint.ContactPointSystem.PHONE)
.setValue("555-555-5555");
@ -360,7 +361,7 @@ abstract public class BaseEmpiR4Test extends BaseJpaR4Test {
Person person = createPerson();
Patient patient = createPatient();
EmpiLink empiLink = new EmpiLink();
EmpiLink empiLink = myEmpiLinkDaoSvc.newEmpiLink();
empiLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
empiLink.setMatchResult(EmpiMatchResultEnum.MATCH);
empiLink.setPersonPid(myIdHelperService.getPidOrNull(person));
@ -372,4 +373,12 @@ abstract public class BaseEmpiR4Test extends BaseJpaR4Test {
myEmpiSearchParameterLoader.daoUpdateEmpiSearchParameters();
mySearchParamRegistry.forceRefresh();
}
protected void logAllLinks() {
ourLog.info("Logging all EMPI Links:");
List<EmpiLink> links = myEmpiLinkDao.findAll();
for (EmpiLink link : links) {
ourLog.info(link.toString());
}
}
}

View File

@ -1,7 +1,8 @@
package ca.uhn.fhir.jpa.empi.dao;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.rules.json.EmpiRulesJson;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.jpa.util.TestUtil;
@ -19,6 +20,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class EmpiLinkDaoSvcTest extends BaseEmpiR4Test {
@Autowired
EmpiLinkDaoSvc myEmpiLinkDaoSvc;
@Autowired
IEmpiSettings myEmpiSettings;
@Test
public void testCreate() {
@ -41,4 +44,12 @@ public class EmpiLinkDaoSvcTest extends BaseEmpiR4Test {
assertNotEquals(updatedLink.getCreated(), updatedLink.getUpdated());
}
@Test
public void testNew() {
EmpiLink newLink = myEmpiLinkDaoSvc.newEmpiLink();
EmpiRulesJson rules = myEmpiSettings.getEmpiRules();
assertEquals("1", rules.getVersion());
assertEquals(rules.getVersion(), newLink.getVersion());
}
}

View File

@ -11,8 +11,8 @@ public class EmpiEnumTest {
public void empiEnumOrdinals() {
// This test is here to enforce that new values in these enums are always added to the end
assertEquals(4, EmpiMatchResultEnum.values().length);
assertEquals(EmpiMatchResultEnum.POSSIBLE_DUPLICATE, EmpiMatchResultEnum.values()[EmpiMatchResultEnum.values().length - 1]);
assertEquals(5, EmpiMatchResultEnum.values().length);
assertEquals(EmpiMatchResultEnum.GOLDEN_RECORD, EmpiMatchResultEnum.values()[EmpiMatchResultEnum.values().length - 1]);
assertEquals(2, EmpiLinkSourceEnum.values().length);
assertEquals(EmpiLinkSourceEnum.MANUAL, EmpiLinkSourceEnum.values()[EmpiLinkSourceEnum.values().length - 1]);

View File

@ -45,7 +45,7 @@ public class EmpiExpungeTest extends BaseEmpiR4Test {
myTargetId = myTargetEntity.getIdDt().toVersionless();
myPersonEntity = (ResourceTable) myPersonDao.create(new Person()).getEntity();
EmpiLink empiLink = new EmpiLink();
EmpiLink empiLink = myEmpiLinkDaoSvc.newEmpiLink();
empiLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
empiLink.setMatchResult(EmpiMatchResultEnum.MATCH);
empiLink.setPersonPid(myPersonEntity.getId());

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.empi.matcher;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import org.hamcrest.TypeSafeMatcher;
import org.hl7.fhir.instance.model.api.IAnyResource;

View File

@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.empi.matcher;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hl7.fhir.instance.model.api.IAnyResource;

View File

@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.empi.matcher;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import org.hamcrest.Description;
import org.hamcrest.Matcher;

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.empi.matcher;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import org.hamcrest.Description;
import org.hamcrest.Matcher;

View File

@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.empi.matcher;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hl7.fhir.instance.model.api.IAnyResource;

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.empi.matcher;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import org.hamcrest.Description;
import org.hamcrest.Matcher;

View File

@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.empi.matcher;
import ca.uhn.fhir.jpa.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hl7.fhir.instance.model.api.IAnyResource;

View File

@ -47,7 +47,8 @@ public class EmpiProviderQueryLinkR4Test extends BaseLinkR4Test {
Person person2 = createPerson();
myPerson2Id = new StringType(person2.getIdElement().toVersionless().getValue());
Long person2Pid = myIdHelperService.getPidOrNull(person2);
EmpiLink possibleDuplicateEmpiLink = new EmpiLink().setPersonPid(person1Pid).setTargetPid(person2Pid).setMatchResult(EmpiMatchResultEnum.POSSIBLE_DUPLICATE).setLinkSource(EmpiLinkSourceEnum.AUTO);
EmpiLink possibleDuplicateEmpiLink = myEmpiLinkDaoSvc.newEmpiLink().setPersonPid(person1Pid).setTargetPid(person2Pid).setMatchResult(EmpiMatchResultEnum.POSSIBLE_DUPLICATE).setLinkSource(EmpiLinkSourceEnum.AUTO);
saveLink(possibleDuplicateEmpiLink);
}
@ -59,12 +60,12 @@ public class EmpiProviderQueryLinkR4Test extends BaseLinkR4Test {
List<Parameters.ParametersParameterComponent> list = result.getParameter();
assertThat(list, hasSize(1));
List<Parameters.ParametersParameterComponent> part = list.get(0).getPart();
assertEmpiLink(4, part, myPersonId.getValue(), myPatientId.getValue(), EmpiMatchResultEnum.POSSIBLE_MATCH);
assertEmpiLink(7, part, myPersonId.getValue(), myPatientId.getValue(), EmpiMatchResultEnum.POSSIBLE_MATCH, "false", "true", null);
}
@Test
public void testQueryLinkThreeMatches() {
// Add a second patient
// Add a third patient
Patient patient = createPatientAndUpdateLinks(buildJanePatient());
IdType patientId = patient.getIdElement().toVersionless();
Person person = getPersonFromTarget(patient);
@ -75,7 +76,7 @@ public class EmpiProviderQueryLinkR4Test extends BaseLinkR4Test {
List<Parameters.ParametersParameterComponent> list = result.getParameter();
assertThat(list, hasSize(3));
List<Parameters.ParametersParameterComponent> part = list.get(2).getPart();
assertEmpiLink(4, part, personId.getValue(), patientId.getValue(), EmpiMatchResultEnum.MATCH);
assertEmpiLink(7, part, personId.getValue(), patientId.getValue(), EmpiMatchResultEnum.MATCH, "false", "false", "2");
}
@Test
@ -85,7 +86,7 @@ public class EmpiProviderQueryLinkR4Test extends BaseLinkR4Test {
List<Parameters.ParametersParameterComponent> list = result.getParameter();
assertThat(list, hasSize(1));
List<Parameters.ParametersParameterComponent> part = list.get(0).getPart();
assertEmpiLink(2, part, myPerson1Id.getValue(), myPerson2Id.getValue(), EmpiMatchResultEnum.POSSIBLE_DUPLICATE);
assertEmpiLink(2, part, myPerson1Id.getValue(), myPerson2Id.getValue(), EmpiMatchResultEnum.POSSIBLE_DUPLICATE, "false", "false", null);
}
@Test
@ -116,7 +117,7 @@ public class EmpiProviderQueryLinkR4Test extends BaseLinkR4Test {
}
}
private void assertEmpiLink(int theExpectedSize, List<Parameters.ParametersParameterComponent> thePart, String thePersonId, String theTargetId, EmpiMatchResultEnum theMatchResult) {
private void assertEmpiLink(int theExpectedSize, List<Parameters.ParametersParameterComponent> thePart, String thePersonId, String theTargetId, EmpiMatchResultEnum theMatchResult, String theEidMatch, String theNewPerson, String theScore) {
assertThat(thePart, hasSize(theExpectedSize));
assertThat(thePart.get(0).getName(), is("personId"));
assertThat(thePart.get(0).getValue().toString(), is(removeVersion(thePersonId)));
@ -127,6 +128,15 @@ public class EmpiProviderQueryLinkR4Test extends BaseLinkR4Test {
assertThat(thePart.get(2).getValue().toString(), is(theMatchResult.name()));
assertThat(thePart.get(3).getName(), is("linkSource"));
assertThat(thePart.get(3).getValue().toString(), is("AUTO"));
assertThat(thePart.get(4).getName(), is("eidMatch"));
assertThat(thePart.get(4).getValue().primitiveValue(), is(theEidMatch));
assertThat(thePart.get(5).getName(), is("newPerson"));
assertThat(thePart.get(5).getValue().primitiveValue(), is(theNewPerson));
assertThat(thePart.get(6).getName(), is("score"));
assertThat(thePart.get(6).getValue().primitiveValue(), is(theScore));
}
}

View File

@ -50,7 +50,7 @@ public class SearchParameterTest extends BaseEmpiR4Test {
ourLog.info("Search result: {}", encoded);
List<Person.PersonLinkComponent> links = person.getLink();
assertEquals(2, links.size());
assertEquals(Person.IdentityAssuranceLevel.LEVEL3, links.get(0).getAssurance());
assertEquals(Person.IdentityAssuranceLevel.LEVEL2, links.get(1).getAssurance());
assertEquals(Person.IdentityAssuranceLevel.LEVEL2, links.get(0).getAssurance());
assertEquals(Person.IdentityAssuranceLevel.LEVEL1, links.get(1).getAssurance());
}
}

View File

@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiCandidateSearchCriteriaBuilderSvc;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
@ -19,7 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class EmpiCandidateSearchCriteriaBuilderSvcTest extends BaseEmpiR4Test {
@Autowired
EmpiCandidateSearchCriteriaBuilderSvc myEmpiCandidateSearchCriteriaBuilderSvc;
EmpiCandidateSearchCriteriaBuilderSvc myEmpiCandidateSearchCriteriaBuilderSvc;
@Test
public void testEmptyCase() {

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiCandidateSearchSvc;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Practitioner;
@ -19,7 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
public class EmpiCandidateSearchSvcTest extends BaseEmpiR4Test {
@Autowired
EmpiCandidateSearchSvc myEmpiCandidateSearchSvc;
EmpiCandidateSearchSvc myEmpiCandidateSearchSvc;
@Test
public void testFindCandidates() {

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
@ -22,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class EmpiLinkSvcTest extends BaseEmpiR4Test {
private static final EmpiMatchOutcome POSSIBLE_MATCH = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.POSSIBLE_MATCH);
@Autowired
IEmpiLinkSvc myEmpiLinkSvc;
@ -36,7 +38,7 @@ public class EmpiLinkSvcTest extends BaseEmpiR4Test {
public void compareEmptyPatients() {
Patient patient = new Patient();
patient.setId("Patient/1");
EmpiMatchResultEnum result = myEmpiResourceMatcherSvc.getMatchResult(patient, patient);
EmpiMatchResultEnum result = myEmpiResourceMatcherSvc.getMatchResult(patient, patient).getMatchResultEnum();
assertEquals(EmpiMatchResultEnum.NO_MATCH, result);
}
@ -49,14 +51,14 @@ public class EmpiLinkSvcTest extends BaseEmpiR4Test {
Patient patient = createPatient();
{
myEmpiLinkSvc.updateLink(person, patient, EmpiMatchResultEnum.POSSIBLE_MATCH, EmpiLinkSourceEnum.AUTO, createContextForCreate());
myEmpiLinkSvc.updateLink(person, patient, POSSIBLE_MATCH, EmpiLinkSourceEnum.AUTO, createContextForCreate());
assertLinkCount(1);
Person newPerson = myPersonDao.read(personId);
assertEquals(1, newPerson.getLink().size());
}
{
myEmpiLinkSvc.updateLink(person, patient, EmpiMatchResultEnum.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
myEmpiLinkSvc.updateLink(person, patient, EmpiMatchOutcome.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
assertLinkCount(1);
Person newPerson = myPersonDao.read(personId);
assertEquals(0, newPerson.getLink().size());
@ -70,7 +72,7 @@ public class EmpiLinkSvcTest extends BaseEmpiR4Test {
Person person = createPerson();
Person target = createPerson();
myEmpiLinkSvc.updateLink(person, target, EmpiMatchResultEnum.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, createContextForCreate());
myEmpiLinkSvc.updateLink(person, target, EmpiMatchOutcome.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, createContextForCreate());
assertLinkCount(1);
}
@ -87,7 +89,7 @@ public class EmpiLinkSvcTest extends BaseEmpiR4Test {
saveNoMatchLink(personPid, targetPid);
myEmpiLinkSvc.updateLink(person, target, EmpiMatchResultEnum.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, createContextForCreate());
myEmpiLinkSvc.updateLink(person, target, EmpiMatchOutcome.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, createContextForCreate());
assertFalse(myEmpiLinkDaoSvc.getEmpiLinksByPersonPidTargetPidAndMatchResult(personPid, targetPid, EmpiMatchResultEnum.POSSIBLE_DUPLICATE).isPresent());
assertLinkCount(1);
}
@ -105,13 +107,13 @@ public class EmpiLinkSvcTest extends BaseEmpiR4Test {
saveNoMatchLink(targetPid, personPid);
myEmpiLinkSvc.updateLink(person, target, EmpiMatchResultEnum.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, createContextForCreate());
myEmpiLinkSvc.updateLink(person, target, EmpiMatchOutcome.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, createContextForCreate());
assertFalse(myEmpiLinkDaoSvc.getEmpiLinksByPersonPidTargetPidAndMatchResult(personPid, targetPid, EmpiMatchResultEnum.POSSIBLE_DUPLICATE).isPresent());
assertLinkCount(1);
}
private void saveNoMatchLink(Long thePersonPid, Long theTargetPid) {
EmpiLink noMatchLink = new EmpiLink()
EmpiLink noMatchLink = myEmpiLinkDaoSvc.newEmpiLink()
.setPersonPid(thePersonPid)
.setTargetPid(theTargetPid)
.setLinkSource(EmpiLinkSourceEnum.MANUAL)
@ -124,9 +126,9 @@ public class EmpiLinkSvcTest extends BaseEmpiR4Test {
Person person = createPerson(buildJanePerson());
Patient patient = createPatient(buildJanePatient());
myEmpiLinkSvc.updateLink(person, patient, EmpiMatchResultEnum.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
myEmpiLinkSvc.updateLink(person, patient, EmpiMatchOutcome.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
try {
myEmpiLinkSvc.updateLink(person, patient, EmpiMatchResultEnum.MATCH, EmpiLinkSourceEnum.AUTO, null);
myEmpiLinkSvc.updateLink(person, patient, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.AUTO, null);
fail();
} catch (InternalErrorException e) {
assertThat(e.getMessage(), is(equalTo("EMPI system is not allowed to modify links on manually created links")));
@ -140,7 +142,7 @@ public class EmpiLinkSvcTest extends BaseEmpiR4Test {
// Test: it should be impossible to have a AUTO NO_MATCH record. The only NO_MATCH records in the system must be MANUAL.
try {
myEmpiLinkSvc.updateLink(person, patient, EmpiMatchResultEnum.NO_MATCH, EmpiLinkSourceEnum.AUTO, null);
myEmpiLinkSvc.updateLink(person, patient, EmpiMatchOutcome.NO_MATCH, EmpiLinkSourceEnum.AUTO, null);
fail();
} catch (InternalErrorException e) {
assertThat(e.getMessage(), is(equalTo("EMPI system is not allowed to automatically NO_MATCH a resource")));
@ -154,8 +156,8 @@ public class EmpiLinkSvcTest extends BaseEmpiR4Test {
Patient patient2 = createPatient(buildJanePatient());
assertEquals(0, myEmpiLinkDao.count());
myEmpiLinkDaoSvc.createOrUpdateLinkEntity(person, patient1, EmpiMatchResultEnum.MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
myEmpiLinkDaoSvc.createOrUpdateLinkEntity(person, patient2, EmpiMatchResultEnum.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
myEmpiLinkDaoSvc.createOrUpdateLinkEntity(person, patient1, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
myEmpiLinkDaoSvc.createOrUpdateLinkEntity(person, patient2, EmpiMatchOutcome.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
myEmpiLinkSvc.syncEmpiLinksToPersonLinks(person, createContextForCreate());
assertTrue(person.hasLink());
assertEquals(patient1.getIdElement().toVersionless().getValue(), person.getLinkFirstRep().getTarget().getReference());

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.model.CanonicalEID;
import ca.uhn.fhir.empi.util.EIDHelper;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
@ -15,15 +16,20 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.TestPropertySource;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static ca.uhn.fhir.empi.api.EmpiMatchResultEnum.MATCH;
import static ca.uhn.fhir.empi.api.EmpiMatchResultEnum.POSSIBLE_DUPLICATE;
import static ca.uhn.fhir.empi.api.EmpiMatchResultEnum.POSSIBLE_MATCH;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.in;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.slf4j.LoggerFactory.getLogger;
@TestPropertySource(properties = {
@ -43,6 +49,9 @@ public class EmpiMatchLinkSvcMultipleEidModeTest extends BaseEmpiR4Test {
public void testIncomingPatientWithEIDThatMatchesPersonWithHapiEidAddsExternalEidsToPerson() {
// Existing Person with system-assigned EID found linked from matched Patient. incoming Patient has EID. Replace Person system-assigned EID with Patient EID.
Patient patient = createPatientAndUpdateLinks(buildJanePatient());
assertLinksMatchResult(MATCH);
assertLinksNewPerson(true);
assertLinksMatchedByEid(false);
Person janePerson = getPersonFromTarget(patient);
List<CanonicalEID> hapiEid = myEidHelper.getHapiEid(janePerson);
@ -52,6 +61,9 @@ public class EmpiMatchLinkSvcMultipleEidModeTest extends BaseEmpiR4Test {
addExternalEID(janePatient, "12345");
addExternalEID(janePatient, "67890");
createPatientAndUpdateLinks(janePatient);
assertLinksMatchResult(MATCH, MATCH);
assertLinksNewPerson(true, false);
assertLinksMatchedByEid(false, false);
//We want to make sure the patients were linked to the same person.
assertThat(patient, is(samePersonAs(janePatient)));
@ -75,6 +87,25 @@ public class EmpiMatchLinkSvcMultipleEidModeTest extends BaseEmpiR4Test {
assertThat(thirdIdentifier.getValue(), is(equalTo("67890")));
}
private void assertLinksMatchResult(EmpiMatchResultEnum... theExpectedValues) {
assertFields(EmpiLink::getMatchResult, theExpectedValues);
}
private void assertLinksNewPerson(Boolean... theExpectedValues) {
assertFields(EmpiLink::getNewPerson, theExpectedValues);
}
private void assertLinksMatchedByEid(Boolean... theExpectedValues) {
assertFields(EmpiLink::getEidMatch, theExpectedValues);
}
private <T> void assertFields(Function<EmpiLink, T> theAccessor, T... theExpectedValues) {
List<EmpiLink> links = myEmpiLinkDao.findAll();
assertEquals(theExpectedValues.length, links.size());
for (int i = 0; i < links.size(); ++i) {
assertEquals(theExpectedValues[i], theAccessor.apply(links.get(i)), "Value at index " + i + " was not equal");
}
}
@Test
// Test Case #4
@ -86,11 +117,17 @@ public class EmpiMatchLinkSvcMultipleEidModeTest extends BaseEmpiR4Test {
addExternalEID(patient1, "id_3");
addExternalEID(patient1, "id_4");
createPatientAndUpdateLinks(patient1);
assertLinksMatchResult(MATCH);
assertLinksNewPerson(true);
assertLinksMatchedByEid(false);
Patient patient2 = buildPaulPatient();
addExternalEID(patient2, "id_5");
addExternalEID(patient2, "id_1");
patient2 = createPatientAndUpdateLinks(patient2);
assertLinksMatchResult(MATCH, MATCH);
assertLinksNewPerson(true, false);
assertLinksMatchedByEid(false, true);
assertThat(patient1, is(samePersonAs(patient2)));
@ -102,13 +139,14 @@ public class EmpiMatchLinkSvcMultipleEidModeTest extends BaseEmpiR4Test {
assertThat(personFromTarget.getIdentifier(), hasSize(5));
updatePatientAndUpdateLinks(patient2);
assertLinksMatchResult(MATCH, MATCH);
assertLinksNewPerson(true, false);
assertLinksMatchedByEid(false, true);
assertThat(patient1, is(samePersonAs(patient2)));
personFromTarget = getPersonFromTarget(patient2);
assertThat(personFromTarget.getIdentifier(), hasSize(6));
}
@Test
@ -118,16 +156,21 @@ public class EmpiMatchLinkSvcMultipleEidModeTest extends BaseEmpiR4Test {
addExternalEID(patient1, "eid-1");
addExternalEID(patient1, "eid-11");
patient1 = createPatientAndUpdateLinks(patient1);
assertLinksMatchResult(MATCH);
assertLinksNewPerson(true);
assertLinksMatchedByEid(false);
Patient patient2 = buildJanePatient();
addExternalEID(patient2, "eid-2");
addExternalEID(patient2, "eid-22");
patient2 = createPatientAndUpdateLinks(patient2);
assertLinksMatchResult(MATCH, MATCH, POSSIBLE_DUPLICATE);
assertLinksNewPerson(true, true, false);
assertLinksMatchedByEid(false, false, false);
List<EmpiLink> possibleDuplicates = myEmpiLinkDaoSvc.getPossibleDuplicates();
assertThat(possibleDuplicates, hasSize(1));
List<Long> duplicatePids = Stream.of(patient1, patient2)
.map(this::getPersonFromTarget)
.map(myIdHelperService::getPidOrNull)
@ -146,26 +189,39 @@ public class EmpiMatchLinkSvcMultipleEidModeTest extends BaseEmpiR4Test {
addExternalEID(patient1, "eid-1");
addExternalEID(patient1, "eid-11");
patient1 = createPatientAndUpdateLinks(patient1);
assertLinksMatchResult(MATCH);
assertLinksNewPerson(true);
assertLinksMatchedByEid(false);
Patient patient2 = buildPaulPatient();
addExternalEID(patient2, "eid-2");
addExternalEID(patient2, "eid-22");
patient2 = createPatientAndUpdateLinks(patient2);
assertLinksMatchResult(MATCH, MATCH);
assertLinksNewPerson(true, true);
assertLinksMatchedByEid(false, false);
Patient patient3 = buildPaulPatient();
addExternalEID(patient3, "eid-22");
patient3 = createPatientAndUpdateLinks(patient3);
assertLinksMatchResult(MATCH, MATCH, MATCH);
assertLinksNewPerson(true, true, false);
assertLinksMatchedByEid(false, false, true);
//Now, Patient 2 and 3 are linked, and the person has 2 eids.
assertThat(patient2, is(samePersonAs(patient3)));
//Now lets change one of the EIDs on an incoming patient to one that matches our original patient.
//This should create a situation in which the incoming EIDs are matched to _two_ unique patients. In this case, we want to
//Now lets change one of the EIDs on the second patient to one that matches our original patient.
//This should create a situation in which the incoming EIDs are matched to _two_ different persons. In this case, we want to
//set them all to possible_match, and set the two persons as possible duplicates.
patient2.getIdentifier().clear();
addExternalEID(patient2, "eid-11");
addExternalEID(patient2, "eid-22");
patient2 = updatePatientAndUpdateLinks(patient2);
logAllLinks();
assertLinksMatchResult(MATCH, POSSIBLE_MATCH, MATCH, POSSIBLE_MATCH, POSSIBLE_DUPLICATE);
assertLinksNewPerson(true, true, false, false, false);
assertLinksMatchedByEid(false, true, true, true, true);
assertThat(patient2, is(not(matchedToAPerson())));
assertThat(patient2, is(possibleMatchWith(patient1)));

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.empi.model.CanonicalEID;
import ca.uhn.fhir.empi.util.EIDHelper;
@ -87,7 +87,7 @@ public class EmpiMatchLinkSvcTest extends BaseEmpiR4Test {
//Create a manual NO_MATCH between janePerson and unmatchedJane.
Patient unmatchedJane = createPatient(buildJanePatient());
myEmpiLinkSvc.updateLink(janePerson, unmatchedJane, EmpiMatchResultEnum.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
myEmpiLinkSvc.updateLink(janePerson, unmatchedJane, EmpiMatchOutcome.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
//rerun EMPI rules against unmatchedJane.
myEmpiMatchLinkSvc.updateEmpiLinksForEmpiTarget(unmatchedJane, createContextForCreate());
@ -105,7 +105,7 @@ public class EmpiMatchLinkSvcTest extends BaseEmpiR4Test {
Patient unmatchedPatient = createPatient(buildJanePatient());
//This simulates an admin specifically saying that unmatchedPatient does NOT match janePerson.
myEmpiLinkSvc.updateLink(janePerson, unmatchedPatient, EmpiMatchResultEnum.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
myEmpiLinkSvc.updateLink(janePerson, unmatchedPatient, EmpiMatchOutcome.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
//TODO change this so that it will only partially match.
//Now normally, when we run update links, it should link to janePerson. However, this manual NO_MATCH link
@ -317,7 +317,7 @@ public class EmpiMatchLinkSvcTest extends BaseEmpiR4Test {
//In a normal situation, janePatient2 would just match to jane patient, but here we need to hack it so they are their
//own individual Persons for the purpose of this test.
IAnyResource person = myPersonHelper.createPersonFromEmpiTarget(janePatient2);
myEmpiLinkSvc.updateLink(person, janePatient2, EmpiMatchResultEnum.MATCH, EmpiLinkSourceEnum.AUTO, createContextForCreate());
myEmpiLinkSvc.updateLink(person, janePatient2, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.AUTO, createContextForCreate());
assertThat(janePatient, is(not(samePersonAs(janePatient2))));
//In theory, this will match both Persons!
@ -386,20 +386,20 @@ public class EmpiMatchLinkSvcTest extends BaseEmpiR4Test {
Person.PersonLinkComponent linkFirstRep = janePerson.getLinkFirstRep();
assertThat(linkFirstRep.getTarget().getReference(), is(equalTo(patient.getIdElement().toVersionless().toString())));
assertThat(linkFirstRep.getAssurance(), is(equalTo(Person.IdentityAssuranceLevel.LEVEL3)));
assertThat(linkFirstRep.getAssurance(), is(equalTo(Person.IdentityAssuranceLevel.LEVEL2)));
}
@Test
public void testManualMatchesGenerateAssuranceLevel4() {
Patient patient = createPatientAndUpdateLinks(buildJanePatient());
Person janePerson = getPersonFromTarget(patient);
myEmpiLinkSvc.updateLink(janePerson, patient, EmpiMatchResultEnum.MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
myEmpiLinkSvc.updateLink(janePerson, patient, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
janePerson = getPersonFromTarget(patient);
Person.PersonLinkComponent linkFirstRep = janePerson.getLinkFirstRep();
assertThat(linkFirstRep.getTarget().getReference(), is(equalTo(patient.getIdElement().toVersionless().toString())));
assertThat(linkFirstRep.getAssurance(), is(equalTo(Person.IdentityAssuranceLevel.LEVEL4)));
assertThat(linkFirstRep.getAssurance(), is(equalTo(Person.IdentityAssuranceLevel.LEVEL3)));
}
//Case #1

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
@ -28,8 +29,8 @@ import java.util.Collections;
import java.util.List;
import java.util.UUID;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
@ -39,6 +40,7 @@ public class EmpiPersonMergerSvcTest extends BaseEmpiR4Test {
public static final String FAMILY_NAME = "Chan";
public static final String POSTAL_CODE = "M6G 1B4";
private static final String BAD_GIVEN_NAME = "Bob";
private static final EmpiMatchOutcome POSSIBLE_MATCH = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.POSSIBLE_MATCH);
@Autowired
IEmpiPersonMergerSvc myEmpiPersonMergerSvc;
@ -107,7 +109,7 @@ public class EmpiPersonMergerSvcTest extends BaseEmpiR4Test {
@Test
public void mergeRemovesPossibleDuplicatesLink() {
EmpiLink empiLink = new EmpiLink().setPersonPid(myToPersonPid).setTargetPid(myFromPersonPid).setMatchResult(EmpiMatchResultEnum.POSSIBLE_DUPLICATE).setLinkSource(EmpiLinkSourceEnum.AUTO);
EmpiLink empiLink = myEmpiLinkDaoSvc.newEmpiLink().setPersonPid(myToPersonPid).setTargetPid(myFromPersonPid).setMatchResult(EmpiMatchResultEnum.POSSIBLE_DUPLICATE).setLinkSource(EmpiLinkSourceEnum.AUTO);
saveLink(empiLink);
assertEquals(1, myEmpiLinkDao.count());
mergePersons();
@ -371,7 +373,7 @@ public class EmpiPersonMergerSvcTest extends BaseEmpiR4Test {
private EmpiLink createEmpiLink(Person thePerson, Patient theTargetPatient) {
thePerson.addLink().setTarget(new Reference(theTargetPatient));
return myEmpiLinkDaoSvc.createOrUpdateLinkEntity(thePerson, theTargetPatient, EmpiMatchResultEnum.POSSIBLE_MATCH, EmpiLinkSourceEnum.AUTO, createContextForCreate());
return myEmpiLinkDaoSvc.createOrUpdateLinkEntity(thePerson, theTargetPatient, POSSIBLE_MATCH, EmpiLinkSourceEnum.AUTO, createContextForCreate());
}
private void populatePerson(Person thePerson) {

View File

@ -1,4 +1,5 @@
{
"version": "1",
"candidateSearchParams": [
{
"resourceType": "Patient",

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.migrate.tasks;
* #L%
*/
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
import ca.uhn.fhir.jpa.migrate.taskdef.ArbitrarySqlTask;
import ca.uhn.fhir.jpa.migrate.taskdef.CalculateHashesTask;
@ -126,11 +127,24 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks<VersionEnum> {
Builder.BuilderWithTableName pkgVerMod = version.onTable("NPM_PACKAGE_VER");
pkgVerMod.modifyColumn("20200629.1", "PKG_DESC").nullable().withType(ColumnTypeEnum.STRING, 200);
pkgVerMod.modifyColumn("20200629.2", "DESC_UPPER").nullable().withType(ColumnTypeEnum.STRING, 200);
init510_20200706_to_20200714();
Builder.BuilderWithTableName empiLink = version.onTable("MPI_LINK");
empiLink.addColumn("20200715.1", "VERSION").nonNullable().type(ColumnTypeEnum.STRING, EmpiLink.VERSION_LENGTH);
empiLink.addColumn("20200715.2", "EID_MATCH").nullable().type(ColumnTypeEnum.BOOLEAN);
empiLink.addColumn("20200715.3", "NEW_PERSON").nullable().type(ColumnTypeEnum.BOOLEAN);
empiLink.addColumn("20200715.4", "VECTOR").nullable().type(ColumnTypeEnum.LONG);
empiLink.addColumn("20200715.5", "SCORE").nullable().type(ColumnTypeEnum.FLOAT);
}
protected void init510_20200610() {
}
protected void init510_20200706_to_20200714() {
}
private void init501() { //20200514 - present
Builder version = forVersion(VersionEnum.V5_0_1);

View File

@ -0,0 +1,15 @@
package ca.uhn.fhir.empi.api;
public class EmpiMatchEvaluation {
public final boolean match;
public final double score;
public EmpiMatchEvaluation(boolean theMatch, double theScore) {
match = theMatch;
score = theScore;
}
public static EmpiMatchEvaluation max(EmpiMatchEvaluation theLeft, EmpiMatchEvaluation theRight) {
return new EmpiMatchEvaluation(theLeft.match | theRight.match, Math.max(theLeft.score, theRight.score));
}
}

View File

@ -0,0 +1,86 @@
package ca.uhn.fhir.empi.api;
/**
* This data object captures the final outcome of an EMPI match
*/
public final class EmpiMatchOutcome {
public static final EmpiMatchOutcome POSSIBLE_DUPLICATE = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.POSSIBLE_DUPLICATE);
public static final EmpiMatchOutcome NO_MATCH = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.NO_MATCH);
public static final EmpiMatchOutcome NEW_PERSON_MATCH = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.MATCH).setNewPerson(true);
public static final EmpiMatchOutcome EID_MATCH = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.MATCH).setEidMatch(true);
public static final EmpiMatchOutcome EID_POSSIBLE_MATCH = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.POSSIBLE_MATCH).setEidMatch(true);
public static final EmpiMatchOutcome EID_POSSIBLE_DUPLICATE = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.POSSIBLE_DUPLICATE).setEidMatch(true);
/**
* A bitmap that indicates which rules matched
*/
public final Long vector;
/**
* The sum of all scores for all rules evaluated. Similarity rules add the similarity score (between 0.0 and 1.0) whereas
* matcher rules add either a 0.0 or 1.0.
*/
public final Double score;
/**
* Did the EMPI match operation result in creating a new Person resource?
*/
private boolean myNewPerson;
/**
* Did the EMPI match occur as a result of EIDs matching?
*/
private boolean myEidMatch;
/**
* Based on the EMPI Rules, what was the final match result?
*/
private EmpiMatchResultEnum myMatchResultEnum;
public EmpiMatchOutcome(Long theVector, Double theScore) {
vector = theVector;
score = theScore;
}
public boolean isMatch() {
return myMatchResultEnum == EmpiMatchResultEnum.MATCH;
}
public boolean isPossibleMatch() {
return myMatchResultEnum == EmpiMatchResultEnum.POSSIBLE_MATCH;
}
public boolean isPossibleDuplicate() {
return myMatchResultEnum == EmpiMatchResultEnum.POSSIBLE_DUPLICATE;
}
public EmpiMatchResultEnum getMatchResultEnum() {
return myMatchResultEnum;
}
public EmpiMatchOutcome setMatchResultEnum(EmpiMatchResultEnum theMatchResultEnum) {
myMatchResultEnum = theMatchResultEnum;
return this;
}
public boolean isNewPerson() {
return myNewPerson;
}
/** @param theNewPerson this match is creating a new person */
public EmpiMatchOutcome setNewPerson(boolean theNewPerson) {
myNewPerson = theNewPerson;
return this;
}
public boolean isEidMatch() {
return myEidMatch;
}
/** @param theEidMatch the link was established via a shared EID */
public EmpiMatchOutcome setEidMatch(boolean theEidMatch) {
myEidMatch = theEidMatch;
return this;
}
}

View File

@ -39,7 +39,12 @@ public enum EmpiMatchResultEnum {
/**
* Link between two Person resources indicating they may be duplicates.
*/
POSSIBLE_DUPLICATE
POSSIBLE_DUPLICATE,
/**
* Link between Person and Target pointing to the Golden Record for that Person
*/
GOLDEN_RECORD
// Stored in database as ORDINAL. Only add new values to bottom!
}

View File

@ -34,7 +34,7 @@ public interface IEmpiLinkSvc {
* @param theLinkSource MANUAL or AUTO: what caused the link.
* @param theEmpiTransactionContext
*/
void updateLink(IAnyResource thePerson, IAnyResource theTargetResource, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiTransactionContext);
void updateLink(IAnyResource thePerson, IAnyResource theTargetResource, EmpiMatchOutcome theMatchResult, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiTransactionContext);
/**
* Replace Person.link values from what they should be based on EmpiLink values

View File

@ -0,0 +1,7 @@
package ca.uhn.fhir.empi.api;
import ca.uhn.fhir.empi.rules.json.EmpiRulesJson;
public interface IEmpiRuleValidator {
void validate(EmpiRulesJson theEmpiRules);
}

View File

@ -35,4 +35,6 @@ public interface IEmpiSettings {
boolean isPreventEidUpdates();
boolean isPreventMultipleEids();
String getRuleVersion();
}

View File

@ -25,9 +25,9 @@ import org.hl7.fhir.instance.model.api.IAnyResource;
public class MatchedTarget {
private final IAnyResource myTarget;
private final EmpiMatchResultEnum myMatchResult;
private final EmpiMatchOutcome myMatchResult;
public MatchedTarget(IAnyResource theTarget, EmpiMatchResultEnum theMatchResult) {
public MatchedTarget(IAnyResource theTarget, EmpiMatchOutcome theMatchResult) {
myTarget = theTarget;
myMatchResult = theMatchResult;
}
@ -36,15 +36,15 @@ public class MatchedTarget {
return myTarget;
}
public EmpiMatchResultEnum getMatchResult() {
public EmpiMatchOutcome getMatchResult() {
return myMatchResult;
}
public boolean isMatch() {
return myMatchResult == EmpiMatchResultEnum.MATCH;
return myMatchResult.isMatch();
}
public boolean isPossibleMatch() {
return myMatchResult == EmpiMatchResultEnum.POSSIBLE_MATCH;
return myMatchResult.isPossibleMatch();
}
}

View File

@ -23,6 +23,7 @@ package ca.uhn.fhir.empi.rules.config;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.IEmpiRuleValidator;
import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson;
import ca.uhn.fhir.empi.rules.json.EmpiFilterSearchParamJson;
import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson;
@ -42,7 +43,7 @@ import java.util.HashSet;
import java.util.Set;
@Service
public class EmpiRuleValidator {
public class EmpiRuleValidator implements IEmpiRuleValidator {
private static final Logger ourLog = LoggerFactory.getLogger(EmpiRuleValidator.class);
private final FhirContext myFhirContext;

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.empi.rules.config;
* #L%
*/
import ca.uhn.fhir.empi.api.IEmpiRuleValidator;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.rules.json.EmpiRulesJson;
import ca.uhn.fhir.util.JsonUtil;
@ -30,7 +31,7 @@ import java.io.IOException;
@Component
public class EmpiSettings implements IEmpiSettings {
private final EmpiRuleValidator myEmpiRuleValidator;
private final IEmpiRuleValidator myEmpiRuleValidator;
private boolean myEnabled;
private int myConcurrentConsumers = EMPI_DEFAULT_CONCURRENT_CONSUMERS;
@ -48,7 +49,7 @@ public class EmpiSettings implements IEmpiSettings {
private boolean myPreventMultipleEids;
@Autowired
public EmpiSettings(EmpiRuleValidator theEmpiRuleValidator) {
public EmpiSettings(IEmpiRuleValidator theEmpiRuleValidator) {
myEmpiRuleValidator = theEmpiRuleValidator;
}
@ -107,6 +108,11 @@ public class EmpiSettings implements IEmpiSettings {
return myPreventMultipleEids;
}
@Override
public String getRuleVersion() {
return myEmpiRules.getVersion();
}
public EmpiSettings setPreventMultipleEids(boolean thePreventMultipleEids) {
myPreventMultipleEids = thePreventMultipleEids;
return this;

View File

@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.util.StdConverter;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.Validate;
import java.util.ArrayList;
import java.util.Collections;
@ -35,6 +36,8 @@ import java.util.Map;
@JsonDeserialize(converter = EmpiRulesJson.EmpiRulesJsonConverter.class)
public class EmpiRulesJson implements IModelJson {
@JsonProperty(value = "version", required = true)
String myVersion;
@JsonProperty(value = "candidateSearchParams", required = true)
List<EmpiResourceSearchParamJson> myCandidateSearchParams = new ArrayList<>();
@JsonProperty(value = "candidateFilterSearchParams", required = true)
@ -112,22 +115,17 @@ public class EmpiRulesJson implements IModelJson {
myEnterpriseEIDSystem = theEnterpriseEIDSystem;
}
/**
* Ensure the vector map is initialized after we deserialize
*/
static class EmpiRulesJsonConverter extends StdConverter<EmpiRulesJson, EmpiRulesJson> {
public String getVersion() {
return myVersion;
}
/**
* This empty constructor is required by Jackson
*/
public EmpiRulesJsonConverter() {
}
public EmpiRulesJson setVersion(String theVersion) {
myVersion = theVersion;
return this;
}
@Override
public EmpiRulesJson convert(EmpiRulesJson theEmpiRulesJson) {
theEmpiRulesJson.initialize();
return theEmpiRulesJson;
}
private void validate() {
Validate.notBlank(myVersion, "version may not be blank");
}
public String getSummary() {
@ -157,4 +155,23 @@ public class EmpiRulesJson implements IModelJson {
VectorMatchResultMap getVectorMatchResultMapForUnitTest() {
return myVectorMatchResultMap;
}
/**
* Ensure the vector map is initialized after we deserialize
*/
static class EmpiRulesJsonConverter extends StdConverter<EmpiRulesJson, EmpiRulesJson> {
/**
* This empty constructor is required by Jackson
*/
public EmpiRulesJsonConverter() {
}
@Override
public EmpiRulesJson convert(EmpiRulesJson theEmpiRulesJson) {
theEmpiRulesJson.validate();
theEmpiRulesJson.initialize();
return theEmpiRulesJson;
}
}
}

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.empi.rules.metric;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.phonetic.PhoneticEncoderEnum;
import ca.uhn.fhir.empi.api.EmpiMatchEvaluation;
import ca.uhn.fhir.empi.rules.metric.matcher.EmpiPersonNameMatchModeEnum;
import ca.uhn.fhir.empi.rules.metric.matcher.HapiDateMatcher;
import ca.uhn.fhir.empi.rules.metric.matcher.HapiStringMatcher;
@ -77,15 +78,25 @@ public enum EmpiMetricEnum {
return ((IEmpiFieldMatcher) myEmpiFieldMetric).matches(theFhirContext, theLeftBase, theRightBase, theExact);
}
public boolean match(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, @Nullable Double theThreshold) {
public EmpiMatchEvaluation match(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, @Nullable Double theThreshold) {
if (isSimilarity()) {
return ((IEmpiFieldSimilarity) myEmpiFieldMetric).similarity(theFhirContext, theLeftBase, theRightBase, theExact) >= theThreshold;
return matchBySimilarity((IEmpiFieldSimilarity) myEmpiFieldMetric, theFhirContext, theLeftBase, theRightBase, theExact, theThreshold);
} else {
return ((IEmpiFieldMatcher) myEmpiFieldMetric).matches(theFhirContext, theLeftBase, theRightBase, theExact);
return matchByMatcher((IEmpiFieldMatcher) myEmpiFieldMetric, theFhirContext, theLeftBase, theRightBase, theExact);
}
}
public boolean isSimilarity() {
private EmpiMatchEvaluation matchBySimilarity(IEmpiFieldSimilarity theSimilarity, FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, Double theThreshold) {
double similarityResult = theSimilarity.similarity(theFhirContext, theLeftBase, theRightBase, theExact);
return new EmpiMatchEvaluation(similarityResult >= theThreshold, similarityResult);
}
private EmpiMatchEvaluation matchByMatcher(IEmpiFieldMatcher theMatcher, FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) {
boolean matcherResult = theMatcher.matches(theFhirContext, theLeftBase, theRightBase, theExact);
return new EmpiMatchEvaluation(matcherResult, matcherResult ? 1.0 : 0.0);
}
public boolean isSimilarity() {
return myEmpiFieldMetric instanceof IEmpiFieldSimilarity;
}
}

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.empi.rules.svc;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiMatchEvaluation;
import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson;
import ca.uhn.fhir.util.FhirTerser;
import org.apache.commons.lang3.Validate;
@ -58,7 +59,7 @@ public class EmpiResourceFieldMatcher {
* @return A boolean indicating whether they match.
*/
@SuppressWarnings("rawtypes")
public boolean match(IBaseResource theLeftResource, IBaseResource theRightResource) {
public EmpiMatchEvaluation match(IBaseResource theLeftResource, IBaseResource theRightResource) {
validate(theLeftResource);
validate(theRightResource);
@ -69,17 +70,18 @@ public class EmpiResourceFieldMatcher {
}
@SuppressWarnings("rawtypes")
private boolean match(List<IBase> theLeftValues, List<IBase> theRightValues) {
boolean retval = false;
private EmpiMatchEvaluation match(List<IBase> theLeftValues, List<IBase> theRightValues) {
EmpiMatchEvaluation retval = new EmpiMatchEvaluation(false, 0.0);
for (IBase leftValue : theLeftValues) {
for (IBase rightValue : theRightValues) {
retval |= match(leftValue, rightValue);
EmpiMatchEvaluation nextMatch = match(leftValue, rightValue);
retval = EmpiMatchEvaluation.max(retval, nextMatch);
}
}
return retval;
}
private boolean match(IBase theLeftValue, IBase theRightValue) {
private EmpiMatchEvaluation match(IBase theLeftValue, IBase theRightValue) {
return myEmpiFieldMatchJson.getMetric().match(myFhirContext, theLeftValue, theRightValue, myEmpiFieldMatchJson.getExact(), myEmpiFieldMatchJson.getMatchThreshold());
}

View File

@ -22,6 +22,8 @@ package ca.uhn.fhir.empi.rules.svc;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiMatchEvaluation;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.log.Logs;
@ -47,19 +49,19 @@ public class EmpiResourceMatcherSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
private final FhirContext myFhirContext;
private final IEmpiSettings myEmpiConfig;
private final IEmpiSettings myEmpiSettings;
private EmpiRulesJson myEmpiRulesJson;
private final List<EmpiResourceFieldMatcher> myFieldMatchers = new ArrayList<>();
@Autowired
public EmpiResourceMatcherSvc(FhirContext theFhirContext, IEmpiSettings theEmpiConfig) {
public EmpiResourceMatcherSvc(FhirContext theFhirContext, IEmpiSettings theEmpiSettings) {
myFhirContext = theFhirContext;
myEmpiConfig = theEmpiConfig;
myEmpiSettings = theEmpiSettings;
}
@PostConstruct
public void init() {
myEmpiRulesJson = myEmpiConfig.getEmpiRules();
myEmpiRulesJson = myEmpiSettings.getEmpiRules();
if (myEmpiRulesJson == null) {
throw new ConfigurationException("Failed to load EMPI Rules. If EMPI is enabled, then EMPI rules must be available in context.");
}
@ -78,18 +80,19 @@ public class EmpiResourceMatcherSvc {
*
* @return an {@link EmpiMatchResultEnum} indicating the result of the comparison.
*/
public EmpiMatchResultEnum getMatchResult(IBaseResource theLeftResource, IBaseResource theRightResource) {
public EmpiMatchOutcome getMatchResult(IBaseResource theLeftResource, IBaseResource theRightResource) {
return match(theLeftResource, theRightResource);
}
EmpiMatchResultEnum match(IBaseResource theLeftResource, IBaseResource theRightResource) {
long matchVector = getMatchVector(theLeftResource, theRightResource);
EmpiMatchResultEnum matchResult = myEmpiRulesJson.getMatchResult(matchVector);
EmpiMatchOutcome match(IBaseResource theLeftResource, IBaseResource theRightResource) {
EmpiMatchOutcome matchResult = getMatchOutcome(theLeftResource, theRightResource);
EmpiMatchResultEnum matchResultEnum = myEmpiRulesJson.getMatchResult(matchResult.vector);
matchResult.setMatchResultEnum(matchResultEnum);
if (ourLog.isDebugEnabled()) {
if (matchResult == EmpiMatchResultEnum.MATCH || matchResult == EmpiMatchResultEnum.POSSIBLE_MATCH) {
ourLog.debug("{} {} with field matchers {}", matchResult, theRightResource.getIdElement().toUnqualifiedVersionless(), myEmpiRulesJson.getFieldMatchNamesForVector(matchVector));
if (matchResult.isMatch() || matchResult.isPossibleMatch()) {
ourLog.debug("{} {} with field matchers {}", matchResult, theRightResource.getIdElement().toUnqualifiedVersionless(), myEmpiRulesJson.getFieldMatchNamesForVector(matchResult.vector));
} else if (ourLog.isTraceEnabled()) {
ourLog.trace("{} {}. Field matcher results: {}", matchResult, theRightResource.getIdElement().toUnqualifiedVersionless(), myEmpiRulesJson.getDetailedFieldMatchResultForUnmatchedVector(matchVector));
ourLog.trace("{} {}. Field matcher results: {}", matchResult, theRightResource.getIdElement().toUnqualifiedVersionless(), myEmpiRulesJson.getDetailedFieldMatchResultForUnmatchedVector(matchResult.vector));
}
}
return matchResult;
@ -110,15 +113,18 @@ public class EmpiResourceMatcherSvc {
* 0001|0010 = 0011
* The binary string is now `0011`, which when you return it as a long becomes `3`.
*/
private long getMatchVector(IBaseResource theLeftResource, IBaseResource theRightResource) {
long retval = 0;
private EmpiMatchOutcome getMatchOutcome(IBaseResource theLeftResource, IBaseResource theRightResource) {
long vector = 0;
double score = 0.0;
for (int i = 0; i < myFieldMatchers.size(); ++i) {
//any that are not for the resourceType in question.
EmpiResourceFieldMatcher fieldComparator = myFieldMatchers.get(i);
if (fieldComparator.match(theLeftResource, theRightResource)) {
retval |= (1 << i);
EmpiMatchEvaluation matchEvaluation = fieldComparator.match(theLeftResource, theRightResource);
if (matchEvaluation.match) {
vector |= (1 << i);
}
score += matchEvaluation.score;
}
return retval;
return new EmpiMatchOutcome(vector, score);
}
}

View File

@ -47,9 +47,9 @@ public final class AssuranceLevelUtil {
private static CanonicalIdentityAssuranceLevel getAssuranceFromAutoResult(EmpiMatchResultEnum theMatchResult) {
switch (theMatchResult) {
case MATCH:
return CanonicalIdentityAssuranceLevel.LEVEL3;
case POSSIBLE_MATCH:
return CanonicalIdentityAssuranceLevel.LEVEL2;
case POSSIBLE_MATCH:
return CanonicalIdentityAssuranceLevel.LEVEL1;
case POSSIBLE_DUPLICATE:
case NO_MATCH:
default:
@ -60,7 +60,7 @@ public final class AssuranceLevelUtil {
private static CanonicalIdentityAssuranceLevel getAssuranceFromManualResult(EmpiMatchResultEnum theMatchResult) {
switch (theMatchResult) {
case MATCH:
return CanonicalIdentityAssuranceLevel.LEVEL4;
return CanonicalIdentityAssuranceLevel.LEVEL3;
case NO_MATCH:
case POSSIBLE_DUPLICATE:
case POSSIBLE_MATCH:

View File

@ -1,6 +1,8 @@
package ca.uhn.fhir.empi;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator;
import ca.uhn.fhir.empi.rules.config.EmpiSettings;
import ca.uhn.fhir.empi.rules.json.EmpiRulesJson;
@ -8,9 +10,9 @@ import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc;
import ca.uhn.fhir.rest.server.util.ISearchParamRetriever;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
@ExtendWith(MockitoExtension.class)
@ -37,4 +39,16 @@ public abstract class BaseR4Test {
retval.init();
return retval;
}
protected void assertMatch(EmpiMatchResultEnum theExpectedMatchEnum, EmpiMatchOutcome theMatchResult) {
assertEquals(theExpectedMatchEnum, theMatchResult.getMatchResultEnum());
}
protected void assertMatchResult(EmpiMatchResultEnum theExpectedMatchEnum, long theExpectedVector, double theExpectedScore, boolean theExpectedNewPerson, boolean theExpectedEidMatch, EmpiMatchOutcome theMatchResult) {
assertEquals(theExpectedScore, theMatchResult.score, 0.001);
assertEquals(theExpectedVector, theMatchResult.vector);
assertEquals(theExpectedEidMatch, theMatchResult.isEidMatch());
assertEquals(theExpectedNewPerson, theMatchResult.isNewPerson());
assertEquals(theExpectedMatchEnum, theMatchResult.getMatchResultEnum());
}
}

View File

@ -12,6 +12,8 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
@ -27,6 +29,16 @@ public class EmpiRulesJsonR4Test extends BaseEmpiRulesR4Test {
myRules = buildActiveBirthdateIdRules();
}
@Test
public void testValidate() throws IOException {
EmpiRulesJson rules = new EmpiRulesJson();
try {
JsonUtil.serialize(rules);
} catch (NullPointerException e) {
assertThat(e.getMessage(), containsString("version may not be blank"));
}
}
@Test
public void testSerDeser() throws IOException {
String json = JsonUtil.serialize(myRules);

View File

@ -51,6 +51,7 @@ public abstract class BaseEmpiRulesR4Test extends BaseR4Test {
.setMatchThreshold(NAME_THRESHOLD);
EmpiRulesJson retval = new EmpiRulesJson();
retval.setVersion("test version");
retval.addResourceSearchParam(patientBirthdayBlocking);
retval.addResourceSearchParam(patientIdentifierBlocking);
retval.addFilterSearchParam(activePatientsBlockingFilter);

View File

@ -10,8 +10,6 @@ import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CustomResourceMatcherR4Test extends BaseR4Test {
public static final String FIELD_EXACT_MATCH_NAME = EmpiMetricEnum.NAME_ANY_ORDER.name();
@ -27,53 +25,54 @@ public class CustomResourceMatcherR4Test extends BaseR4Test {
@Test
public void testExactNameAnyOrder() {
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_ANY_ORDER, true));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry));
}
@Test
public void testNormalizedNameAnyOrder() {
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_ANY_ORDER, false));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry));
}
@Test
public void testExactNameFirstAndLast() {
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_FIRST_AND_LAST, true));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatchResult(EmpiMatchResultEnum.MATCH, 1L, 1.0, false, false, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry));
}
@Test
public void testNormalizedNameFirstAndLast() {
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_FIRST_AND_LAST, false));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry));
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry));
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry));
}
private EmpiRulesJson buildNameRules(EmpiMetricEnum theExactNameAnyOrder, boolean theExact) {

View File

@ -36,7 +36,7 @@ public class EmpiResourceFieldMatcherR4Test extends BaseEmpiRulesR4Test {
Patient patient = new Patient();
patient.setActive(true);
assertFalse(myComparator.match(patient, myJohny));
assertFalse(myComparator.match(patient, myJohny).match);
}
@Test
@ -77,6 +77,6 @@ public class EmpiResourceFieldMatcherR4Test extends BaseEmpiRulesR4Test {
@Test
public void testMatch() {
assertTrue(myComparator.match(myJohn, myJohny));
assertTrue(myComparator.match(myJohn, myJohny).match);
}
}

View File

@ -1,17 +1,16 @@
package ca.uhn.fhir.empi.rules.svc;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class EmpiResourceMatcherSvcR4Test extends BaseEmpiRulesR4Test {
public static final double NAME_DELTA = 0.0001;
private EmpiResourceMatcherSvc myEmpiResourceMatcherSvc;
private Patient myJohn;
private Patient myJohny;
@ -33,27 +32,28 @@ public class EmpiResourceMatcherSvcR4Test extends BaseEmpiRulesR4Test {
@Test
public void testCompareFirstNameMatch() {
EmpiMatchResultEnum result = myEmpiResourceMatcherSvc.match(myJohn, myJohny);
assertEquals(EmpiMatchResultEnum.POSSIBLE_MATCH, result);
EmpiMatchOutcome result = myEmpiResourceMatcherSvc.match(myJohn, myJohny);
assertMatchResult(EmpiMatchResultEnum.POSSIBLE_MATCH, 1L, 0.816, false, false, result);
}
@Test
public void testCompareBothNamesMatch() {
myJohn.addName().setFamily("Smith");
myJohny.addName().setFamily("Smith");
EmpiMatchResultEnum result = myEmpiResourceMatcherSvc.match(myJohn, myJohny);
assertEquals(EmpiMatchResultEnum.MATCH, result);
EmpiMatchOutcome result = myEmpiResourceMatcherSvc.match(myJohn, myJohny);
assertMatchResult(EmpiMatchResultEnum.MATCH, 3L, 1.816, false, false, result);
}
@Test
public void testMatchResult() {
assertEquals(EmpiMatchResultEnum.POSSIBLE_MATCH, myEmpiResourceMatcherSvc.getMatchResult(myJohn, myJohny));
assertMatchResult(EmpiMatchResultEnum.POSSIBLE_MATCH, 1L, 0.816, false, false, myEmpiResourceMatcherSvc.getMatchResult(myJohn, myJohny));
myJohn.addName().setFamily("Smith");
myJohny.addName().setFamily("Smith");
assertEquals(EmpiMatchResultEnum.MATCH, myEmpiResourceMatcherSvc.getMatchResult(myJohn, myJohny));
assertMatchResult(EmpiMatchResultEnum.MATCH, 3L, 1.816, false, false, myEmpiResourceMatcherSvc.getMatchResult(myJohn, myJohny));
Patient patient3 = new Patient();
patient3.setId("Patient/3");
patient3.addName().addGiven("Henry");
assertEquals(EmpiMatchResultEnum.NO_MATCH, myEmpiResourceMatcherSvc.getMatchResult(myJohn, patient3));
assertMatchResult(EmpiMatchResultEnum.NO_MATCH, 0L, 0.0, false, false, myEmpiResourceMatcherSvc.getMatchResult(myJohn, patient3));
}
}

View File

@ -9,9 +9,9 @@ import static ca.uhn.fhir.empi.api.EmpiMatchResultEnum.MATCH;
import static ca.uhn.fhir.empi.api.EmpiMatchResultEnum.NO_MATCH;
import static ca.uhn.fhir.empi.api.EmpiMatchResultEnum.POSSIBLE_DUPLICATE;
import static ca.uhn.fhir.empi.api.EmpiMatchResultEnum.POSSIBLE_MATCH;
import static ca.uhn.fhir.empi.model.CanonicalIdentityAssuranceLevel.LEVEL1;
import static ca.uhn.fhir.empi.model.CanonicalIdentityAssuranceLevel.LEVEL2;
import static ca.uhn.fhir.empi.model.CanonicalIdentityAssuranceLevel.LEVEL3;
import static ca.uhn.fhir.empi.model.CanonicalIdentityAssuranceLevel.LEVEL4;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo;
@ -22,9 +22,9 @@ public class AssuranceLevelUtilTest {
@Test
public void testValidPersonLinkLevels() {
assertThat(AssuranceLevelUtil.getAssuranceLevel(POSSIBLE_MATCH, AUTO), is(equalTo(LEVEL2)));
assertThat(AssuranceLevelUtil.getAssuranceLevel(MATCH, AUTO), is(equalTo(LEVEL3)));
assertThat(AssuranceLevelUtil.getAssuranceLevel(MATCH, MANUAL), is(equalTo(LEVEL4)));
assertThat(AssuranceLevelUtil.getAssuranceLevel(POSSIBLE_MATCH, AUTO), is(equalTo(LEVEL1)));
assertThat(AssuranceLevelUtil.getAssuranceLevel(MATCH, AUTO), is(equalTo(LEVEL2)));
assertThat(AssuranceLevelUtil.getAssuranceLevel(MATCH, MANUAL), is(equalTo(LEVEL3)));
}

View File

@ -1,4 +1,5 @@
{
"version": "1",
"candidateSearchParams" : [],
"candidateFilterSearchParams" : [{
"resourceType" : "Patient",

View File

@ -1,4 +1,5 @@
{
"version": "1",
"candidateSearchParams" : [],
"candidateFilterSearchParams" : [],
"matchFields" : [ {

View File

@ -1,4 +1,5 @@
{
"version": "1",
"candidateSearchParams" : [{
"resourceType" : "Patient",
"searchParams" : ["foo"]

View File

@ -1,4 +1,5 @@
{
"version": "1",
"candidateSearchParams" : [],
"candidateFilterSearchParams" : [],
"matchFields" : [],

View File

@ -1,4 +1,5 @@
{
"version": "1",
"candidateSearchParams": [],
"candidateFilterSearchParams": [],
"matchFields": [

View File

@ -1,4 +1,5 @@
{
"version": "1",
"candidateSearchParams" : [],
"candidateFilterSearchParams" : [],
"matchFields" : [ {

View File

@ -1,4 +1,5 @@
{
"version": "1",
"candidateSearchParams" : [],
"candidateFilterSearchParams" : [],
"matchFields" : [ {

View File

@ -1,4 +1,5 @@
{
"version": "1",
"candidateSearchParams" : [],
"candidateFilterSearchParams" : [],
"matchFields" : [ {