adding block list for mdm (#5059)

* using any/exact enum

* bluck rule evaluation svc

* adding blocklist

* adding documentation

* cleanup

* adding mdm blocking service and code cleanup

* review points

* another review point

* review points

---------

Co-authored-by: leif stawnyczy <leifstawnyczy@leifs-mbp.home>
This commit is contained in:
TipzCM 2023-07-13 09:59:02 -04:00 committed by GitHub
parent c88662205d
commit 906355dd65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1756 additions and 629 deletions

View File

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

View File

@ -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.
"

View File

@ -14,6 +14,103 @@ 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.
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

View File

@ -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.

View File

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

View File

@ -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<BlockedFieldJson> 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<IBase> 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;
}
}

View File

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

View File

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

View File

@ -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<BlockRuleTestCase> getTestCasesFhir() {
IParser parser = ourFhirContext.newJsonParser();
List<BlockRuleTestCase> 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));
}
}

View File

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

View File

@ -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,8 +60,11 @@ 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 {
public class MdmMatchLinkSvcTest {
@Nested
public class NoBlockLinkTest extends BaseMdmR4Test {
@Autowired
IMdmLinkSvc myMdmLinkSvc;
@Autowired
@ -676,5 +689,80 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
assertNotNull(l.getScore());
});
}
}
@Nested
@ContextConfiguration(classes = {
BlockListConfig.class
})
public class BlockLinkTest extends BaseMdmR4Test {
@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<IBaseResource> grs = getAllGoldenPatients();
assertEquals(2, grs.size());
assertEquals(0, myMdmLinkDaoSvc.getPossibleDuplicates().size());
List<MdmLink> links = new ArrayList<>();
for (IBaseResource gr : grs) {
links.addAll(getAllMdmLinks((Patient)gr));
}
assertEquals(2, links.size());
Set<Long> 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<MdmLink> getAllMdmLinks(Patient theGoldenPatient) {
return myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theGoldenPatient).stream()
.map( link -> (MdmLink) link)
.collect(Collectors.toList());
}
}
}

View File

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

View File

@ -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<BlockListRuleJson> myBlockListItemJsonList;
public List<BlockListRuleJson> getBlockListItemJsonList() {
if (myBlockListItemJsonList == null) {
myBlockListItemJsonList = new ArrayList<>();
}
return myBlockListItemJsonList;
}
public BlockListJson addBlockListRule(BlockListRuleJson theRule) {
getBlockListItemJsonList().add(theRule);
return this;
}
}

View File

@ -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<BlockedFieldJson> myBlockedFields;
public String getResourceType() {
return myResourceType;
}
public void setResourceType(String theResourceType) {
myResourceType = theResourceType;
}
public List<BlockedFieldJson> getBlockedFields() {
if (myBlockedFields == null) {
myBlockedFields = new ArrayList<>();
}
return myBlockedFields;
}
public BlockedFieldJson addBlockListField() {
BlockedFieldJson rule = new BlockedFieldJson();
getBlockedFields().add(rule);
return rule;
}
}

View File

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

View File

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

View File

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

View File

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