diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTypeUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTypeUtil.java new file mode 100644 index 00000000000..b9ef2662d33 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/FhirTypeUtil.java @@ -0,0 +1,39 @@ +package ca.uhn.fhir.util; + +public final class FhirTypeUtil { + + private FhirTypeUtil() {} + + /** + * Returns true if the type is a primitive fhir type + * (ie, a type that is IPrimitiveType), false otherwise. + */ + public static boolean isPrimitiveType(String theFhirType) { + switch (theFhirType) { + default: + // non-primitive type (or unknown type) + return false; + case "string": + case "code": + case "markdown": + case "id": + case "uri": + case "url": + case "canonical": + case "oid": + case "uuid": + case "boolean": + case "unsignedInt": + case "positiveInt": + case "decimal": + case "integer64": + case "integer": + case "date": + case "dateTime": + case "time": + case "instant": + case "base64Binary": + return true; + } + } +} diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_8_0/5058-mdm-blocklist-rules.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_8_0/5058-mdm-blocklist-rules.yaml new file mode 100644 index 00000000000..135e52c0100 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_8_0/5058-mdm-blocklist-rules.yaml @@ -0,0 +1,8 @@ +--- +type: add +issue: 5058 +title: "Added infrastructure to allow consumers to define + MDM block rules based on fhir path and specific values. + To utilize this feature, an IBlockListRuleProvider must be + wired up with the required rules json. +" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_customizations.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_customizations.md index cc193e23b40..8f0f6d90601 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_customizations.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_customizations.md @@ -14,7 +14,104 @@ MDM supports a pointcut invocation right before it starts matching an incoming s In a scenario where a patient was given a placeholder name(John Doe), it would be desirable to ignore a 'name' matching rule but allow matching on other valid rules like matching SSN or a matching address. -The following provides a full implementation of an interceptor that prevents matching on a patient name when it detects a placeholder value. +This can be done with a custom interceptor, or by providing a set of BlockListRules and wiring in a +IBlockListRuleProvider that provides it. + +--- + +### Block List Rules + +MDM can be configured to block certain resources from MDM matching entirely +using a set of json rules. + +Blocked resources will still have an associated Golden Resource +created, and will still be available for future resources to match, +but no matching will be done to existing resources in the system. + +In order to prevent MDM matching using the block rule list, +an IBlockListRuleProvider must be wired in and a set of block rules provided. + +Blocking rules are provided in a list of rule-sets, +with each one applicable to a specified resource type. + +Within each rule-set, a collection of fields specify the +`fhirPath` and `value` (case insensitive) on which to test an input resource for blocking. + +If a resource matches on all blocked fields in a rule-set, +MDM matching will be blocked for the entire resource. + +If multiple rule-sets apply to the same resource, they will be checked +in sequence until one is found to be applicable. If none are, MDM matching +will continue as before. + +Below is an example of MDM blocking rules used to prevent matching on Patients +with name "John Doe" or "Jane Doe". + +```json +{ + "blocklist": [{ + "resourceType": "Patient", + "fields": [{ + "fhirPath": "name.first().family", + "value": "doe" + }, { + "fhirPath": "name.first().given.first()", + "value": "john" + }] + }, { + "resourceType": "Patient", + "fields": [{ + "fhirPath": "name.first().family", + "value": "doe" + }, { + "fhirPath": "name.first().given.first()", + "value": "jane" + }] + }] +} +``` + +Note that, for these rules, because the `fhirPath` specifies the `first()` name, +Patient resource A below would be blocked. But Patient resource B would not be. + +##### Patient Resource A + +```json +{ + "resourceType": "Patient", + "name": [{ + "family": "doe", + "given": [ + "jane" + ] + }] +} +``` + +##### Patient Resource B + +```json +{ + "resourceType": "Patient", + "name": [{ + "family": "jetson", + "given": [ + "jane" + ] + },{ + "family": "doe", + "given": [ + "jane" + ] + }] +} +``` + +--- + +### Interceptor Blocking + +The following provides a full implementation of an interceptor that prevents matching on a patient name when it detects a placeholder value. ```java {{snippet:classpath:/ca/uhn/hapi/fhir/docs/interceptor/PatientNameModifierMdmPreProcessingInterceptor.java|patientInterceptor}} diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_details.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_details.md index 0f4f787241b..033dc0b7081 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_details.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_details.md @@ -31,7 +31,6 @@ external system). 1. Source resources are only ever compared to Golden Resources via this EID. - ## Meta Tags In order for MDM to work, the service adds several pieces of metadata to a given resource. This section explains what MDM does to the resources it processes. diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java index 6c963e29b75..21ab9fa5488 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.jpa.mdm.broker.MdmMessageHandler; import ca.uhn.fhir.jpa.mdm.broker.MdmMessageKeySvc; import ca.uhn.fhir.jpa.mdm.broker.MdmQueueConsumerLoader; import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; +import ca.uhn.fhir.jpa.mdm.svc.BlockRuleEvaluationSvcImpl; import ca.uhn.fhir.jpa.mdm.svc.GoldenResourceMergerSvcImpl; import ca.uhn.fhir.jpa.mdm.svc.GoldenResourceSearchSvcImpl; import ca.uhn.fhir.jpa.mdm.svc.IMdmModelConverterSvc; @@ -60,6 +61,8 @@ import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc; import ca.uhn.fhir.mdm.api.IMdmSettings; import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService; import ca.uhn.fhir.mdm.batch2.MdmBatch2Config; +import ca.uhn.fhir.mdm.blocklist.svc.IBlockListRuleProvider; +import ca.uhn.fhir.mdm.blocklist.svc.IBlockRuleEvaluationSvc; import ca.uhn.fhir.mdm.dao.IMdmLinkImplFactory; import ca.uhn.fhir.mdm.dao.MdmLinkFactory; import ca.uhn.fhir.mdm.interceptor.IMdmStorageInterceptor; @@ -74,6 +77,7 @@ import ca.uhn.fhir.mdm.util.MdmPartitionHelper; import ca.uhn.fhir.mdm.util.MessageHelper; import ca.uhn.fhir.validation.IResourceLoader; 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.annotation.Import; @@ -114,6 +118,12 @@ public class MdmConsumerConfig { return new MdmMatchLinkSvc(); } + @Bean + IBlockRuleEvaluationSvc blockRuleEvaluationSvc( + @Autowired FhirContext theContext, @Autowired(required = false) IBlockListRuleProvider theProvider) { + return new BlockRuleEvaluationSvcImpl(theContext, theProvider); + } + @Bean MdmEidUpdateService eidUpdateService() { return new MdmEidUpdateService(); diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/BlockRuleEvaluationSvcImpl.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/BlockRuleEvaluationSvcImpl.java new file mode 100644 index 00000000000..7f64250f0cf --- /dev/null +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/BlockRuleEvaluationSvcImpl.java @@ -0,0 +1,120 @@ +package ca.uhn.fhir.jpa.mdm.svc; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.fhirpath.FhirPathExecutionException; +import ca.uhn.fhir.fhirpath.IFhirPath; +import ca.uhn.fhir.mdm.blocklist.json.BlockListJson; +import ca.uhn.fhir.mdm.blocklist.json.BlockListRuleJson; +import ca.uhn.fhir.mdm.blocklist.json.BlockedFieldJson; +import ca.uhn.fhir.mdm.blocklist.svc.IBlockListRuleProvider; +import ca.uhn.fhir.mdm.blocklist.svc.IBlockRuleEvaluationSvc; +import ca.uhn.fhir.util.FhirTypeUtil; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.slf4j.Logger; + +import java.util.List; +import javax.annotation.Nullable; + +import static org.slf4j.LoggerFactory.getLogger; + +/** + * An implementation of IBlockRuleEvaluationSvc. + * Evaluates whether or not a provided resource + * is blocked from mdm matching or not. + */ +public class BlockRuleEvaluationSvcImpl implements IBlockRuleEvaluationSvc { + private static final Logger ourLog = getLogger(BlockRuleEvaluationSvcImpl.class); + + private final IFhirPath myFhirPath; + + private final IBlockListRuleProvider myBlockListRuleProvider; + + public BlockRuleEvaluationSvcImpl( + FhirContext theContext, @Nullable IBlockListRuleProvider theIBlockListRuleProvider) { + myFhirPath = theContext.newFhirPath(); + myBlockListRuleProvider = theIBlockListRuleProvider; + } + + private boolean hasBlockList() { + return myBlockListRuleProvider != null && myBlockListRuleProvider.getBlocklistRules() != null; + } + + @Override + public boolean isMdmMatchingBlocked(IAnyResource theResource) { + if (hasBlockList()) { + return isMdmMatchingBlockedInternal(theResource); + } + return false; + } + + private boolean isMdmMatchingBlockedInternal(IAnyResource theResource) { + BlockListJson blockListJson = myBlockListRuleProvider.getBlocklistRules(); + String resourceType = theResource.fhirType(); + + // gather only applicable rules + // these rules are 'or''d, so if any match, + // mdm matching is blocked + return blockListJson.getBlockListItemJsonList().stream() + .filter(r -> r.getResourceType().equals(resourceType)) + .anyMatch(rule -> isMdmBlockedForFhirPath(theResource, rule)); + } + + private boolean isMdmBlockedForFhirPath(IAnyResource theResource, BlockListRuleJson theRule) { + List blockedFields = theRule.getBlockedFields(); + + // rules are 'and'ed + // This means that if we detect any reason *not* to block + // we don't; only if all block rules pass do we block + for (BlockedFieldJson field : blockedFields) { + String path = field.getFhirPath(); + String blockedValue = field.getBlockedValue(); + + List results; + try { + // can throw FhirPathExecutionException if path is incorrect + // or functions are invalid. + // single() explicitly throws this (but may be what is desired) + // so we'll catch and not block if this fails + results = myFhirPath.evaluate(theResource, path, IBase.class); + } catch (FhirPathExecutionException ex) { + ourLog.warn( + "FhirPath evaluation failed with an exception." + + " No blocking will be applied and mdm matching will continue as before.", + ex); + return false; + } + + // fhir path should return exact values + if (results.size() != 1) { + // no results means no blocking + // too many matches means no blocking + ourLog.trace("Too many values at field {}", path); + return false; + } + + IBase first = results.get(0); + + if (FhirTypeUtil.isPrimitiveType(first.fhirType())) { + IPrimitiveType primitiveType = (IPrimitiveType) first; + if (!primitiveType.getValueAsString().equalsIgnoreCase(blockedValue)) { + // doesn't match + // no block + ourLog.trace("Value at path {} does not match - mdm will not block.", path); + return false; + } + } else { + // blocking can only be done by evaluating primitive types + // additional fhirpath values required + ourLog.warn( + "FhirPath {} yields a non-primitive value; blocking is only supported on primitive field types.", + path); + return false; + } + } + + // if we got here, all blocking rules evaluated to true + return true; + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java index a1db8b8234e..34b562a5d3d 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java @@ -20,12 +20,14 @@ package ca.uhn.fhir.jpa.mdm.svc; import ca.uhn.fhir.jpa.mdm.svc.candidate.CandidateList; +import ca.uhn.fhir.jpa.mdm.svc.candidate.CandidateStrategyEnum; import ca.uhn.fhir.jpa.mdm.svc.candidate.MatchedGoldenResourceCandidate; import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc; import ca.uhn.fhir.mdm.api.IMdmLinkSvc; import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; import ca.uhn.fhir.mdm.api.MdmMatchOutcome; import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; +import ca.uhn.fhir.mdm.blocklist.svc.IBlockRuleEvaluationSvc; import ca.uhn.fhir.mdm.log.Logs; import ca.uhn.fhir.mdm.model.MdmTransactionContext; import ca.uhn.fhir.mdm.util.GoldenResourceHelper; @@ -63,6 +65,9 @@ public class MdmMatchLinkSvc { @Autowired private MdmEidUpdateService myEidUpdateService; + @Autowired + private IBlockRuleEvaluationSvc myBlockRuleEvaluationSvc; + /** * Given an MDM source (consisting of any supported MDM type), find a suitable Golden Resource candidate for them, * or create one if one does not exist. Performs matching based on rules defined in mdm-rules.json. @@ -84,9 +89,24 @@ public class MdmMatchLinkSvc { private MdmTransactionContext doMdmUpdate( IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) { - CandidateList candidateList = myMdmGoldenResourceFindingSvc.findGoldenResourceCandidates(theResource); + // we initialize to an empty list + // we require a candidatestrategy, but it doesn't matter + // because empty lists are effectively no matches + // (and so the candidate strategy doesn't matter) + CandidateList candidateList = new CandidateList(CandidateStrategyEnum.LINK); - if (candidateList.isEmpty()) { + /* + * If a resource is blocked, we will not conduct + * MDM matching. But we will still create golden resources + * (so that future resources may match to it). + */ + boolean isResourceBlocked = myBlockRuleEvaluationSvc.isMdmMatchingBlocked(theResource); + + if (!isResourceBlocked) { + candidateList = myMdmGoldenResourceFindingSvc.findGoldenResourceCandidates(theResource); + } + + if (isResourceBlocked || candidateList.isEmpty()) { handleMdmWithNoCandidates(theResource, theMdmTransactionContext); } else if (candidateList.exactlyOneMatch()) { handleMdmWithSingleCandidate(theResource, candidateList.getOnlyMatch(), theMdmTransactionContext); diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/BlockListConfig.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/BlockListConfig.java new file mode 100644 index 00000000000..3d63759d0c9 --- /dev/null +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/BlockListConfig.java @@ -0,0 +1,14 @@ +package ca.uhn.fhir.jpa.mdm.config; + +import ca.uhn.fhir.mdm.blocklist.svc.IBlockListRuleProvider; +import org.springframework.context.annotation.Bean; + +import static org.mockito.Mockito.mock; + +public class BlockListConfig { + + @Bean + public IBlockListRuleProvider ruleProvider() { + return mock(IBlockListRuleProvider.class); + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/BlockRuleEvaluationSvcImplTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/BlockRuleEvaluationSvcImplTest.java new file mode 100644 index 00000000000..8201df9eb01 --- /dev/null +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/BlockRuleEvaluationSvcImplTest.java @@ -0,0 +1,536 @@ +package ca.uhn.fhir.jpa.mdm.svc; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.mdm.svc.testmodels.BlockRuleTestCase; +import ca.uhn.fhir.mdm.blocklist.json.BlockListJson; +import ca.uhn.fhir.mdm.blocklist.json.BlockListRuleJson; +import ca.uhn.fhir.mdm.blocklist.svc.IBlockListRuleProvider; +import ca.uhn.fhir.parser.IParser; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.when; +import static org.slf4j.LoggerFactory.getLogger; + +@ExtendWith(MockitoExtension.class) +public class BlockRuleEvaluationSvcImplTest { + private static final Logger ourLog = getLogger(BlockRuleEvaluationSvcImplTest.class); + + @Mock + private IBlockListRuleProvider myRuleProvider; + + @Spy + private static FhirContext ourFhirContext = FhirContext.forR4Cached(); + + @InjectMocks + private BlockRuleEvaluationSvcImpl myRuleEvaluationSvc; + + /** + * Provides a list of test cases to maximize coverage + * and type matching. + */ + private static Collection getTestCasesFhir() { + IParser parser = ourFhirContext.newJsonParser(); + + List data = new ArrayList<>(); + + /* + * Basic start case + * Blocking on single() name that is jane doe + */ + { + String patientStr = """ + { + "resourceType": "Patient", + "name": [{ + "family": "doe", + "given": [ + "jane" + ] + }] + } + """; + BlockListJson blockListJson = new BlockListJson(); + BlockListRuleJson rule = new BlockListRuleJson(); + rule.setResourceType("Patient"); + rule.addBlockListField() + .setFhirPath("name.single().family") + .setBlockedValue("Doe"); + rule.addBlockListField() + .setFhirPath("name.single().given.first()") + .setBlockedValue("Jane"); + blockListJson.addBlockListRule(rule); + data.add( + new BlockRuleTestCase( + "Basic happy path test - Block on Jane Doe", + blockListJson, + parser.parseResource(Patient.class, patientStr), + true + ) + ); + } + + /* + * Blocking on official name with "Jane Doe". + * Patient has multiple given names on official name + * Mdm is not blocked (no unique value found to compare) + */ + { + String patientStr = """ + { + "resourceType": "Patient", + "name": [{ + "family": "smith", + "given": [ + "jane" + ] + }, + { + "family": "doe", + "use": "official", + "given": [ + "trixie", + "janet", + "jane" + ] + }] + } + """; + BlockListJson blockListJson = new BlockListJson(); + BlockListRuleJson rule = new BlockListRuleJson(); + rule.setResourceType("Patient"); + rule.addBlockListField() + .setFhirPath("name.where(use = 'official').family") + .setBlockedValue("Doe"); + rule.addBlockListField() + .setFhirPath("name.where(use = 'official').given") + .setBlockedValue("Jane"); + blockListJson.addBlockListRule(rule); + data.add( + new BlockRuleTestCase( + "Blocking on official name 'Jane Doe'", + blockListJson, + parser.parseResource(Patient.class, patientStr), + false + ) + ); + } + + /* + * Blocking on extension + */ + { + String patientStr = """ + { + "resourceType": "Patient", + "extension": [{ + "url": "http://localhost/test", + "valueString": "example" + }] + } + """; + BlockListJson blockListJson = new BlockListJson(); + BlockListRuleJson rule = new BlockListRuleJson(); + rule.setResourceType("Patient"); + rule.addBlockListField() + .setFhirPath("extension.where(url = 'http://localhost/test').value.first()") + .setBlockedValue("example"); + blockListJson.addBlockListRule(rule); + data.add( + new BlockRuleTestCase( + "Blocking on extension value", + blockListJson, + parser.parseResource(Patient.class, patientStr), + true + ) + ); + } + + /* + * Block on identifier with specific system and value + * Patient contains specific identifier (and others) + * Mdm is blocked + */ + { + String patientStr = """ + { + "resourceType": "Patient", + "identifier": [{ + "system": "urn:oid:2.2.36.146.595.217.0.1", + "value": "23456" + }, { + "system": "urn:oid:1.2.36.146.595.217.0.1", + "value": "12345" + }] + } + """; + BlockListJson blockListJson = new BlockListJson(); + BlockListRuleJson rule = new BlockListRuleJson(); + rule.setResourceType("Patient"); + rule.addBlockListField() + .setFhirPath("identifier.where(system = 'urn:oid:1.2.36.146.595.217.0.1').value") + .setBlockedValue("12345"); + blockListJson.addBlockListRule(rule); + data.add( + new BlockRuleTestCase( + "Blocking on identifier with specific system and value", + blockListJson, + parser.parseResource(Patient.class, patientStr), + true + ) + ); + } + + /* + * Block on first identifier with provided system + * and value. + */ + { + String patientStr = """ + { + "resourceType": "Patient", + "identifier": [{ + "system": "urn:oid:1.2.36.146.595.217.0.1", + "value": "44444" + }, { + "system": "urn:oid:1.2.36.146.595.217.0.1", + "value": "12345" + }] + } + """; + BlockListJson blockListJson = new BlockListJson(); + BlockListRuleJson rule = new BlockListRuleJson(); + rule.setResourceType("Patient"); + rule.addBlockListField() + .setFhirPath("identifier.first().system") + .setBlockedValue("urn:oid:1.2.36.146.595.217.0.1"); + rule.addBlockListField() + .setFhirPath("identifier.first().value") + .setBlockedValue("12345"); + blockListJson.addBlockListRule(rule); + data.add( + new BlockRuleTestCase( + "Block on first identifier with value and system", + blockListJson, + parser.parseResource(Patient.class, patientStr), + false + ) + ); + } + + /* + * Blocked fields do not exist on resource, so mdm is not blocked + */ + { + String patientStr = """ + { + "resourceType": "Patient", + "name": [{ + "family": "doe", + "given": [ + "jane" + ] + }] + } + """; + BlockListJson blockListJson = new BlockListJson(); + BlockListRuleJson rule = new BlockListRuleJson(); + rule.setResourceType("Patient"); + rule.addBlockListField() + .setFhirPath("identifier.system") + .setBlockedValue("urn:oid:1.2.36.146.595.217.0.1"); + rule.addBlockListField() + .setFhirPath("identifier.value") + .setBlockedValue("12345"); + blockListJson.addBlockListRule(rule); + data.add( + new BlockRuleTestCase( + "Blocking on field that doesn't exist", + blockListJson, + parser.parseResource(patientStr), + false + ) + ); + } + + /* + * DateTime + * multi-type field is blocked on specific date; resource has matching + * specific date so mdm is blocked + */ + { + String patientStr = """ + { + "resourceType": "Patient", + "name": [{ + "family": "doe", + "given": [ + "jane" + ] + }], + "deceasedDateTime": "2000-01-01" + } + """; + BlockListJson blockListJson = new BlockListJson(); + BlockListRuleJson rule = new BlockListRuleJson(); + rule.setResourceType("Patient"); + rule.addBlockListField() + .setFhirPath("deceased.value") + .setBlockedValue("2000-01-01"); + blockListJson.addBlockListRule(rule); + data.add(new BlockRuleTestCase( + "Blocking on multi-type field (date or boolean) with specific date value.", + blockListJson, + parser.parseResource(patientStr), + true + )); + } + + /* + * DateTime + * Block is specified on multi-type datetime. Resource + * has a multi-type boolean value, so MDM is not blocked + */ + { + String patientStr = """ + { + "resourceType": "Patient", + "name": [{ + "family": "doe", + "given": [ + "jane" + ] + }], + "deceasedBoolean": true + } + """; + BlockListJson blockListJson = new BlockListJson(); + BlockListRuleJson rule = new BlockListRuleJson(); + rule.setResourceType("Patient"); + rule.addBlockListField() + .setFhirPath("deceased.value") + .setBlockedValue("2000-01-01"); + blockListJson.addBlockListRule(rule); + data.add(new BlockRuleTestCase( + "Blocking on multi-value (boolean, date) value on date value when actual value is boolean", + blockListJson, + parser.parseResource(patientStr), + false + )); + } + + /* + * Code (Enum) + * Blocking is on exact link.type value. + * Patient has this exact enum value and so mdm is blocked. + */ + { + String patientStr = """ + { + "resourceType": "Patient", + "name": [{ + "family": "hirasawa", + "given": [ + "yui" + ] + }], + "link": [{ + "type": "seealso" + }] + } + """; + BlockListJson blockListJson = new BlockListJson(); + BlockListRuleJson rule = new BlockListRuleJson(); + rule.setResourceType("Patient"); + rule.addBlockListField() + .setFhirPath("link.first().type") + .setBlockedValue("seealso"); + blockListJson.addBlockListRule(rule); + data.add(new BlockRuleTestCase( + "Blocking on link.type value (an enum)", + blockListJson, + parser.parseResource(patientStr), + true + )); + } + + /* + * CodableConcept + * Blocking is on exact maritalStatus.coding.code. + * Patient has this value, so the value is blocked. + */ + { + String patientStr = """ + { + "resourceType": "Patient", + "name": [{ + "family": "jetson", + "given": [ + "jane" + ] + }], + "maritalStatus": { + "coding": [{ + "system": "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus", + "code": "M" + }] + } + } + """; + BlockListJson blockListJson = new BlockListJson(); + BlockListRuleJson rule = new BlockListRuleJson(); + rule.setResourceType("Patient"); + rule.addBlockListField() + .setFhirPath("maritalStatus.coding.where(system = 'http://terminology.hl7.org/CodeSystem/v3-MaritalStatus').code") + .setBlockedValue("m"); + blockListJson.addBlockListRule(rule); + data.add(new BlockRuleTestCase( + "Blocking on maritalStatus with specific system and blocked value", + blockListJson, + parser.parseResource(patientStr), + true + )); + } + + /* + * Boolean (trivial, but completions sake) + * Blocking on active = true. + * Patient is active, so mdm is blocked. + */ + { + String patientStr = """ + { + "resourceType": "Patient", + "active": true + } + """; + BlockListJson json = new BlockListJson(); + BlockListRuleJson rule = new BlockListRuleJson(); + rule.setResourceType("Patient"); + rule.addBlockListField() + .setFhirPath("active") + .setBlockedValue("true"); + json.addBlockListRule(rule); + data.add(new BlockRuleTestCase( + "Blocking on boolean field", + json, + parser.parseResource(patientStr), + true + )); + } + + /* + * Blocking using 'single()' when no single value exists + */ + { + String patientStr = """ + { + "resourceType": "Patient", + "identifier": [{ + "system": "urn:oid:1.2.36.146.595.217.0.1", + "value": "44444" + }, { + "system": "urn:oid:1.2.36.146.595.217.0.1", + "value": "12345" + }] + } + """; + BlockListJson blockListJson = new BlockListJson(); + BlockListRuleJson rule = new BlockListRuleJson(); + rule.setResourceType("Patient"); + rule.addBlockListField() + .setFhirPath("identifier.single().system") + .setBlockedValue("urn:oid:1.2.36.146.595.217.0.1"); + rule.addBlockListField() + .setFhirPath("identifier.single().value") + .setBlockedValue("12345"); + blockListJson.addBlockListRule(rule); + data.add( + new BlockRuleTestCase( + "Block on single() identifier with multiple present identifiers", + blockListJson, + parser.parseResource(Patient.class, patientStr), + false + ) + ); + } + + /* + * Block attempt on non-primitive value + */ + { + String patientStr = """ + { + "resourceType": "Patient", + "identifier": [{ + "system": "urn:oid:1.2.36.146.595.217.0.1", + "value": "12345" + }] + } + """; + BlockListJson blockListJson = new BlockListJson(); + BlockListRuleJson rule = new BlockListRuleJson(); + rule.setResourceType("Patient"); + rule.addBlockListField() + .setFhirPath("identifier") + .setBlockedValue("urn:oid:1.2.36.146.595.217.0.1"); + blockListJson.addBlockListRule(rule); + data.add( + new BlockRuleTestCase( + "Block on identifier field (non-primitive)", + blockListJson, + parser.parseResource(Patient.class, patientStr), + false + ) + ); + } + + return data; + } + + @ParameterizedTest + @MethodSource("getTestCasesFhir") + public void isMdmMatchingBlocked_givenResourceAndRules_returnsExpectedValue(BlockRuleTestCase theTestCase) { + ourLog.info(theTestCase.getId()); + + // setup + BlockListJson blockList = theTestCase.getBlockRule(); + IBaseResource patient = theTestCase.getPatientResource(); + boolean expected = theTestCase.isExpectedBlockResult(); + + // when + when(myRuleProvider.getBlocklistRules()) + .thenReturn(blockList); + + // test + assertEquals(expected, myRuleEvaluationSvc.isMdmMatchingBlocked((IAnyResource) patient), theTestCase.getId()); + } + + @Test + public void isMdmMatchingBlocked_noBlockRules_returnsFalse() { + // setup + Patient patient = new Patient(); + patient.addName() + .setFamily("Doe") + .addGiven("Jane"); + + // test + assertFalse(myRuleEvaluationSvc.isMdmMatchingBlocked(patient)); + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkSvcTest.java index 3e96395dc6e..89201173655 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkSvcTest.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkSvcTest.java @@ -18,6 +18,7 @@ import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; @@ -31,8 +32,11 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.fail; +import static org.slf4j.LoggerFactory.getLogger; public class MdmLinkSvcTest extends BaseMdmR4Test { + private static final Logger ourLog = getLogger(MdmLinkSvcTest.class); + private static final MdmMatchOutcome POSSIBLE_MATCH = new MdmMatchOutcome(null, null).setMatchResultEnum(MdmMatchResultEnum.POSSIBLE_MATCH); @Autowired IMdmLinkSvc myMdmLinkSvc; @@ -180,9 +184,6 @@ public class MdmLinkSvcTest extends BaseMdmR4Test { .stream().map(p -> p.getIdElement().toVersionless().getIdPart()) .collect(Collectors.toList()); - System.out.println(actual); - System.out.println(expected); - assertThat(actual, Matchers.containsInAnyOrder(expected.toArray())); } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java index 959970fa1e7..d436173b0a0 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.mdm.svc; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.entity.MdmLink; import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; +import ca.uhn.fhir.jpa.mdm.config.BlockListConfig; import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.mdm.api.IMdmLink; @@ -12,6 +13,9 @@ import ca.uhn.fhir.mdm.api.MdmConstants; import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; import ca.uhn.fhir.mdm.api.MdmMatchOutcome; import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; +import ca.uhn.fhir.mdm.blocklist.json.BlockListJson; +import ca.uhn.fhir.mdm.blocklist.json.BlockListRuleJson; +import ca.uhn.fhir.mdm.blocklist.svc.IBlockListRuleProvider; import ca.uhn.fhir.mdm.model.CanonicalEID; import ca.uhn.fhir.mdm.model.MdmTransactionContext; import ca.uhn.fhir.mdm.util.EIDHelper; @@ -21,16 +25,22 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; import ca.uhn.fhir.rest.param.TokenParam; import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.HumanName; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import java.util.ArrayList; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -50,631 +60,709 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; -public class MdmMatchLinkSvcTest extends BaseMdmR4Test { - @Autowired - IMdmLinkSvc myMdmLinkSvc; - @Autowired - private EIDHelper myEidHelper; - @Autowired - private GoldenResourceHelper myGoldenResourceHelper; - @Autowired - private IMdmLinkUpdaterSvc myMdmLinkUpdaterSvc; +public class MdmMatchLinkSvcTest { + @Nested + public class NoBlockLinkTest extends BaseMdmR4Test { + @Autowired + IMdmLinkSvc myMdmLinkSvc; + @Autowired + private EIDHelper myEidHelper; + @Autowired + private GoldenResourceHelper myGoldenResourceHelper; + @Autowired + private IMdmLinkUpdaterSvc myMdmLinkUpdaterSvc; - @Test - public void testAddPatientLinksToNewGoldenResourceIfNoneFound() { - createPatientAndUpdateLinks(buildJanePatient()); - assertLinkCount(1); - assertLinksMatchResult(MATCH); - assertLinksCreatedNewResource(true); - assertLinksMatchedByEid(false); - assertLinksMatchScore(1.0); - assertLinksMatchVector((Long) null); + @Test + public void testAddPatientLinksToNewGoldenResourceIfNoneFound() { + createPatientAndUpdateLinks(buildJanePatient()); + assertLinkCount(1); + assertLinksMatchResult(MATCH); + assertLinksCreatedNewResource(true); + assertLinksMatchedByEid(false); + assertLinksMatchScore(1.0); + assertLinksMatchVector((Long) null); + } + + @Test + public void testAddMedicationLinksToNewGoldenRecordMedicationIfNoneFound() { + createDummyOrganization(); + + createMedicationAndUpdateLinks(buildMedication("Organization/mfr")); + assertLinkCount(1); + assertLinksMatchResult(MATCH); + assertLinksCreatedNewResource(true); + assertLinksMatchedByEid(false); + assertLinksMatchScore(1.0); + assertLinksMatchVector((Long) null); + } + + @Test + public void testAddPatientLinksToNewlyCreatedResourceIfNoMatch() { + Patient patient1 = createPatientAndUpdateLinks(buildJanePatient()); + Patient patient2 = createPatientAndUpdateLinks(buildPaulPatient()); + + assertLinkCount(2); + + assertThat(patient1, is(not(sameGoldenResourceAs(patient2)))); + + assertLinksMatchResult(MATCH, MATCH); + assertLinksCreatedNewResource(true, true); + assertLinksMatchedByEid(false, false); + assertLinksMatchScore(1.0, 1.0); + assertLinksMatchVector(null, null); + } + + @Test + public void testAddPatientLinksToExistingGoldenResourceIfMatch() { + Patient patient1 = createPatientAndUpdateLinks(buildJanePatient()); + assertLinkCount(1); + + Patient patient2 = createPatientAndUpdateLinks(buildJanePatient()); + assertLinkCount(2); + + assertThat(patient1, is(sameGoldenResourceAs(patient2))); + assertLinksMatchResult(MATCH, MATCH); + assertLinksCreatedNewResource(true, false); + assertLinksMatchedByEid(false, false); + assertLinksMatchScore(1.0, 2.0 / 3.0); + assertLinksMatchVector(null, 6L); + } + + @Test + public void testWhenMatchOccursOnGoldenResourceThatHasBeenManuallyNOMATCHedThatItIsBlocked() { + Patient originalJane = createPatientAndUpdateLinks(buildJanePatient()); + IAnyResource janeGoldenResource = getGoldenResourceFromTargetResource(originalJane); + + //Create a manual NO_MATCH between janeGoldenResource and unmatchedJane. + Patient unmatchedJane = createPatient(buildJanePatient()); + myMdmLinkSvc.updateLink(janeGoldenResource, unmatchedJane, MdmMatchOutcome.NO_MATCH, MdmLinkSourceEnum.MANUAL, createContextForCreate("Patient")); + + //rerun MDM rules against unmatchedJane. + myMdmMatchLinkSvc.updateMdmLinksForMdmSource(unmatchedJane, createContextForCreate("Patient")); + + assertThat(unmatchedJane, is(not(sameGoldenResourceAs(janeGoldenResource)))); + assertThat(unmatchedJane, is(not(linkedTo(originalJane)))); + + assertLinksMatchResult(MATCH, NO_MATCH, MATCH); + assertLinksCreatedNewResource(true, false, true); + assertLinksMatchedByEid(false, false, false); + assertLinksMatchScore(1.0, null, 1.0); + assertLinksMatchVector(null, null, null); + } + + @Test + public void testWhenPOSSIBLE_MATCHOccursOnGoldenResourceThatHasBeenManuallyNOMATCHedThatItIsBlocked() { + Patient originalJane = createPatientAndUpdateLinks(buildJanePatient()); + + IBundleProvider search = myPatientDao.search(buildGoldenRecordSearchParameterMap()); + IAnyResource janeGoldenResource = (IAnyResource) search.getResources(0, 1).get(0); + + Patient unmatchedPatient = createPatient(buildJanePatient()); + + // This simulates an admin specifically saying that unmatchedPatient does NOT match janeGoldenResource. + myMdmLinkSvc.updateLink(janeGoldenResource, unmatchedPatient, MdmMatchOutcome.NO_MATCH, MdmLinkSourceEnum.MANUAL, createContextForCreate("Patient")); + // TODO change this so that it will only partially match. + + //Now normally, when we run update links, it should link to janeGoldenResource. However, this manual NO_MATCH link + //should cause a whole new GoldenResource to be created. + myMdmMatchLinkSvc.updateMdmLinksForMdmSource(unmatchedPatient, createContextForCreate("Patient")); + + assertThat(unmatchedPatient, is(not(sameGoldenResourceAs(janeGoldenResource)))); + assertThat(unmatchedPatient, is(not(linkedTo(originalJane)))); + + assertLinksMatchResult(MATCH, NO_MATCH, MATCH); + assertLinksCreatedNewResource(true, false, true); + assertLinksMatchedByEid(false, false, false); + assertLinksMatchScore(1.0, null, 1.0); + assertLinksMatchVector(null, null, null); + } + + @Test + public void testWhenPatientIsCreatedWithEIDThatItPropagatesToNewGoldenResource() { + String sampleEID = "sample-eid"; + Patient janePatient = addExternalEID(buildJanePatient(), sampleEID); + janePatient = createPatientAndUpdateLinks(janePatient); + + Optional mdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(JpaPid.fromId(janePatient.getIdElement().getIdPartAsLong())); + assertThat(mdmLink.isPresent(), is(true)); + + Patient patient = getTargetResourceFromMdmLink(mdmLink.get(), "Patient"); + List externalEid = myEidHelper.getExternalEid(patient); + + assertThat(externalEid.get(0).getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient")))); + assertThat(externalEid.get(0).getValue(), is(equalTo(sampleEID))); + } + + @Test + public void testWhenPatientIsCreatedWithoutAnEIDTheGoldenResourceGetsAutomaticallyAssignedOne() { + Patient patient = createPatientAndUpdateLinks(buildJanePatient()); + IMdmLink mdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(JpaPid.fromId(patient.getIdElement().getIdPartAsLong())).get(); + + Patient targetPatient = getTargetResourceFromMdmLink(mdmLink, "Patient"); + Identifier identifierFirstRep = targetPatient.getIdentifierFirstRep(); + assertThat(identifierFirstRep.getSystem(), is(equalTo(MdmConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM))); + assertThat(identifierFirstRep.getValue(), not(blankOrNullString())); + } + + @Test + public void testPatientAttributesAreCopiedOverWhenGoldenResourceIsCreatedFromPatient() { + Patient patient = createPatientAndUpdateLinks(buildPatientWithNameIdAndBirthday("Gary", "GARY_ID", new Date())); + + Optional mdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(JpaPid.fromId(patient.getIdElement().getIdPartAsLong())); + Patient read = getTargetResourceFromMdmLink(mdmLink.get(), "Patient"); + + assertThat(read.getNameFirstRep().getFamily(), is(equalTo(patient.getNameFirstRep().getFamily()))); + assertThat(read.getNameFirstRep().getGivenAsSingleString(), is(equalTo(patient.getNameFirstRep().getGivenAsSingleString()))); + assertThat(read.getBirthDateElement().toHumanDisplay(), is(equalTo(patient.getBirthDateElement().toHumanDisplay()))); + assertThat(read.getTelecomFirstRep().getValue(), is(equalTo(patient.getTelecomFirstRep().getValue()))); + assertThat(read.getPhoto().size(), is(equalTo(patient.getPhoto().size()))); + assertThat(read.getPhotoFirstRep().getData(), is(equalTo(patient.getPhotoFirstRep().getData()))); + assertThat(read.getGender(), is(equalTo(patient.getGender()))); + } + + @Test + public void testPatientMatchingAnotherPatientLinksToSameGoldenResource() { + Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); + Patient sameJanePatient = createPatientAndUpdateLinks(buildJanePatient()); + assertThat(janePatient, is(sameGoldenResourceAs(sameJanePatient))); + } + + @Test + public void testIncomingPatientWithEIDThatMatchesGoldenResourceWithHapiEidAddsExternalEidToGoldenResource() { + // Existing GoldenResource with system-assigned EID found linked from matched Patient. incoming Patient has EID. + // Replace GoldenResource system-assigned EID with Patient EID. + Patient patient = createPatientAndUpdateLinks(buildJanePatient()); + + IAnyResource janeGoldenResource = getGoldenResourceFromTargetResource(patient); + List hapiEid = myEidHelper.getHapiEid(janeGoldenResource); + String foundHapiEid = hapiEid.get(0).getValue(); + + Patient janePatient = addExternalEID(buildJanePatient(), "12345"); + createPatientAndUpdateLinks(janePatient); + + //We want to make sure the patients were linked to the same Golden Resource. + assertThat(patient, is(sameGoldenResourceAs(janePatient))); + + Patient sourcePatient = getGoldenResourceFromTargetResource(patient); + + List identifier = sourcePatient.getIdentifier(); + + //The collision should have kept the old identifier + Identifier firstIdentifier = identifier.get(0); + assertThat(firstIdentifier.getSystem(), is(equalTo(MdmConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM))); + assertThat(firstIdentifier.getValue(), is(equalTo(foundHapiEid))); + + //The collision should have added a new identifier with the external system. + Identifier secondIdentifier = identifier.get(1); + assertThat(secondIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient")))); + assertThat(secondIdentifier.getValue(), is(equalTo("12345"))); + } + + @Test + public void testIncomingPatientWithEidMatchesAnotherPatientWithSameEIDAreLinked() { + // Create Use Case #3 + Patient patient1 = addExternalEID(buildJanePatient(), "uniqueid"); + createPatientAndUpdateLinks(patient1); + + Patient patient2 = buildPaulPatient(); + patient2 = addExternalEID(patient2, "uniqueid"); + createPatientAndUpdateLinks(patient2); + + assertThat(patient1, is(sameGoldenResourceAs(patient2))); + } + + @Test + public void testHavingMultipleEIDsOnIncomingPatientMatchesCorrectly() { + Patient patient1 = buildJanePatient(); + addExternalEID(patient1, "id_1"); + addExternalEID(patient1, "id_2"); + addExternalEID(patient1, "id_3"); + addExternalEID(patient1, "id_4"); + createPatientAndUpdateLinks(patient1); + + Patient patient2 = buildPaulPatient(); + addExternalEID(patient2, "id_5"); + addExternalEID(patient2, "id_1"); + createPatientAndUpdateLinks(patient2); + + assertThat(patient1, is(sameGoldenResourceAs(patient2))); + } + + @Test + public void testDuplicateGoldenResourceLinkIsCreatedWhenAnIncomingPatientArrivesWithEIDThatMatchesAnotherEIDPatient() { + + Patient patient1 = addExternalEID(buildJanePatient(), "eid-1"); + patient1 = createPatientAndUpdateLinks(patient1); + + Patient patient2 = addExternalEID(buildJanePatient(), "eid-2"); + patient2 = createPatientAndUpdateLinks(patient2); + + List possibleDuplicates = (List) myMdmLinkDaoSvc.getPossibleDuplicates(); + assertThat(possibleDuplicates, hasSize(1)); + + Patient finalPatient1 = patient1; + Patient finalPatient2 = patient2; + List duplicatePids = runInTransaction(() -> Stream.of(finalPatient1, finalPatient2) + .map(t -> myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), getGoldenResourceFromTargetResource(t))) + .collect(Collectors.toList())); + + //The two GoldenResources related to the patients should both show up in the only existing POSSIBLE_DUPLICATE MdmLink. + MdmLink mdmLink = possibleDuplicates.get(0); + assertThat(mdmLink.getGoldenResourcePersistenceId(), is(in(duplicatePids))); + assertThat(mdmLink.getSourcePersistenceId(), is(in(duplicatePids))); + } + + @Test + public void testPatientWithNoMdmTagIsNotMatched() { + // Patient with "no-mdm" tag is not matched + Patient janePatient = buildJanePatient(); + janePatient.getMeta().addTag(MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_NO_MDM_MANAGED, "Don't MDM on me!"); + String s = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(janePatient); + createPatientAndUpdateLinks(janePatient); + assertLinkCount(0); + } + + @Test + public void testPractitionersDoNotMatchToPatients() { + Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); + Practitioner janePractitioner = createPractitionerAndUpdateLinks(buildJanePractitioner()); + + assertLinkCount(2); + assertThat(janePatient, is(not(sameGoldenResourceAs(janePractitioner)))); + } + + @Test + public void testPractitionersThatMatchShouldLink() { + Practitioner janePractitioner = createPractitionerAndUpdateLinks(buildJanePractitioner()); + Practitioner anotherJanePractitioner = createPractitionerAndUpdateLinks(buildJanePractitioner()); + + assertLinkCount(2); + assertThat(anotherJanePractitioner, is(sameGoldenResourceAs(janePractitioner))); + } + + @Test + public void testWhenThereAreNoMATCHOrPOSSIBLE_MATCHOutcomesThatANewGoldenResourceIsCreated() { + /** + * CASE 1: No MATCHED and no PROBABLE_MATCHED outcomes -> a new GoldenResource resource + * is created and linked to that Pat/Prac. + */ + assertLinkCount(0); + Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); + assertLinkCount(1); + assertThat(janePatient, is(matchedToAGoldenResource())); + } + + @Test + public void testWhenAllMATCHResultsAreToSameGoldenResourceThatTheyAreLinked() { + /** + * CASE 2: All of the MATCHED Pat/Prac resources are already linked to the same GoldenResource -> + * a new Link is created between the new Pat/Prac and that GoldenResource and is set to MATCHED. + */ + Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); + Patient janePatient2 = createPatientAndUpdateLinks(buildJanePatient()); + + assertLinkCount(2); + assertThat(janePatient, is(sameGoldenResourceAs(janePatient2))); + + Patient incomingJanePatient = createPatientAndUpdateLinks(buildJanePatient()); + assertThat(incomingJanePatient, is(sameGoldenResourceAs(janePatient, janePatient2))); + assertThat(incomingJanePatient, is(linkedTo(janePatient, janePatient2))); + } + + @Test + public void testMATCHResultWithMultipleCandidatesCreatesPOSSIBLE_DUPLICATELinksAndNoGoldenResourceIsCreated() { + /** + * CASE 3: The MATCHED Pat/Prac resources link to more than one GoldenResource -> Mark all links as POSSIBLE_MATCH. + * All other GoldenResource resources are marked as POSSIBLE_DUPLICATE of this first GoldenResource. + */ + Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); + Patient janePatient2 = createPatient(buildJanePatient()); + + //In a normal situation, janePatient2 would just match to jane patient, but here we need to hack it so they are their + //own individual GoldenResource for the purpose of this test. + IAnyResource goldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(janePatient2, new MdmTransactionContext(MdmTransactionContext.OperationType.CREATE_RESOURCE)); + myMdmLinkSvc.updateLink(goldenResource, janePatient2, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, createContextForCreate("Patient")); + assertThat(janePatient, is(not(sameGoldenResourceAs(janePatient2)))); + + //In theory, this will match both GoldenResources! + Patient incomingJanePatient = createPatientAndUpdateLinks(buildJanePatient()); + + //There should now be a single POSSIBLE_DUPLICATE link with + assertThat(janePatient, is(possibleDuplicateOf(janePatient2))); + + //There should now be 2 POSSIBLE_MATCH links with this goldenResource. + assertThat(incomingJanePatient, is(possibleMatchWith(janePatient, janePatient2))); + + //Ensure there is no successful MATCH links for incomingJanePatient + Optional matchedLinkForTargetPid = runInTransaction(() -> myMdmLinkDaoSvc.getMatchedLinkForSourcePid(myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), incomingJanePatient))); + assertThat(matchedLinkForTargetPid.isPresent(), is(false)); + + logAllLinks(); + assertLinksMatchResult(MATCH, MATCH, POSSIBLE_MATCH, POSSIBLE_MATCH, POSSIBLE_DUPLICATE); + assertLinksCreatedNewResource(true, true, false, false, false); + assertLinksMatchedByEid(false, false, false, false, false); + } + + @Test + public void testWhenAllMatchResultsArePOSSIBLE_MATCHThattheyAreLinkedAndNoGoldenResourceIsCreated() { + /** + * CASE 4: Only POSSIBLE_MATCH outcomes -> In this case, mdm-link records are created with POSSIBLE_MATCH + * outcome and await manual assignment to either NO_MATCH or MATCHED. GoldenResource link is added. + */ + Patient patient = buildJanePatient(); + patient.getNameFirstRep().setFamily("familyone"); + patient = createPatientAndUpdateLinks(patient); + assertThat(patient, is(sameGoldenResourceAs(patient))); + + Patient patient2 = buildJanePatient(); + patient2.getNameFirstRep().setFamily("pleasedonotmatchatall"); + patient2 = createPatientAndUpdateLinks(patient2); + assertThat(patient2, is(possibleMatchWith(patient))); + + Patient patient3 = buildJanePatient(); + patient3.getNameFirstRep().setFamily("pleasedonotmatchatall"); + patient3 = createPatientAndUpdateLinks(patient3); + + assertThat(patient3, is(possibleMatchWith(patient2))); + assertThat(patient3, is(possibleMatchWith(patient))); + + IBundleProvider bundle = myPatientDao.search(buildGoldenRecordSearchParameterMap()); + assertEquals(1, bundle.size()); + + //TODO GGG MDM: Convert these asserts to checking the MPI_LINK table + + assertLinksMatchResult(MATCH, POSSIBLE_MATCH, POSSIBLE_MATCH); + assertLinksCreatedNewResource(true, false, false); + assertLinksMatchedByEid(false, false, false); + } + + private SearchParameterMap buildGoldenRecordSearchParameterMap() { + SearchParameterMap searchParameterMap = new SearchParameterMap(); + searchParameterMap.setLoadSynchronous(true); + searchParameterMap.add("_tag", new TokenParam(MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_HAPI_MDM_MANAGED)); + return searchParameterMap; + } + + @Test + public void testWhenAnIncomingResourceHasMatchesAndPossibleMatchesThatItLinksToMatch() { + Patient patient = buildJanePatient(); + patient.getNameFirstRep().setFamily("familyone"); + patient = createPatientAndUpdateLinks(patient); + assertThat(patient, is(sameGoldenResourceAs(patient))); + + Patient patient2 = buildJanePatient(); + patient2.getNameFirstRep().setFamily("pleasedonotmatchatall"); + patient2 = createPatientAndUpdateLinks(patient2); + + Patient patient3 = buildJanePatient(); + patient3.getNameFirstRep().setFamily("familyone"); + patient3 = createPatientAndUpdateLinks(patient3); + + assertThat(patient2, is(not(sameGoldenResourceAs(patient)))); + assertThat(patient2, is(possibleMatchWith(patient))); + assertThat(patient3, is(sameGoldenResourceAs(patient))); + } + + + @Test + public void testPossibleMatchUpdatedToMatch() { + // setup + Patient patient = buildJanePatient(); + patient.getNameFirstRep().setFamily("familyone"); + patient = createPatientAndUpdateLinks(patient); + assertThat(patient, is(sameGoldenResourceAs(patient))); + + Patient patient2 = buildJanePatient(); + patient2.getNameFirstRep().setFamily("pleasedonotmatchatall"); + patient2 = createPatientAndUpdateLinks(patient2); + + assertThat(patient2, is(not(sameGoldenResourceAs(patient)))); + assertThat(patient2, is(not(linkedTo(patient)))); + assertThat(patient2, is(possibleMatchWith(patient))); + + patient2.getNameFirstRep().setFamily(patient.getNameFirstRep().getFamily()); + + // execute + updatePatientAndUpdateLinks(patient2); + + // validate + assertThat(patient2, is(linkedTo(patient))); + assertThat(patient2, is(sameGoldenResourceAs(patient))); + } + + @Test + public void testCreateGoldenResourceFromMdmTarget() { + // Create Use Case #2 - adding patient with no EID + Patient janePatient = buildJanePatient(); + Patient janeGoldenResourcePatient = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(janePatient, new MdmTransactionContext(MdmTransactionContext.OperationType.CREATE_RESOURCE)); + + // golden record now contains HAPI-generated EID and HAPI tag + assertTrue(MdmResourceUtil.isMdmManaged(janeGoldenResourcePatient)); + assertFalse(myEidHelper.getHapiEid(janeGoldenResourcePatient).isEmpty()); + + // original checks - verifies that EIDs are assigned + assertThat("Resource must not be identical", janePatient != janeGoldenResourcePatient); + assertFalse(janePatient.getIdentifier().isEmpty()); + assertFalse(janeGoldenResourcePatient.getIdentifier().isEmpty()); + + CanonicalEID janeId = myEidHelper.getHapiEid(janePatient).get(0); + CanonicalEID janeGoldenResourceId = myEidHelper.getHapiEid(janeGoldenResourcePatient).get(0); + + // source and target EIDs must match, as target EID should be reset to the newly created EID + assertEquals(janeId.getValue(), janeGoldenResourceId.getValue()); + assertEquals(janeId.getSystem(), janeGoldenResourceId.getSystem()); + } + + //Case #1 + @Test + public void testPatientUpdateOverwritesGoldenResourceDataOnChanges() { + Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); + Patient janeSourcePatient = getGoldenResourceFromTargetResource(janePatient); + + //Change Jane's name to paul. + Patient patient1 = buildPaulPatient(); + patient1.setId(janePatient.getId()); + Patient janePaulPatient = updatePatientAndUpdateLinks(patient1); + + assertThat(janeSourcePatient, is(sameGoldenResourceAs(janePaulPatient))); + + //Ensure the related GoldenResource was updated with new info. + Patient sourcePatientFromTarget = getGoldenResourceFromTargetResource(janePaulPatient); + HumanName nameFirstRep = sourcePatientFromTarget.getNameFirstRep(); + + assertThat(nameFirstRep.getGivenAsSingleString(), is(equalToIgnoringCase("paul"))); + } + + @Test + //Test Case #1 + public void testPatientUpdatesOverwriteGoldenResourceData() { + Patient paul = buildPaulPatient(); + String incorrectBirthdate = "1980-06-27"; + paul.getBirthDateElement().setValueAsString(incorrectBirthdate); + paul = createPatientAndUpdateLinks(paul); + + Patient sourcePatientFromTarget = getGoldenResourceFromTargetResource(paul); + assertThat(sourcePatientFromTarget.getBirthDateElement().getValueAsString(), is(incorrectBirthdate)); + + String correctBirthdate = "1990-06-28"; + paul.getBirthDateElement().setValueAsString(correctBirthdate); + + paul = updatePatientAndUpdateLinks(paul); + + sourcePatientFromTarget = getGoldenResourceFromTargetResource(paul); + assertThat(sourcePatientFromTarget.getBirthDateElement().getValueAsString(), is(equalTo(correctBirthdate))); + assertLinkCount(1); + } + + @Test + // Test Case #3 + public void testUpdatedEidThatWouldRelinkAlsoCausesPossibleDuplicate() { + Patient paul = createPatientAndUpdateLinks(addExternalEID(buildPaulPatient(), EID_1)); + Patient originalPaulGolden = getGoldenResourceFromTargetResource(paul); + + Patient jane = createPatientAndUpdateLinks(addExternalEID(buildJanePatient(), EID_2)); + Patient originalJaneGolden = getGoldenResourceFromTargetResource(jane); + + clearExternalEIDs(paul); + addExternalEID(paul, EID_2); + updatePatientAndUpdateLinks(paul); + + assertThat(originalJaneGolden, is(possibleDuplicateOf(originalPaulGolden))); + assertThat(jane, is(sameGoldenResourceAs(paul))); + } + + @Test + // Test Case #3a + public void originalLinkIsNoMatch() { + // setup + Patient paul = createPatientAndUpdateLinks(addExternalEID(buildPaulPatient(), EID_1)); + Patient originalPaulGolden = getGoldenResourceFromTargetResource(paul); + + Patient jane = createPatientAndUpdateLinks(addExternalEID(buildJanePatient(), EID_2)); + Patient originalJaneGolden = getGoldenResourceFromTargetResource(jane); + + MdmTransactionContext mdmCtx = buildUpdateLinkMdmTransactionContext(); + myMdmLinkUpdaterSvc.updateLink(originalPaulGolden, paul, NO_MATCH, mdmCtx); + + clearExternalEIDs(paul); + addExternalEID(paul, EID_2); + + // execute + updatePatientAndUpdateLinks(paul); + + // verify + assertThat(originalJaneGolden, is(not(possibleDuplicateOf(originalPaulGolden)))); + assertThat(jane, is(sameGoldenResourceAs(paul))); + } + + @Test + public void testSinglyLinkedGoldenResourceThatGetsAnUpdatedEidSimplyUpdatesEID() { + //Use Case # 2 + Patient paul = createPatientAndUpdateLinks(addExternalEID(buildPaulPatient(), EID_1)); + Patient originalPaulGolden = getGoldenResourceFromTargetResource(paul); + + String oldEid = myEidHelper.getExternalEid(originalPaulGolden).get(0).getValue(); + assertThat(oldEid, is(equalTo(EID_1))); + + clearExternalEIDs(paul); + addExternalEID(paul, EID_2); + + paul = updatePatientAndUpdateLinks(paul); + assertNoDuplicates(); + + Patient newlyFoundPaulPatient = getGoldenResourceFromTargetResource(paul); + assertThat(originalPaulGolden, is(sameGoldenResourceAs(newlyFoundPaulPatient))); + String newEid = myEidHelper.getExternalEid(newlyFoundPaulPatient).get(0).getValue(); + assertThat(newEid, is(equalTo(EID_2))); + } + + private void assertNoDuplicates() { + List possibleDuplicates = (List) myMdmLinkDaoSvc.getPossibleDuplicates(); + assertThat(possibleDuplicates, hasSize(0)); + } + + @Test + //Test Case #3 + public void testWhenAnEidChangeWouldCauseARelinkingThatAPossibleDuplicateIsCreated() { + Patient patient1 = buildJanePatient(); + addExternalEID(patient1, "eid-1"); + patient1 = createPatientAndUpdateLinks(patient1); + + Patient patient2 = buildPaulPatient(); + addExternalEID(patient2, "eid-2"); + patient2 = createPatientAndUpdateLinks(patient2); + + Patient patient3 = buildPaulPatient(); + addExternalEID(patient3, "eid-2"); + patient3 = createPatientAndUpdateLinks(patient3); + + //Now, Patient 2 and 3 are linked, and the GoldenResource has 2 eids. + assertThat(patient2, is(sameGoldenResourceAs(patient3))); + assertNoDuplicates(); + // GoldenResource A -> {P1} + // GoldenResource B -> {P2, P3} + + patient2.getIdentifier().clear(); + addExternalEID(patient2, "eid-1"); + patient2 = updatePatientAndUpdateLinks(patient2); + + // GoldenResource A -> {P1, P2} + // GoldenResource B -> {P3} + // Possible duplicates A<->B + + assertThat(patient2, is(sameGoldenResourceAs(patient1))); + + List possibleDuplicates = (List) myMdmLinkDaoSvc.getPossibleDuplicates(); + assertThat(possibleDuplicates, hasSize(1)); + assertThat(patient3, is(possibleDuplicateOf(patient1))); + } + + @Test + public void testWhen_POSSIBLE_MATCH_And_POSSIBLE_DUPLICATE_LinksCreated_ScorePopulatedOnPossibleMatchLinks() { + Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); + Patient janePatient2 = createPatient(buildJanePatient()); + + //In a normal situation, janePatient2 would just match to jane patient, but here we need to hack it so they are their + //own individual GoldenResource for the purpose of this test. + IAnyResource goldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(janePatient2, + new MdmTransactionContext(MdmTransactionContext.OperationType.CREATE_RESOURCE)); + myMdmLinkSvc.updateLink(goldenResource, janePatient2, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, + MdmLinkSourceEnum.AUTO, createContextForCreate("Patient")); + assertThat(janePatient, is(not(sameGoldenResourceAs(janePatient2)))); + + //In theory, this will match both GoldenResources! + Patient incomingJanePatient = createPatientAndUpdateLinks(buildJanePatient()); + + //There should now be a single POSSIBLE_DUPLICATE link with + assertThat(janePatient, is(possibleDuplicateOf(janePatient2))); + + //There should now be 2 POSSIBLE_MATCH links with this goldenResource. + assertThat(incomingJanePatient, is(possibleMatchWith(janePatient, janePatient2))); + + // Ensure both links are POSSIBLE_MATCH and both have a score value + List janetPatientLinks = runInTransaction(() -> myMdmLinkDaoSvc.findMdmLinksBySourceResource(incomingJanePatient)); + assertEquals(2, janetPatientLinks.size()); + janetPatientLinks.forEach(l -> { + assertEquals(MdmMatchResultEnum.POSSIBLE_MATCH, l.getMatchResult()); + assertNotNull(l.getScore()); + }); + } } - @Test - public void testAddMedicationLinksToNewGoldenRecordMedicationIfNoneFound() { - createDummyOrganization(); + @Nested + @ContextConfiguration(classes = { + BlockListConfig.class + }) + public class BlockLinkTest extends BaseMdmR4Test { - createMedicationAndUpdateLinks(buildMedication("Organization/mfr")); - assertLinkCount(1); - assertLinksMatchResult(MATCH); - assertLinksCreatedNewResource(true); - assertLinksMatchedByEid(false); - assertLinksMatchScore(1.0); - assertLinksMatchVector((Long) null); + @Autowired + private IBlockListRuleProvider myBlockListRuleProvider; + + @Test + public void updateMdmLinksForMdmSource_createBlockedResource_alwaysCreatesNewGoldenResource() { + // setup + String blockedFirstName = "Jane"; + String blockedLastName = "Doe"; + + BlockListJson blockListJson = new BlockListJson(); + BlockListRuleJson rule = new BlockListRuleJson(); + rule.setResourceType("Patient"); + rule.addBlockListField() + .setFhirPath("name.single().family") + .setBlockedValue(blockedLastName); + rule.addBlockListField() + .setFhirPath("name.single().given.first()") + .setBlockedValue(blockedFirstName); + blockListJson.addBlockListRule(rule); + + MdmTransactionContext mdmContext = createContextForCreate("Patient"); + + // when + when(myBlockListRuleProvider.getBlocklistRules()) + .thenReturn(blockListJson); + + // create patients + Patient unblockedPatient; + { + unblockedPatient = buildJanePatient(); + unblockedPatient = createPatient(unblockedPatient); + myMdmMatchLinkSvc.updateMdmLinksForMdmSource(unblockedPatient, mdmContext); + } + + // our blocked name is Jane Doe... let's make sure that's the case + Patient blockedPatient = buildJanePatient(); + assertEquals(blockedLastName, blockedPatient.getName().get(0).getFamily()); + assertEquals(blockedFirstName, blockedPatient.getName().get(0).getGivenAsSingleString()); + blockedPatient = createPatient(blockedPatient); + + // test + myMdmMatchLinkSvc.updateMdmLinksForMdmSource(blockedPatient, mdmContext); + + // verify + List grs = getAllGoldenPatients(); + assertEquals(2, grs.size()); + assertEquals(0, myMdmLinkDaoSvc.getPossibleDuplicates().size()); + + List links = new ArrayList<>(); + for (IBaseResource gr : grs) { + links.addAll(getAllMdmLinks((Patient)gr)); + } + assertEquals(2, links.size()); + Set ids = new HashSet<>(); + for (MdmLink link : links) { + JpaPid pid = link.getSourcePersistenceId(); + assertTrue(ids.add(pid.getId())); + JpaPid gpid = link.getGoldenResourcePersistenceId(); + assertTrue(ids.add(gpid.getId())); + } + } + + public List getAllMdmLinks(Patient theGoldenPatient) { + return myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theGoldenPatient).stream() + .map( link -> (MdmLink) link) + .collect(Collectors.toList()); + } } - - @Test - public void testAddPatientLinksToNewlyCreatedResourceIfNoMatch() { - Patient patient1 = createPatientAndUpdateLinks(buildJanePatient()); - Patient patient2 = createPatientAndUpdateLinks(buildPaulPatient()); - - assertLinkCount(2); - - assertThat(patient1, is(not(sameGoldenResourceAs(patient2)))); - - assertLinksMatchResult(MATCH, MATCH); - assertLinksCreatedNewResource(true, true); - assertLinksMatchedByEid(false, false); - assertLinksMatchScore(1.0, 1.0); - assertLinksMatchVector(null, null); - } - - @Test - public void testAddPatientLinksToExistingGoldenResourceIfMatch() { - Patient patient1 = createPatientAndUpdateLinks(buildJanePatient()); - assertLinkCount(1); - - Patient patient2 = createPatientAndUpdateLinks(buildJanePatient()); - assertLinkCount(2); - - assertThat(patient1, is(sameGoldenResourceAs(patient2))); - assertLinksMatchResult(MATCH, MATCH); - assertLinksCreatedNewResource(true, false); - assertLinksMatchedByEid(false, false); - assertLinksMatchScore(1.0, 2.0/3.0); - assertLinksMatchVector(null, 6L); - } - - @Test - public void testWhenMatchOccursOnGoldenResourceThatHasBeenManuallyNOMATCHedThatItIsBlocked() { - Patient originalJane = createPatientAndUpdateLinks(buildJanePatient()); - IAnyResource janeGoldenResource = getGoldenResourceFromTargetResource(originalJane); - - //Create a manual NO_MATCH between janeGoldenResource and unmatchedJane. - Patient unmatchedJane = createPatient(buildJanePatient()); - myMdmLinkSvc.updateLink(janeGoldenResource, unmatchedJane, MdmMatchOutcome.NO_MATCH, MdmLinkSourceEnum.MANUAL, createContextForCreate("Patient")); - - //rerun MDM rules against unmatchedJane. - myMdmMatchLinkSvc.updateMdmLinksForMdmSource(unmatchedJane, createContextForCreate("Patient")); - - assertThat(unmatchedJane, is(not(sameGoldenResourceAs(janeGoldenResource)))); - assertThat(unmatchedJane, is(not(linkedTo(originalJane)))); - - assertLinksMatchResult(MATCH, NO_MATCH, MATCH); - assertLinksCreatedNewResource(true, false, true); - assertLinksMatchedByEid(false, false, false); - assertLinksMatchScore(1.0, null, 1.0); - assertLinksMatchVector(null, null, null); - } - - @Test - public void testWhenPOSSIBLE_MATCHOccursOnGoldenResourceThatHasBeenManuallyNOMATCHedThatItIsBlocked() { - Patient originalJane = createPatientAndUpdateLinks(buildJanePatient()); - - IBundleProvider search = myPatientDao.search(buildGoldenRecordSearchParameterMap()); - IAnyResource janeGoldenResource = (IAnyResource) search.getResources(0, 1).get(0); - - Patient unmatchedPatient = createPatient(buildJanePatient()); - - // This simulates an admin specifically saying that unmatchedPatient does NOT match janeGoldenResource. - myMdmLinkSvc.updateLink(janeGoldenResource, unmatchedPatient, MdmMatchOutcome.NO_MATCH, MdmLinkSourceEnum.MANUAL, createContextForCreate("Patient")); - // TODO change this so that it will only partially match. - - //Now normally, when we run update links, it should link to janeGoldenResource. However, this manual NO_MATCH link - //should cause a whole new GoldenResource to be created. - myMdmMatchLinkSvc.updateMdmLinksForMdmSource(unmatchedPatient, createContextForCreate("Patient")); - - assertThat(unmatchedPatient, is(not(sameGoldenResourceAs(janeGoldenResource)))); - assertThat(unmatchedPatient, is(not(linkedTo(originalJane)))); - - assertLinksMatchResult(MATCH, NO_MATCH, MATCH); - assertLinksCreatedNewResource(true, false, true); - assertLinksMatchedByEid(false, false, false); - assertLinksMatchScore(1.0, null, 1.0); - assertLinksMatchVector(null, null, null); - } - - @Test - public void testWhenPatientIsCreatedWithEIDThatItPropagatesToNewGoldenResource() { - String sampleEID = "sample-eid"; - Patient janePatient = addExternalEID(buildJanePatient(), sampleEID); - janePatient = createPatientAndUpdateLinks(janePatient); - - Optional mdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(JpaPid.fromId(janePatient.getIdElement().getIdPartAsLong())); - assertThat(mdmLink.isPresent(), is(true)); - - Patient patient = getTargetResourceFromMdmLink(mdmLink.get(), "Patient"); - List externalEid = myEidHelper.getExternalEid(patient); - - assertThat(externalEid.get(0).getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient")))); - assertThat(externalEid.get(0).getValue(), is(equalTo(sampleEID))); - } - - @Test - public void testWhenPatientIsCreatedWithoutAnEIDTheGoldenResourceGetsAutomaticallyAssignedOne() { - Patient patient = createPatientAndUpdateLinks(buildJanePatient()); - IMdmLink mdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(JpaPid.fromId(patient.getIdElement().getIdPartAsLong())).get(); - - Patient targetPatient = getTargetResourceFromMdmLink(mdmLink, "Patient"); - Identifier identifierFirstRep = targetPatient.getIdentifierFirstRep(); - assertThat(identifierFirstRep.getSystem(), is(equalTo(MdmConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM))); - assertThat(identifierFirstRep.getValue(), not(blankOrNullString())); - } - - @Test - public void testPatientAttributesAreCopiedOverWhenGoldenResourceIsCreatedFromPatient() { - Patient patient = createPatientAndUpdateLinks(buildPatientWithNameIdAndBirthday("Gary", "GARY_ID", new Date())); - - Optional mdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(JpaPid.fromId(patient.getIdElement().getIdPartAsLong())); - Patient read = getTargetResourceFromMdmLink(mdmLink.get(), "Patient"); - - assertThat(read.getNameFirstRep().getFamily(), is(equalTo(patient.getNameFirstRep().getFamily()))); - assertThat(read.getNameFirstRep().getGivenAsSingleString(), is(equalTo(patient.getNameFirstRep().getGivenAsSingleString()))); - assertThat(read.getBirthDateElement().toHumanDisplay(), is(equalTo(patient.getBirthDateElement().toHumanDisplay()))); - assertThat(read.getTelecomFirstRep().getValue(), is(equalTo(patient.getTelecomFirstRep().getValue()))); - assertThat(read.getPhoto().size(), is(equalTo(patient.getPhoto().size()))); - assertThat(read.getPhotoFirstRep().getData(), is(equalTo(patient.getPhotoFirstRep().getData()))); - assertThat(read.getGender(), is(equalTo(patient.getGender()))); - } - - @Test - public void testPatientMatchingAnotherPatientLinksToSameGoldenResource() { - Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); - Patient sameJanePatient = createPatientAndUpdateLinks(buildJanePatient()); - assertThat(janePatient, is(sameGoldenResourceAs(sameJanePatient))); - } - - @Test - public void testIncomingPatientWithEIDThatMatchesGoldenResourceWithHapiEidAddsExternalEidToGoldenResource() { - // Existing GoldenResource with system-assigned EID found linked from matched Patient. incoming Patient has EID. - // Replace GoldenResource system-assigned EID with Patient EID. - Patient patient = createPatientAndUpdateLinks(buildJanePatient()); - - IAnyResource janeGoldenResource = getGoldenResourceFromTargetResource(patient); - List hapiEid = myEidHelper.getHapiEid(janeGoldenResource); - String foundHapiEid = hapiEid.get(0).getValue(); - - Patient janePatient = addExternalEID(buildJanePatient(), "12345"); - createPatientAndUpdateLinks(janePatient); - - //We want to make sure the patients were linked to the same Golden Resource. - assertThat(patient, is(sameGoldenResourceAs(janePatient))); - - Patient sourcePatient = getGoldenResourceFromTargetResource(patient); - - List identifier = sourcePatient.getIdentifier(); - - //The collision should have kept the old identifier - Identifier firstIdentifier = identifier.get(0); - assertThat(firstIdentifier.getSystem(), is(equalTo(MdmConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM))); - assertThat(firstIdentifier.getValue(), is(equalTo(foundHapiEid))); - - //The collision should have added a new identifier with the external system. - Identifier secondIdentifier = identifier.get(1); - assertThat(secondIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient")))); - assertThat(secondIdentifier.getValue(), is(equalTo("12345"))); - } - - @Test - public void testIncomingPatientWithEidMatchesAnotherPatientWithSameEIDAreLinked() { - // Create Use Case #3 - Patient patient1 = addExternalEID(buildJanePatient(), "uniqueid"); - createPatientAndUpdateLinks(patient1); - - Patient patient2 = buildPaulPatient(); - patient2 = addExternalEID(patient2, "uniqueid"); - createPatientAndUpdateLinks(patient2); - - assertThat(patient1, is(sameGoldenResourceAs(patient2))); - } - - @Test - public void testHavingMultipleEIDsOnIncomingPatientMatchesCorrectly() { - Patient patient1 = buildJanePatient(); - addExternalEID(patient1, "id_1"); - addExternalEID(patient1, "id_2"); - addExternalEID(patient1, "id_3"); - addExternalEID(patient1, "id_4"); - createPatientAndUpdateLinks(patient1); - - Patient patient2 = buildPaulPatient(); - addExternalEID(patient2, "id_5"); - addExternalEID(patient2, "id_1"); - createPatientAndUpdateLinks(patient2); - - assertThat(patient1, is(sameGoldenResourceAs(patient2))); - } - - @Test - public void testDuplicateGoldenResourceLinkIsCreatedWhenAnIncomingPatientArrivesWithEIDThatMatchesAnotherEIDPatient() { - - Patient patient1 = addExternalEID(buildJanePatient(), "eid-1"); - patient1 = createPatientAndUpdateLinks(patient1); - - Patient patient2 = addExternalEID(buildJanePatient(), "eid-2"); - patient2 = createPatientAndUpdateLinks(patient2); - - List possibleDuplicates = (List) myMdmLinkDaoSvc.getPossibleDuplicates(); - assertThat(possibleDuplicates, hasSize(1)); - - Patient finalPatient1 = patient1; - Patient finalPatient2 = patient2; - List duplicatePids = runInTransaction(()->Stream.of(finalPatient1, finalPatient2) - .map(t -> myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), getGoldenResourceFromTargetResource(t))) - .collect(Collectors.toList())); - - //The two GoldenResources related to the patients should both show up in the only existing POSSIBLE_DUPLICATE MdmLink. - MdmLink mdmLink = possibleDuplicates.get(0); - assertThat(mdmLink.getGoldenResourcePersistenceId(), is(in(duplicatePids))); - assertThat(mdmLink.getSourcePersistenceId(), is(in(duplicatePids))); - } - - @Test - public void testPatientWithNoMdmTagIsNotMatched() { - // Patient with "no-mdm" tag is not matched - Patient janePatient = buildJanePatient(); - janePatient.getMeta().addTag(MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_NO_MDM_MANAGED, "Don't MDM on me!"); - String s = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(janePatient); - createPatientAndUpdateLinks(janePatient); - assertLinkCount(0); - } - - @Test - public void testPractitionersDoNotMatchToPatients() { - Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); - Practitioner janePractitioner = createPractitionerAndUpdateLinks(buildJanePractitioner()); - - assertLinkCount(2); - assertThat(janePatient, is(not(sameGoldenResourceAs(janePractitioner)))); - } - - @Test - public void testPractitionersThatMatchShouldLink() { - Practitioner janePractitioner = createPractitionerAndUpdateLinks(buildJanePractitioner()); - Practitioner anotherJanePractitioner = createPractitionerAndUpdateLinks(buildJanePractitioner()); - - assertLinkCount(2); - assertThat(anotherJanePractitioner, is(sameGoldenResourceAs(janePractitioner))); - } - - @Test - public void testWhenThereAreNoMATCHOrPOSSIBLE_MATCHOutcomesThatANewGoldenResourceIsCreated() { - /** - * CASE 1: No MATCHED and no PROBABLE_MATCHED outcomes -> a new GoldenResource resource - * is created and linked to that Pat/Prac. - */ - assertLinkCount(0); - Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); - assertLinkCount(1); - assertThat(janePatient, is(matchedToAGoldenResource())); - } - - @Test - public void testWhenAllMATCHResultsAreToSameGoldenResourceThatTheyAreLinked() { - /** - * CASE 2: All of the MATCHED Pat/Prac resources are already linked to the same GoldenResource -> - * a new Link is created between the new Pat/Prac and that GoldenResource and is set to MATCHED. - */ - Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); - Patient janePatient2 = createPatientAndUpdateLinks(buildJanePatient()); - - assertLinkCount(2); - assertThat(janePatient, is(sameGoldenResourceAs(janePatient2))); - - Patient incomingJanePatient = createPatientAndUpdateLinks(buildJanePatient()); - assertThat(incomingJanePatient, is(sameGoldenResourceAs(janePatient, janePatient2))); - assertThat(incomingJanePatient, is(linkedTo(janePatient, janePatient2))); - } - - @Test - public void testMATCHResultWithMultipleCandidatesCreatesPOSSIBLE_DUPLICATELinksAndNoGoldenResourceIsCreated() { - /** - * CASE 3: The MATCHED Pat/Prac resources link to more than one GoldenResource -> Mark all links as POSSIBLE_MATCH. - * All other GoldenResource resources are marked as POSSIBLE_DUPLICATE of this first GoldenResource. - */ - Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); - Patient janePatient2 = createPatient(buildJanePatient()); - - //In a normal situation, janePatient2 would just match to jane patient, but here we need to hack it so they are their - //own individual GoldenResource for the purpose of this test. - IAnyResource goldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(janePatient2, new MdmTransactionContext(MdmTransactionContext.OperationType.CREATE_RESOURCE)); - myMdmLinkSvc.updateLink(goldenResource, janePatient2, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, createContextForCreate("Patient")); - assertThat(janePatient, is(not(sameGoldenResourceAs(janePatient2)))); - - //In theory, this will match both GoldenResources! - Patient incomingJanePatient = createPatientAndUpdateLinks(buildJanePatient()); - - //There should now be a single POSSIBLE_DUPLICATE link with - assertThat(janePatient, is(possibleDuplicateOf(janePatient2))); - - //There should now be 2 POSSIBLE_MATCH links with this goldenResource. - assertThat(incomingJanePatient, is(possibleMatchWith(janePatient, janePatient2))); - - //Ensure there is no successful MATCH links for incomingJanePatient - Optional matchedLinkForTargetPid = runInTransaction(()->myMdmLinkDaoSvc.getMatchedLinkForSourcePid(myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), incomingJanePatient))); - assertThat(matchedLinkForTargetPid.isPresent(), is(false)); - - logAllLinks(); - assertLinksMatchResult(MATCH, MATCH, POSSIBLE_MATCH, POSSIBLE_MATCH, POSSIBLE_DUPLICATE); - assertLinksCreatedNewResource(true, true, false, false, false); - assertLinksMatchedByEid(false, false, false, false, false); - } - - @Test - public void testWhenAllMatchResultsArePOSSIBLE_MATCHThattheyAreLinkedAndNoGoldenResourceIsCreated() { - /** - * CASE 4: Only POSSIBLE_MATCH outcomes -> In this case, mdm-link records are created with POSSIBLE_MATCH - * outcome and await manual assignment to either NO_MATCH or MATCHED. GoldenResource link is added. - */ - Patient patient = buildJanePatient(); - patient.getNameFirstRep().setFamily("familyone"); - patient = createPatientAndUpdateLinks(patient); - assertThat(patient, is(sameGoldenResourceAs(patient))); - - Patient patient2 = buildJanePatient(); - patient2.getNameFirstRep().setFamily("pleasedonotmatchatall"); - patient2 = createPatientAndUpdateLinks(patient2); - assertThat(patient2, is(possibleMatchWith(patient))); - - Patient patient3 = buildJanePatient(); - patient3.getNameFirstRep().setFamily("pleasedonotmatchatall"); - patient3 = createPatientAndUpdateLinks(patient3); - - assertThat(patient3, is(possibleMatchWith(patient2))); - assertThat(patient3, is(possibleMatchWith(patient))); - - IBundleProvider bundle = myPatientDao.search(buildGoldenRecordSearchParameterMap()); - assertEquals(1, bundle.size()); - - //TODO GGG MDM: Convert these asserts to checking the MPI_LINK table - - assertLinksMatchResult(MATCH, POSSIBLE_MATCH, POSSIBLE_MATCH); - assertLinksCreatedNewResource(true, false, false); - assertLinksMatchedByEid(false, false, false); - } - - private SearchParameterMap buildGoldenRecordSearchParameterMap() { - SearchParameterMap searchParameterMap = new SearchParameterMap(); - searchParameterMap.setLoadSynchronous(true); - searchParameterMap.add("_tag", new TokenParam(MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_HAPI_MDM_MANAGED)); - return searchParameterMap; - } - - @Test - public void testWhenAnIncomingResourceHasMatchesAndPossibleMatchesThatItLinksToMatch() { - Patient patient = buildJanePatient(); - patient.getNameFirstRep().setFamily("familyone"); - patient = createPatientAndUpdateLinks(patient); - assertThat(patient, is(sameGoldenResourceAs(patient))); - - Patient patient2 = buildJanePatient(); - patient2.getNameFirstRep().setFamily("pleasedonotmatchatall"); - patient2 = createPatientAndUpdateLinks(patient2); - - Patient patient3 = buildJanePatient(); - patient3.getNameFirstRep().setFamily("familyone"); - patient3 = createPatientAndUpdateLinks(patient3); - - assertThat(patient2, is(not(sameGoldenResourceAs(patient)))); - assertThat(patient2, is(possibleMatchWith(patient))); - assertThat(patient3, is(sameGoldenResourceAs(patient))); - } - - - @Test - public void testPossibleMatchUpdatedToMatch() { - // setup - Patient patient = buildJanePatient(); - patient.getNameFirstRep().setFamily("familyone"); - patient = createPatientAndUpdateLinks(patient); - assertThat(patient, is(sameGoldenResourceAs(patient))); - - Patient patient2 = buildJanePatient(); - patient2.getNameFirstRep().setFamily("pleasedonotmatchatall"); - patient2 = createPatientAndUpdateLinks(patient2); - - assertThat(patient2, is(not(sameGoldenResourceAs(patient)))); - assertThat(patient2, is(not(linkedTo(patient)))); - assertThat(patient2, is(possibleMatchWith(patient))); - - patient2.getNameFirstRep().setFamily(patient.getNameFirstRep().getFamily()); - - // execute - updatePatientAndUpdateLinks(patient2); - - // validate - assertThat(patient2, is(linkedTo(patient))); - assertThat(patient2, is(sameGoldenResourceAs(patient))); - } - - @Test - public void testCreateGoldenResourceFromMdmTarget() { - // Create Use Case #2 - adding patient with no EID - Patient janePatient = buildJanePatient(); - Patient janeGoldenResourcePatient = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(janePatient, new MdmTransactionContext(MdmTransactionContext.OperationType.CREATE_RESOURCE)); - - // golden record now contains HAPI-generated EID and HAPI tag - assertTrue(MdmResourceUtil.isMdmManaged(janeGoldenResourcePatient)); - assertFalse(myEidHelper.getHapiEid(janeGoldenResourcePatient).isEmpty()); - - // original checks - verifies that EIDs are assigned - assertThat("Resource must not be identical", janePatient != janeGoldenResourcePatient); - assertFalse(janePatient.getIdentifier().isEmpty()); - assertFalse(janeGoldenResourcePatient.getIdentifier().isEmpty()); - - CanonicalEID janeId = myEidHelper.getHapiEid(janePatient).get(0); - CanonicalEID janeGoldenResourceId = myEidHelper.getHapiEid(janeGoldenResourcePatient).get(0); - - // source and target EIDs must match, as target EID should be reset to the newly created EID - assertEquals(janeId.getValue(), janeGoldenResourceId.getValue()); - assertEquals(janeId.getSystem(), janeGoldenResourceId.getSystem()); - } - - //Case #1 - @Test - public void testPatientUpdateOverwritesGoldenResourceDataOnChanges() { - Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); - Patient janeSourcePatient = getGoldenResourceFromTargetResource(janePatient); - - //Change Jane's name to paul. - Patient patient1 = buildPaulPatient(); - patient1.setId(janePatient.getId()); - Patient janePaulPatient = updatePatientAndUpdateLinks(patient1); - - assertThat(janeSourcePatient, is(sameGoldenResourceAs(janePaulPatient))); - - //Ensure the related GoldenResource was updated with new info. - Patient sourcePatientFromTarget = getGoldenResourceFromTargetResource(janePaulPatient); - HumanName nameFirstRep = sourcePatientFromTarget.getNameFirstRep(); - - assertThat(nameFirstRep.getGivenAsSingleString(), is(equalToIgnoringCase("paul"))); - } - - @Test - //Test Case #1 - public void testPatientUpdatesOverwriteGoldenResourceData() { - Patient paul = buildPaulPatient(); - String incorrectBirthdate = "1980-06-27"; - paul.getBirthDateElement().setValueAsString(incorrectBirthdate); - paul = createPatientAndUpdateLinks(paul); - - Patient sourcePatientFromTarget = getGoldenResourceFromTargetResource(paul); - assertThat(sourcePatientFromTarget.getBirthDateElement().getValueAsString(), is(incorrectBirthdate)); - - String correctBirthdate = "1990-06-28"; - paul.getBirthDateElement().setValueAsString(correctBirthdate); - - paul = updatePatientAndUpdateLinks(paul); - - sourcePatientFromTarget = getGoldenResourceFromTargetResource(paul); - assertThat(sourcePatientFromTarget.getBirthDateElement().getValueAsString(), is(equalTo(correctBirthdate))); - assertLinkCount(1); - } - - @Test - // Test Case #3 - public void testUpdatedEidThatWouldRelinkAlsoCausesPossibleDuplicate() { - Patient paul = createPatientAndUpdateLinks(addExternalEID(buildPaulPatient(), EID_1)); - Patient originalPaulGolden = getGoldenResourceFromTargetResource(paul); - - Patient jane = createPatientAndUpdateLinks(addExternalEID(buildJanePatient(), EID_2)); - Patient originalJaneGolden = getGoldenResourceFromTargetResource(jane); - - clearExternalEIDs(paul); - addExternalEID(paul, EID_2); - updatePatientAndUpdateLinks(paul); - - assertThat(originalJaneGolden, is(possibleDuplicateOf(originalPaulGolden))); - assertThat(jane, is(sameGoldenResourceAs(paul))); - } - - @Test - // Test Case #3a - public void originalLinkIsNoMatch() { - // setup - Patient paul = createPatientAndUpdateLinks(addExternalEID(buildPaulPatient(), EID_1)); - Patient originalPaulGolden = getGoldenResourceFromTargetResource(paul); - - Patient jane = createPatientAndUpdateLinks(addExternalEID(buildJanePatient(), EID_2)); - Patient originalJaneGolden = getGoldenResourceFromTargetResource(jane); - - MdmTransactionContext mdmCtx = buildUpdateLinkMdmTransactionContext(); - myMdmLinkUpdaterSvc.updateLink(originalPaulGolden, paul, NO_MATCH, mdmCtx); - - clearExternalEIDs(paul); - addExternalEID(paul, EID_2); - - // execute - updatePatientAndUpdateLinks(paul); - - // verify - assertThat(originalJaneGolden, is(not(possibleDuplicateOf(originalPaulGolden)))); - assertThat(jane, is(sameGoldenResourceAs(paul))); - } - - @Test - public void testSinglyLinkedGoldenResourceThatGetsAnUpdatedEidSimplyUpdatesEID() { - //Use Case # 2 - Patient paul = createPatientAndUpdateLinks(addExternalEID(buildPaulPatient(), EID_1)); - Patient originalPaulGolden = getGoldenResourceFromTargetResource(paul); - - String oldEid = myEidHelper.getExternalEid(originalPaulGolden).get(0).getValue(); - assertThat(oldEid, is(equalTo(EID_1))); - - clearExternalEIDs(paul); - addExternalEID(paul, EID_2); - - paul = updatePatientAndUpdateLinks(paul); - assertNoDuplicates(); - - Patient newlyFoundPaulPatient = getGoldenResourceFromTargetResource(paul); - assertThat(originalPaulGolden, is(sameGoldenResourceAs(newlyFoundPaulPatient))); - String newEid = myEidHelper.getExternalEid(newlyFoundPaulPatient).get(0).getValue(); - assertThat(newEid, is(equalTo(EID_2))); - } - - private void assertNoDuplicates() { - List possibleDuplicates = (List) myMdmLinkDaoSvc.getPossibleDuplicates(); - assertThat(possibleDuplicates, hasSize(0)); - } - - @Test - //Test Case #3 - public void testWhenAnEidChangeWouldCauseARelinkingThatAPossibleDuplicateIsCreated() { - Patient patient1 = buildJanePatient(); - addExternalEID(patient1, "eid-1"); - patient1 = createPatientAndUpdateLinks(patient1); - - Patient patient2 = buildPaulPatient(); - addExternalEID(patient2, "eid-2"); - patient2 = createPatientAndUpdateLinks(patient2); - - Patient patient3 = buildPaulPatient(); - addExternalEID(patient3, "eid-2"); - patient3 = createPatientAndUpdateLinks(patient3); - - //Now, Patient 2 and 3 are linked, and the GoldenResource has 2 eids. - assertThat(patient2, is(sameGoldenResourceAs(patient3))); - assertNoDuplicates(); - // GoldenResource A -> {P1} - // GoldenResource B -> {P2, P3} - - patient2.getIdentifier().clear(); - addExternalEID(patient2, "eid-1"); - patient2 = updatePatientAndUpdateLinks(patient2); - - // GoldenResource A -> {P1, P2} - // GoldenResource B -> {P3} - // Possible duplicates A<->B - - assertThat(patient2, is(sameGoldenResourceAs(patient1))); - - List possibleDuplicates = (List) myMdmLinkDaoSvc.getPossibleDuplicates(); - assertThat(possibleDuplicates, hasSize(1)); - assertThat(patient3, is(possibleDuplicateOf(patient1))); - } - - @Test - public void testWhen_POSSIBLE_MATCH_And_POSSIBLE_DUPLICATE_LinksCreated_ScorePopulatedOnPossibleMatchLinks() { - Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); - Patient janePatient2 = createPatient(buildJanePatient()); - - //In a normal situation, janePatient2 would just match to jane patient, but here we need to hack it so they are their - //own individual GoldenResource for the purpose of this test. - IAnyResource goldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(janePatient2, - new MdmTransactionContext(MdmTransactionContext.OperationType.CREATE_RESOURCE)); - myMdmLinkSvc.updateLink(goldenResource, janePatient2, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, - MdmLinkSourceEnum.AUTO, createContextForCreate("Patient")); - assertThat(janePatient, is(not(sameGoldenResourceAs(janePatient2)))); - - //In theory, this will match both GoldenResources! - Patient incomingJanePatient = createPatientAndUpdateLinks(buildJanePatient()); - - //There should now be a single POSSIBLE_DUPLICATE link with - assertThat(janePatient, is(possibleDuplicateOf(janePatient2))); - - //There should now be 2 POSSIBLE_MATCH links with this goldenResource. - assertThat(incomingJanePatient, is(possibleMatchWith(janePatient, janePatient2))); - - // Ensure both links are POSSIBLE_MATCH and both have a score value - List janetPatientLinks = runInTransaction(() -> myMdmLinkDaoSvc.findMdmLinksBySourceResource(incomingJanePatient)); - assertEquals(2, janetPatientLinks.size()); - janetPatientLinks.forEach( l -> { - assertEquals(MdmMatchResultEnum.POSSIBLE_MATCH, l.getMatchResult()); - assertNotNull(l.getScore()); - }); - } - } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/testmodels/BlockRuleTestCase.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/testmodels/BlockRuleTestCase.java new file mode 100644 index 00000000000..a0c9bd45bca --- /dev/null +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/testmodels/BlockRuleTestCase.java @@ -0,0 +1,52 @@ +package ca.uhn.fhir.jpa.mdm.svc.testmodels; + +import ca.uhn.fhir.mdm.blocklist.json.BlockListJson; +import org.hl7.fhir.instance.model.api.IBaseResource; + +public class BlockRuleTestCase { + + private final String myId; + + /** + * Block rule being tested + */ + private final BlockListJson myBlockRule; + + /** + * Resource being tested (we use only patients for now) + */ + private final IBaseResource myPatientResource; + + /** + * Expected block result; true if blocked, false if not blocked + */ + private final boolean myExpectedBlockResult; + + public BlockRuleTestCase( + String theId, + BlockListJson theJson, + IBaseResource theResource, + boolean theExpectedResult + ) { + myId = theId; + myBlockRule = theJson; + myPatientResource = theResource; + myExpectedBlockResult = theExpectedResult; + } + + public String getId() { + return myId; + } + + public BlockListJson getBlockRule() { + return myBlockRule; + } + + public IBaseResource getPatientResource() { + return myPatientResource; + } + + public boolean isExpectedBlockResult() { + return myExpectedBlockResult; + } +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/json/BlockListJson.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/json/BlockListJson.java new file mode 100644 index 00000000000..3354030f87f --- /dev/null +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/json/BlockListJson.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.mdm.blocklist.json; + +import ca.uhn.fhir.model.api.IModelJson; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +public class BlockListJson implements IModelJson { + + /** + * List of blocklistrules. + * Each item can be thought of as a 'ruleset'. + * These rulesets are applicable to a resource type. + * Each ruleset is applied as an 'or' to the resource being processed. + */ + @JsonProperty(value = "blocklist", required = true) + private List myBlockListItemJsonList; + + public List getBlockListItemJsonList() { + if (myBlockListItemJsonList == null) { + myBlockListItemJsonList = new ArrayList<>(); + } + return myBlockListItemJsonList; + } + + public BlockListJson addBlockListRule(BlockListRuleJson theRule) { + getBlockListItemJsonList().add(theRule); + return this; + } +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/json/BlockListRuleJson.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/json/BlockListRuleJson.java new file mode 100644 index 00000000000..954b5cdbbb1 --- /dev/null +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/json/BlockListRuleJson.java @@ -0,0 +1,42 @@ +package ca.uhn.fhir.mdm.blocklist.json; + +import ca.uhn.fhir.model.api.IModelJson; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; + +public class BlockListRuleJson implements IModelJson { + /** + * The resource type that this block list rule applies to. + */ + @JsonProperty(value = "resourceType", required = true) + private String myResourceType; + + /** + * The list of blocked fields that this rule applies to. + */ + @JsonProperty(value = "fields", required = true) + private List myBlockedFields; + + public String getResourceType() { + return myResourceType; + } + + public void setResourceType(String theResourceType) { + myResourceType = theResourceType; + } + + public List getBlockedFields() { + if (myBlockedFields == null) { + myBlockedFields = new ArrayList<>(); + } + return myBlockedFields; + } + + public BlockedFieldJson addBlockListField() { + BlockedFieldJson rule = new BlockedFieldJson(); + getBlockedFields().add(rule); + return rule; + } +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/json/BlockedFieldJson.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/json/BlockedFieldJson.java new file mode 100644 index 00000000000..89aa40832dc --- /dev/null +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/json/BlockedFieldJson.java @@ -0,0 +1,42 @@ +package ca.uhn.fhir.mdm.blocklist.json; + +import ca.uhn.fhir.model.api.IModelJson; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class BlockedFieldJson implements IModelJson { + + /** + * The fhir path to the field on the resource that is being + * processed. + * This path must lead to a single primitive value, + * otherwise no blocking can be detected. + */ + @JsonProperty(value = "fhirPath", required = true) + private String myFhirPath; + + /** + * The value to block on. + * If the value of the field at `fhirPath` matches this + * value, it will be blocked. + */ + @JsonProperty(value = "value", required = true) + private String myBlockedValue; + + public String getFhirPath() { + return myFhirPath; + } + + public BlockedFieldJson setFhirPath(String theFhirPath) { + myFhirPath = theFhirPath; + return this; + } + + public String getBlockedValue() { + return myBlockedValue; + } + + public BlockedFieldJson setBlockedValue(String theBlockedValue) { + myBlockedValue = theBlockedValue; + return this; + } +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/svc/IBlockListRuleProvider.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/svc/IBlockListRuleProvider.java new file mode 100644 index 00000000000..a761f60ae6d --- /dev/null +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/svc/IBlockListRuleProvider.java @@ -0,0 +1,11 @@ +package ca.uhn.fhir.mdm.blocklist.svc; + +import ca.uhn.fhir.mdm.blocklist.json.BlockListJson; + +public interface IBlockListRuleProvider { + /** + * Returns the provided blocklist rules. + * @return + */ + BlockListJson getBlocklistRules(); +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/svc/IBlockRuleEvaluationSvc.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/svc/IBlockRuleEvaluationSvc.java new file mode 100644 index 00000000000..836235859d1 --- /dev/null +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/blocklist/svc/IBlockRuleEvaluationSvc.java @@ -0,0 +1,16 @@ +package ca.uhn.fhir.mdm.blocklist.svc; + +import org.hl7.fhir.instance.model.api.IAnyResource; + +public interface IBlockRuleEvaluationSvc { + + /** + * Determines if the provided resource is blocked from + * mdm matching or not. + * @param theResource - the resource to assess + * @return - true: no mdm matching should be done + * (a golden resource should still be created) + * false: mdm matching should continue as normal + */ + boolean isMdmMatchingBlocked(IAnyResource theResource); +} diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java index d472cb2a4a0..7cc620083f2 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java @@ -518,7 +518,7 @@ public class ConsentInterceptorTest { when(myConsentSvc.canSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED); when(myConsentSvc.willSeeResource(any(RequestDetails.class), any(IBaseResource.class), any())).thenReturn(ConsentOutcome.PROCEED); - String nextPageLink; + String nextPageLink; HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?_count=1"); try (CloseableHttpResponse status = myClient.execute(httpGet)) { assertEquals(200, status.getStatusLine().getStatusCode()); @@ -533,6 +533,7 @@ public class ConsentInterceptorTest { when(myConsentSvc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED); when(myConsentSvc.willSeeResource(any(RequestDetails.class), any(IBaseResource.class), any())).thenAnswer(t->{ IBaseResource resource = (IBaseResource) t.getArguments()[1]; + ourLog.info(resource.getIdElement().getIdPart() + " == PTB"); if (resource.getIdElement().getIdPart().equals("PTB")) { Patient replacement = new Patient(); replacement.setId("PTB");