Support multiple EID systems for MDM. (#3178)

* Add implementation, changelog, docs, and tests

* Fix potential NPE

* Use constant instead of *
This commit is contained in:
Tadgh 2021-11-24 01:05:13 -05:00 committed by GitHub
parent 3c2b4bb397
commit 41ef7cf961
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 387 additions and 33 deletions

View File

@ -80,7 +80,6 @@ public enum VersionEnum {
V5_5_3,
V5_6_0,
V5_7_0
;
public static VersionEnum latestVersion() {

View File

@ -0,0 +1,5 @@
---
type: add
issue: 3158
title: "Add the concept of `messageKey` to the BaseResourceMessage class. This is to support brokers which permit key-based
partitioning."

View File

@ -0,0 +1,6 @@
---
type: change
issue: 3158
title: "Previously in configuring MDM, you were only allowed to set a single `eidSystem` which was global regardless of how many resource types you were performing
MDM on. This has been changed. That field is now deprecated, and a new field called `eidSystems` (a json object) should be used instead. This will allow you to have granular
control over which resource types use which EIDs. See the [documentation](/hapi-fhir/docs/server_jpa_mdm/mdm_rules.html) of `eidSystems` for more information."

View File

@ -99,7 +99,11 @@ Here is an example of a full HAPI MDM rules json document:
"lastname-jaro,phone,birthday": "POSSIBLE_MATCH",
"firstname-jaro,phone,birthday": "POSSIBLE_MATCH",
"org-name": "MATCH"
}
},
"eidSystems": {
"Organization": "https://hapifhir.org/identifier/naming/business-number",
"Practitioner": "https://hapifhir.org/identifier/naming/license-number"
}
}
```
@ -483,6 +487,13 @@ These entries convert combinations of successful matchFields into an MDM Match R
}
```
### eidSystem
### eidSystems
The external EID systems that the HAPI MDM system can expect to see on incoming resources. These are defined on a per-resource basis. Alternatively, you may use `*` to indicate
that an EID is valid for all managed resource types. The values must be valid URIs, and the keys must be valid resource types, or `*`.
See [MDM EID](/hapi-fhir/docs/server_jpa_mdm/mdm_eid.html) for details on how EIDs are managed by HAPI MDM.
<p class="helpInfoCalloutBox">
Note that this field used to be called `eidSystem`. While that field is deprecated, it will continue to work. In the background, it effectively sets the eid for resource type `*`.
</p>
The external EID system that the HAPI MDM system should expect to see on incoming Patient resources. Must be a valid URI. See [MDM EID](/hapi-fhir/docs/server_jpa_mdm/mdm_eid.html) for details on how EIDs are managed by HAPI MDM.

View File

@ -0,0 +1,27 @@
package ca.uhn.fhir.jpa.mdm.broker;
import ca.uhn.fhir.jpa.subscription.api.ISubscriptionMessageKeySvc;
import ca.uhn.fhir.mdm.model.CanonicalEID;
import ca.uhn.fhir.mdm.util.EIDHelper;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Nullable;
import java.util.List;
@Service
public class MdmMessageKeySvc implements ISubscriptionMessageKeySvc {
@Autowired
private EIDHelper myEIDHelper;
@Nullable
@Override
public String getMessageKeyOrNull(IBaseResource theTargetResource) {
List<CanonicalEID> eidList = myEIDHelper.getExternalEid(theTargetResource);
if (eidList.isEmpty()) {
return null;
}
return eidList.get(0).getValue();
}
}

View File

@ -26,6 +26,7 @@ import ca.uhn.fhir.jpa.batch.mdm.MdmBatchJobSubmitterFactoryImpl;
import ca.uhn.fhir.jpa.dao.mdm.MdmLinkDeleteSvc;
import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptor;
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.dao.MdmLinkFactory;
@ -104,6 +105,10 @@ public class MdmConsumerConfig {
return new MdmMessageHandler();
}
@Bean
MdmMessageKeySvc mdmMessageKeySvc() {
return new MdmMessageKeySvc();
}
@Bean
MdmMatchLinkSvc mdmMatchLinkSvc() {
return new MdmMatchLinkSvc();

View File

@ -77,7 +77,7 @@ public class MdmResourceDaoSvc {
//TODO GGG MDM address this
public Optional<IAnyResource> searchGoldenResourceByEID(String theEid, String theResourceType) {
SearchParameterMap map = buildEidSearchParameterMap(theEid);
SearchParameterMap map = buildEidSearchParameterMap(theEid, theResourceType);
IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theResourceType);
IBundleProvider search = resourceDao.search(map);
@ -101,10 +101,10 @@ public class MdmResourceDaoSvc {
}
@Nonnull
private SearchParameterMap buildEidSearchParameterMap(String theTheEid) {
private SearchParameterMap buildEidSearchParameterMap(String theEid, String theResourceType) {
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add("identifier", new TokenParam(myMdmSettings.getMdmRules().getEnterpriseEIDSystem(), theTheEid));
map.add("identifier", new TokenParam(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType(theResourceType), theEid));
map.add("_tag", new TokenParam(MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS, MdmConstants.CODE_GOLDEN_RECORD));
return map;
}

View File

@ -327,12 +327,12 @@ abstract public class BaseMdmR4Test extends BaseJpaR4Test {
}
protected Patient addExternalEID(Patient thePatient, String theEID) {
thePatient.addIdentifier().setSystem(myMdmSettings.getMdmRules().getEnterpriseEIDSystem()).setValue(theEID);
thePatient.addIdentifier().setSystem(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient")).setValue(theEID);
return thePatient;
}
protected Patient clearExternalEIDs(Patient thePatient) {
thePatient.getIdentifier().removeIf(theIdentifier -> theIdentifier.getSystem().equalsIgnoreCase(myMdmSettings.getMdmRules().getEnterpriseEIDSystem()));
thePatient.getIdentifier().removeIf(theIdentifier -> theIdentifier.getSystem().equalsIgnoreCase(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient")));
return thePatient;
}

View File

@ -71,11 +71,11 @@ public class MdmMatchLinkSvcMultipleEidModeTest extends BaseMdmR4Test {
//The collision should have added a new identifier with the external system.
Identifier secondIdentifier = identifier.get(1);
assertThat(secondIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystem())));
assertThat(secondIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient"))));
assertThat(secondIdentifier.getValue(), is(equalTo("12345")));
Identifier thirdIdentifier = identifier.get(2);
assertThat(thirdIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystem())));
assertThat(thirdIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient"))));
assertThat(thirdIdentifier.getValue(), is(equalTo("67890")));
}

View File

@ -157,7 +157,7 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
Patient patient = getTargetResourceFromMdmLink(mdmLink.get(), "Patient");
List<CanonicalEID> externalEid = myEidHelper.getExternalEid(patient);
assertThat(externalEid.get(0).getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystem())));
assertThat(externalEid.get(0).getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient"))));
assertThat(externalEid.get(0).getValue(), is(equalTo(sampleEID)));
}
@ -222,7 +222,7 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test {
//The collision should have added a new identifier with the external system.
Identifier secondIdentifier = identifier.get(1);
assertThat(secondIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystem())));
assertThat(secondIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType("Patient"))));
assertThat(secondIdentifier.getValue(), is(equalTo("12345")));
}

View File

@ -0,0 +1,20 @@
package ca.uhn.fhir.jpa.subscription.api;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nullable;
/**
* This is used by "message" type subscriptions to provide a key to the message wrapper before submitting it to the channel
*/
public interface ISubscriptionMessageKeySvc {
/**
* Given an {@link IBaseResource}, return a key that can be used to identify the message. This key will be used to
* partition the message into a queue.
*
* @param thePayloadResource the payload resource.
* @return the key or null.
*/
@Nullable
String getMessageKeyOrNull(IBaseResource thePayloadResource);
}

View File

@ -64,6 +64,7 @@ public class SubscriptionDeliveringMessageSubscriber extends BaseSubscriptionDel
protected void doDelivery(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription, IChannelProducer theChannelProducer, IBaseResource thePayloadResource) {
ResourceModifiedMessage payload = new ResourceModifiedMessage(myFhirContext, thePayloadResource, theMsg.getOperationType());
payload.setMessageKey(theMsg.getMessageKeyOrNull());
payload.setTransactionId(theMsg.getTransactionId());
ResourceModifiedJsonMessage message = new ResourceModifiedJsonMessage(payload);
theChannelProducer.send(message);

View File

@ -16,10 +16,13 @@ import java.util.HashMap;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class CanonicalSubscriptionTest {
private static final String TAG_SYSTEM = "https://hapifhir.org/NamingSystem/managing-mdm-system";
private static final String TAG_VALUE = "HAPI-MDM";
@Test
public void testGetChannelExtension() throws IOException {
@ -54,6 +57,14 @@ public class CanonicalSubscriptionTest {
assertThat(s.getChannelExtensions("key3"), Matchers.empty());
}
@Test
public void testCanonicalSubscriptionRetainsMetaTags() throws IOException {
SubscriptionCanonicalizer canonicalizer = new SubscriptionCanonicalizer(FhirContext.forR4());
CanonicalSubscription sub1 = canonicalizer.canonicalize(makeMdmSubscription());
assertTrue(sub1.getTags().keySet().contains(TAG_SYSTEM));
assertEquals(sub1.getTags().get(TAG_SYSTEM), TAG_VALUE);
}
@Test
public void emailDetailsEquals() {
SubscriptionCanonicalizer canonicalizer = new SubscriptionCanonicalizer(FhirContext.forR4());
@ -69,6 +80,14 @@ public class CanonicalSubscriptionTest {
retVal.setChannel(channel);
return retVal;
}
private Subscription makeMdmSubscription() {
Subscription retVal = new Subscription();
Subscription.SubscriptionChannelComponent channel = new Subscription.SubscriptionChannelComponent();
channel.setType(Subscription.SubscriptionChannelType.MESSAGE);
retVal.setChannel(channel);
retVal.getMeta().addTag("https://hapifhir.org/NamingSystem/managing-mdm-system", "HAPI-MDM", "managed by hapi mdm");
return retVal;
}
private CanonicalSubscription serializeAndDeserialize(CanonicalSubscription theSubscription) throws IOException {

View File

@ -35,17 +35,21 @@ import ca.uhn.fhir.mdm.rules.json.MdmSimilarityJson;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.util.FhirTerser;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.xml.crypto.Data;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class MdmRuleValidator implements IMdmRuleValidator {
@ -73,7 +77,40 @@ public class MdmRuleValidator implements IMdmRuleValidator {
validateMdmTypes(theMdmRules);
validateSearchParams(theMdmRules);
validateMatchFields(theMdmRules);
validateSystemIsUri(theMdmRules);
validateSystemsAreUris(theMdmRules);
validateEidSystemsMatchMdmTypes(theMdmRules);
}
private void validateEidSystemsMatchMdmTypes(MdmRulesJson theMdmRules) {
theMdmRules.getEnterpriseEIDSystems().keySet()
.forEach(key -> {
//Ensure each key is either * or a valid resource type.
if (!key.equalsIgnoreCase("*") && !theMdmRules.getMdmTypes().contains(key)) {
throw new ConfigurationException(String.format("There is an eidSystem set for [%s] but that is not one of the mdmTypes. Valid options are [%s].", key, buildValidEidKeysMessage(theMdmRules)));
}
});
}
private String buildValidEidKeysMessage(MdmRulesJson theMdmRulesJson) {
List<String> validTypes = new ArrayList<>(theMdmRulesJson.getMdmTypes());
validTypes.add("*");
return String.join(", ", validTypes);
}
private void validateSystemsAreUris(MdmRulesJson theMdmRules) {
theMdmRules.getEnterpriseEIDSystems().entrySet()
.forEach(entry -> {
String resourceType = entry.getKey();
String uri = entry.getValue();
if (!resourceType.equals("*")) {
try {
myFhirContext.getResourceType(resourceType);
}catch (DataFormatException e) {
throw new ConfigurationException(String.format("%s is not a valid resource type, but is set in the eidSystems field.", resourceType));
}
}
validateIsUri(uri);
});
}
public void validateMdmTypes(MdmRulesJson theMdmRulesJson) {
@ -212,15 +249,10 @@ public class MdmRuleValidator implements IMdmRuleValidator {
validateFieldPathForType(theFieldMatch.getResourceType(), theFieldMatch);
}
private void validateSystemIsUri(MdmRulesJson theMdmRulesJson) {
if (theMdmRulesJson.getEnterpriseEIDSystem() == null) {
return;
}
ourLog.info("Validating system URI {}", theMdmRulesJson.getEnterpriseEIDSystem());
private void validateIsUri(String theUri) {
ourLog.info("Validating system URI {}", theUri);
try {
new URI(theMdmRulesJson.getEnterpriseEIDSystem());
new URI(theUri);
} catch (URISyntaxException e) {
throw new ConfigurationException("Enterprise Identifier System (eidSystem) must be a valid URI");
}

View File

@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.util.StdConverter;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import java.util.ArrayList;
@ -34,6 +35,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static ca.uhn.fhir.mdm.api.MdmConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE;
@JsonDeserialize(converter = MdmRulesJson.MdmRulesJsonConverter.class)
public class MdmRulesJson implements IModelJson {
@ -47,10 +50,16 @@ public class MdmRulesJson implements IModelJson {
List<MdmFieldMatchJson> myMatchFieldJsonList = new ArrayList<>();
@JsonProperty(value = "matchResultMap", required = true)
Map<String, MdmMatchResultEnum> myMatchResultMap = new HashMap<>();
/**
* This field is deprecated, use eidSystems instead.
*/
@Deprecated
@JsonProperty(value = "eidSystem")
String myEnterpriseEIDSystem;
@JsonProperty(value = "eidSystems")
Map<String, String> myEnterpriseEidSystems = new HashMap<>();
@JsonProperty(value = "mdmTypes")
List<String> myMdmTypes;
@ -112,14 +121,57 @@ public class MdmRulesJson implements IModelJson {
return Collections.unmodifiableList(myCandidateFilterSearchParams);
}
/**
* Use {@link this#getEnterpriseEIDSystemForResourceType(String)} instead.
*/
@Deprecated
public String getEnterpriseEIDSystem() {
return myEnterpriseEIDSystem;
}
/**
* Use {@link this#setEnterpriseEIDSystems(Map)} (String)} or {@link this#addEnterpriseEIDSystem(String, String)} instead.
*/
@Deprecated
public void setEnterpriseEIDSystem(String theEnterpriseEIDSystem) {
myEnterpriseEIDSystem = theEnterpriseEIDSystem;
}
public void setEnterpriseEIDSystems(Map<String, String> theEnterpriseEIDSystems) {
myEnterpriseEidSystems = theEnterpriseEIDSystems;
}
public void addEnterpriseEIDSystem(String theResourceType, String theEidSystem) {
if (myEnterpriseEidSystems == null) {
myEnterpriseEidSystems = new HashMap<>();
}
myEnterpriseEidSystems.put(theResourceType, theEidSystem);
}
public Map<String, String> getEnterpriseEIDSystems() {
//First try the new property.
if (myEnterpriseEidSystems != null && !myEnterpriseEidSystems.isEmpty()) {
return myEnterpriseEidSystems;
//If that fails, fall back to our deprecated property.
} else if (!StringUtils.isBlank(myEnterpriseEIDSystem)) {
HashMap<String , String> retVal = new HashMap<>();
retVal.put(ALL_RESOURCE_SEARCH_PARAM_TYPE, myEnterpriseEIDSystem);
return retVal;
//Otherwise, return an empty map.
} else {
return Collections.emptyMap();
}
}
public String getEnterpriseEIDSystemForResourceType(String theResourceType) {
Map<String, String> enterpriseEIDSystems = getEnterpriseEIDSystems();
if (enterpriseEIDSystems.containsKey(ALL_RESOURCE_SEARCH_PARAM_TYPE)) {
return enterpriseEIDSystems.get(ALL_RESOURCE_SEARCH_PARAM_TYPE);
} else {
return enterpriseEIDSystems.get(theResourceType);
}
}
public String getVersion() {
return myVersion;
}
@ -131,6 +183,14 @@ public class MdmRulesJson implements IModelJson {
private void validate() {
Validate.notBlank(myVersion, "version may not be blank");
Map<String, String> enterpriseEIDSystems = getEnterpriseEIDSystems();
//If we have a * eid system, there should only be one.
if (enterpriseEIDSystems.containsKey(ALL_RESOURCE_SEARCH_PARAM_TYPE)) {
Validate.isTrue(enterpriseEIDSystems.size() == 1);
}
}
public String getSummary() {

View File

@ -63,7 +63,8 @@ public final class EIDHelper {
* @return An optional {@link CanonicalEID} representing the external EID. Absent if the EID is not present.
*/
public List<CanonicalEID> getExternalEid(IBaseResource theResource) {
return CanonicalEID.extractFromResource(myFhirContext, myMdmSettings.getMdmRules().getEnterpriseEIDSystem(), theResource);
String resourceType = myFhirContext.getResourceType(theResource);
return CanonicalEID.extractFromResource(myFhirContext, myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType(resourceType), theResource);
}
/**

View File

@ -120,7 +120,7 @@ public class GoldenResourceHelper {
}
private void cloneAllExternalEidsIntoNewGoldenResource(BaseRuntimeChildDefinition theGoldenResourceIdentifier,
IBase theGoldenResource, IBase theNewGoldenResource) {
IAnyResource theGoldenResource, IBase theNewGoldenResource) {
// FHIR choice types - fields within fhir where we have a choice of ids
IFhirPath fhirPath = myFhirContext.newFhirPath();
List<IBase> goldenResourceIdentifiers = theGoldenResourceIdentifier.getAccessor().getValues(theGoldenResource);
@ -128,7 +128,8 @@ public class GoldenResourceHelper {
for (IBase base : goldenResourceIdentifiers) {
Optional<IPrimitiveType> system = fhirPath.evaluateFirst(base, "system", IPrimitiveType.class);
if (system.isPresent()) {
String mdmSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystem();
String resourceType = myFhirContext.getResourceType(theGoldenResource);
String mdmSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType(resourceType);
String baseSystem = system.get().getValueAsString();
if (Objects.equals(baseSystem, mdmSystem)) {
ca.uhn.fhir.util.TerserUtil.cloneEidIntoResource(myFhirContext, theGoldenResourceIdentifier, base, theNewGoldenResource);
@ -188,7 +189,7 @@ public class GoldenResourceHelper {
return theGoldenResource;
}
private void clearExternalEidsFromTheGoldenResource(BaseRuntimeChildDefinition theGoldenResourceIdentifier, IBase theGoldenResource) {
private void clearExternalEidsFromTheGoldenResource(BaseRuntimeChildDefinition theGoldenResourceIdentifier, IBaseResource theGoldenResource) {
IFhirPath fhirPath = myFhirContext.newFhirPath();
List<IBase> goldenResourceIdentifiers = theGoldenResourceIdentifier.getAccessor().getValues(theGoldenResource);
List<IBase> clonedIdentifiers = new ArrayList<>();
@ -197,7 +198,8 @@ public class GoldenResourceHelper {
for (IBase base : goldenResourceIdentifiers) {
Optional<IPrimitiveType> system = fhirPath.evaluateFirst(base, "system", IPrimitiveType.class);
if (system.isPresent()) {
String mdmSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystem();
String resourceType = myFhirContext.getResourceType(theGoldenResource);
String mdmSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystemForResourceType(resourceType);
String baseSystem = system.get().getValueAsString();
if (Objects.equals(baseSystem, mdmSystem)) {
ourLog.debug("Found EID confirming to MDM rules {}. It should not be copied, skipping", baseSystem);

View File

@ -12,6 +12,7 @@ import org.springframework.core.io.Resource;
import java.io.IOException;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.startsWith;
@ -161,6 +162,25 @@ public class MdmRuleValidatorTest extends BaseR4Test {
}
}
@Test
public void testBadEidResourceType() throws IOException {
try {
setMdmRuleJson("bad-rules-illegal-resource-type-eid.json");
}
catch (ConfigurationException e){
assertThat(e.getMessage(), is(equalTo("not-a-resource is not a valid resource type, but is set in the eidSystems field.")));
}
}
@Test
public void testBadEidDoesntMatchKnownMdmTypes() throws IOException {
try {
setMdmRuleJson("bad-rules-illegal-missing-resource-type.json");
}
catch (ConfigurationException e){
assertThat(e.getMessage(), is(equalTo("There is an eidSystem set for [Patient] but that is not one of the mdmTypes. Valid options are [Organization, *].")));
}
}
private void setMdmRuleJson(String theTheS) throws IOException {
MdmRuleValidator mdmRuleValidator = new MdmRuleValidator(ourFhirContext, mySearchParamRetriever);
MdmSettings mdmSettings = new MdmSettings(mdmRuleValidator);

View File

@ -11,9 +11,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
@ -72,4 +76,32 @@ public class MdmRulesJsonR4Test extends BaseMdmRulesR4Test {
assertEquals("There is no matchField with name bad", e.getMessage());
}
}
@Test
public void testInvalidResourceTypeDoesntDeserialize() throws IOException {
myRules = buildOldStyleEidRules();
String eidSystem = myRules.getEnterpriseEIDSystemForResourceType("Patient");
assertThat(eidSystem, is(equalTo(PATIENT_EID_FOR_TEST)));
eidSystem = myRules.getEnterpriseEIDSystemForResourceType("Practitioner");
assertThat(eidSystem, is(equalTo(PATIENT_EID_FOR_TEST)));
eidSystem = myRules.getEnterpriseEIDSystemForResourceType("Medication");
assertThat(eidSystem, is(equalTo(PATIENT_EID_FOR_TEST)));
}
@Override
protected MdmRulesJson buildActiveBirthdateIdRules() {
return super.buildActiveBirthdateIdRules();
}
private MdmRulesJson buildOldStyleEidRules() {
MdmRulesJson mdmRulesJson = super.buildActiveBirthdateIdRules();
mdmRulesJson.setEnterpriseEIDSystems(Collections.emptyMap());
//This sets the new-style eid resource type to `*`
mdmRulesJson.setEnterpriseEIDSystem(PATIENT_EID_FOR_TEST);
return mdmRulesJson;
}
}

View File

@ -18,6 +18,9 @@ public abstract class BaseMdmRulesR4Test extends BaseR4Test {
public static final String PATIENT_GIVEN = "patient-given";
public static final String PATIENT_GIVEN_FIRST = "patient-given-first";
public static final String PATIENT_FAMILY = "patient-last";
public static final String PATIENT_EID_FOR_TEST = "http://hello.com/naming/patient-eid";
public static final String MEDICATION_EID_FOR_TEST= "http://hello.com/naming/medication-eid";
public static final String PRACTITIONER_EID_FOR_TEST = "http://hello.com/naming/practitioner-eid";
public static final double NAME_THRESHOLD = 0.8;
protected MdmFieldMatchJson myGivenNameMatchField;
@ -71,6 +74,10 @@ public abstract class BaseMdmRulesR4Test extends BaseR4Test {
retval.setMdmTypes(Arrays.asList("Patient", "Practitioner", "Medication"));
retval.putMatchResult(myBothNameFields, MdmMatchResultEnum.MATCH);
retval.putMatchResult(PATIENT_GIVEN, MdmMatchResultEnum.POSSIBLE_MATCH);
retval.addEnterpriseEIDSystem("Patient", PATIENT_EID_FOR_TEST);
retval.addEnterpriseEIDSystem("Medication", MEDICATION_EID_FOR_TEST);
retval.addEnterpriseEIDSystem("Practitioner", PRACTITIONER_EID_FOR_TEST);
return retval;
}
}

View File

@ -33,7 +33,7 @@ public class EIDHelperR4Test extends BaseR4Test {
private static final MdmRulesJson ourRules = new MdmRulesJson() {
{
setEnterpriseEIDSystem(EXTERNAL_ID_SYSTEM_FOR_TEST);
addEnterpriseEIDSystem("Patient", EXTERNAL_ID_SYSTEM_FOR_TEST);
setMdmTypes(Arrays.asList(new String[] {"Patient"}));
}
};

View File

@ -0,0 +1,20 @@
{
"version": "1",
"mdmTypes": ["Organization"],
"candidateSearchParams": [],
"candidateFilterSearchParams": [],
"matchFields": [
{
"name": "name-prefix",
"resourceType": "Organization",
"resourcePath": "name",
"matcher": {
"algorithm": "STRING"
}
}
],
"matchResultMap": {},
"eidSystems": {
"Patient": "http://hello.com/uri"
}
}

View File

@ -0,0 +1,20 @@
{
"version": "1",
"mdmTypes": ["Organization"],
"candidateSearchParams": [],
"candidateFilterSearchParams": [],
"matchFields": [
{
"name": "name-prefix",
"resourceType": "Organization",
"resourcePath": "name",
"matcher": {
"algorithm": "STRING"
}
}
],
"matchResultMap": {},
"eidSystems": {
"not-a-resource": "http://hello.com/uri"
}
}

View File

@ -46,6 +46,12 @@ public abstract class BaseResourceMessage implements IResourceMessage, IModelJso
@JsonProperty("mediaType")
private String myMediaType;
/**
* This is used by any message going to kafka for topic partition selection purposes.
*/
@JsonProperty("messageKey")
private String myMessageKey;
/**
* Returns an attribute stored in this message.
* <p>
@ -156,6 +162,15 @@ public abstract class BaseResourceMessage implements IResourceMessage, IModelJso
myMediaType = theMediaType;
}
@Nullable
public String getMessageKeyOrNull() {
return myMessageKey;
}
public void setMessageKey(String theMessageKey) {
myMessageKey = theMessageKey;
}
public enum OperationTypeEnum {
CREATE,
UPDATE,

View File

@ -26,7 +26,6 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
public class ResourceOperationMessage extends BaseResourceModifiedMessage {
public ResourceOperationMessage() {
}

View File

@ -26,6 +26,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import javax.annotation.Nullable;
public abstract class BaseJsonMessage<T> implements Message<T>, IModelJson {
private static final long serialVersionUID = 1L;
@ -58,4 +60,9 @@ public abstract class BaseJsonMessage<T> implements Message<T>, IModelJson {
public void setHeaders(HapiMessageHeaders theHeaders) {
myHeaders = theHeaders;
}
@Nullable
public String getMessageKeyOrNull() {
return null;
}
}

View File

@ -64,7 +64,7 @@
<artifactId>hapi-fhir-structures-hl7org-dstu2</artifactId>
<version>${project.version}</version>
</dependency>
<!-- FIXME KHS remove jpa stuff from here -->
<!-- TODO KHS remove jpa stuff from here -->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-model</artifactId>

View File

@ -25,11 +25,14 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType;
import ca.uhn.fhir.model.dstu2.composite.CodingDt;
import ca.uhn.fhir.model.dstu2.resource.Subscription;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.util.HapiExtensions;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBaseCoding;
import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
import org.hl7.fhir.instance.model.api.IBaseMetaType;
import org.hl7.fhir.instance.model.api.IBaseReference;
@ -79,7 +82,6 @@ public class SubscriptionCanonicalizer {
private CanonicalSubscription canonicalizeDstu2(IBaseResource theSubscription) {
ca.uhn.fhir.model.dstu2.resource.Subscription subscription = (ca.uhn.fhir.model.dstu2.resource.Subscription) theSubscription;
CanonicalSubscription retVal = new CanonicalSubscription();
try {
retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(subscription.getStatus()));
@ -90,12 +92,27 @@ public class SubscriptionCanonicalizer {
retVal.setChannelExtensions(extractExtension(subscription));
retVal.setIdElement(subscription.getIdElement());
retVal.setPayloadString(subscription.getChannel().getPayload());
retVal.setTags(extractTags(subscription));
} catch (FHIRException theE) {
throw new InternalErrorException(theE);
}
return retVal;
}
/**
* Extract the meta tags from the subscription and convert them to a simple string map.
* @param theSubscription The subscription to extract the tags from
* @return A map of tags System:Code
*/
private Map<String, String> extractTags(IBaseResource theSubscription) {
return theSubscription.getMeta().getTag()
.stream()
.collect(Collectors.toMap(
IBaseCoding::getSystem,
IBaseCoding::getCode
));
}
private CanonicalSubscription canonicalizeDstu3(IBaseResource theSubscription) {
org.hl7.fhir.dstu3.model.Subscription subscription = (org.hl7.fhir.dstu3.model.Subscription) theSubscription;
@ -113,6 +130,7 @@ public class SubscriptionCanonicalizer {
retVal.setIdElement(subscription.getIdElement());
retVal.setPayloadString(subscription.getChannel().getPayload());
retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
retVal.setTags(extractTags(subscription));
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
String from;
@ -201,7 +219,6 @@ public class SubscriptionCanonicalizer {
private CanonicalSubscription canonicalizeR4(IBaseResource theSubscription) {
org.hl7.fhir.r4.model.Subscription subscription = (org.hl7.fhir.r4.model.Subscription) theSubscription;
CanonicalSubscription retVal = new CanonicalSubscription();
retVal.setStatus(subscription.getStatus());
retVal.setChannelType(getChannelType(theSubscription));
@ -212,6 +229,7 @@ public class SubscriptionCanonicalizer {
retVal.setIdElement(subscription.getIdElement());
retVal.setPayloadString(subscription.getChannel().getPayload());
retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
retVal.setTags(extractTags(subscription));
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
String from;
@ -266,6 +284,7 @@ public class SubscriptionCanonicalizer {
retVal.setIdElement(subscription.getIdElement());
retVal.setPayloadString(subscription.getContentType());
retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA));
retVal.setTags(extractTags(subscription));
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
String from;

View File

@ -68,6 +68,8 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
private RestHookDetails myRestHookDetails;
@JsonProperty("extensions")
private Map<String, List<String>> myChannelExtensions;
@JsonProperty("tags")
private Map<String, String> myTags;
@JsonProperty("payloadSearchCriteria")
private String myPayloadSearchCriteria;
@ -139,6 +141,15 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
}
}
public Map<String, String> getTags() {
return myTags;
}
public void setTags(Map<String, String> theTags) {
this.myTags = theTags;
}
public void setHeaders(String theHeaders) {
myHeaders = new ArrayList<>();
if (isNotBlank(theHeaders)) {

View File

@ -24,6 +24,8 @@ import ca.uhn.fhir.rest.server.messaging.json.BaseJsonMessage;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.builder.ToStringBuilder;
import javax.annotation.Nullable;
public class ResourceModifiedJsonMessage extends BaseJsonMessage<ResourceModifiedMessage> {
@JsonProperty("payload")
@ -52,6 +54,15 @@ public class ResourceModifiedJsonMessage extends BaseJsonMessage<ResourceModifie
myPayload = thePayload;
}
@Override
@Nullable
public String getMessageKeyOrNull() {
if (myPayload == null) {
return null;
}
return myPayload.getMessageKeyOrNull();
}
@Override
public String toString() {
return new ToStringBuilder(this)

View File

@ -27,6 +27,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nullable;
/**
* Most of this class has been moved to ResourceModifiedMessage in the hapi-fhir-server project, for a reusable channel ResourceModifiedMessage
* that doesn't require knowledge of subscriptions.
@ -40,6 +42,8 @@ public class ResourceModifiedMessage extends BaseResourceModifiedMessage {
@JsonProperty(value = "subscriptionId", required = false)
private String mySubscriptionId;
/**
* Constructor
*/
@ -64,6 +68,7 @@ public class ResourceModifiedMessage extends BaseResourceModifiedMessage {
mySubscriptionId = theSubscriptionId;
}
@Override
public String toString() {
return new ToStringBuilder(this)