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:
parent
c88662205d
commit
906355dd65
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
"
|
|
@ -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}}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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");
|
||||
|
|
Loading…
Reference in New Issue