Un-revert the MDM changes, now that issues have been addressed (#2226)

* Backward compatibility changes

* Revert "Revert "Remove all references to Person from EMPI.""

* Updated docs

* Removed mdm channel references

* Initial implementation

* fix coarseness bug. Fix tests. reinitialize silly bean

* Add forgotten json file

* Generic provider PoC

* Refactored provider to use BundleBuilder

* Removed version-specific MDM providers

* Addressed code review comments

* Fixed after merge

* Fixed docs

* MDM SVG for Update Use Cases

* Removed obsolete docs

Co-authored-by: Nick <nick.goupinets@smilecdr.com>
Co-authored-by: Nick Goupinets <73255752+nvg-smile@users.noreply.github.com>
This commit is contained in:
Tadgh 2020-12-17 10:12:53 -05:00 committed by GitHub
parent effbd19924
commit e79114e2ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
281 changed files with 10802 additions and 10208 deletions

View File

@ -1703,21 +1703,21 @@ public enum Pointcut {
),
/**
* <b>EMPI Hook:</b>
* Invoked whenever a persisted Patient/Practitioner resource (a resource that has just been stored in the
* database via a create/update/patch/etc.) has been matched against related resources and EMPI links have been updated.
* <b>MDM(EMPI) Hook:</b>
* Invoked whenever a persisted resource (a resource that has just been stored in the
* database via a create/update/patch/etc.) has been matched against related resources and MDM links have been updated.
* <p>
* Hooks may accept the following parameters:
* <ul>
* <li>ca.uhn.fhir.rest.server.messaging.ResourceOperationMessage - This parameter should not be modified as processing is complete when this hook is invoked.</li>
* <li>ca.uhn.fhir.rest.server.TransactionLogMessages - This parameter is for informational messages provided by the EMPI module during EMPI procesing. .</li>
* <li>ca.uhn.fhir.rest.server.TransactionLogMessages - This parameter is for informational messages provided by the MDM module during MDM processing.</li>
* </ul>
* </p>
* <p>
* Hooks should return <code>void</code>.
* </p>
*/
EMPI_AFTER_PERSISTED_RESOURCE_CHECKED(void.class, "ca.uhn.fhir.rest.server.messaging.ResourceOperationMessage", "ca.uhn.fhir.rest.server.TransactionLogMessages"),
MDM_AFTER_PERSISTED_RESOURCE_CHECKED(void.class, "ca.uhn.fhir.rest.server.messaging.ResourceOperationMessage", "ca.uhn.fhir.rest.server.TransactionLogMessages"),
/**
* <b>Performance Tracing Hook:</b>

View File

@ -26,25 +26,34 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBackboneElement;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
/**
* This class can be used to build a Bundle resource to be used as a FHIR transaction.
* This class can be used to build a Bundle resource to be used as a FHIR transaction. Convenience methods provide
* support for setting various bundle fields and working with bundle parts such as metadata and entry
* (method and search).
*
* <p>
*
* This is not yet complete, and doesn't support all FHIR features. <b>USE WITH CAUTION</b> as the API
* may change.
*
* @since 5.1.0
*/
public class TransactionBuilder {
public class BundleBuilder {
private final FhirContext myContext;
private final IBaseBundle myBundle;
private final RuntimeResourceDefinition myBundleDef;
private final BaseRuntimeChildDefinition myEntryChild;
private final BaseRuntimeChildDefinition myMetaChild;
private final BaseRuntimeChildDefinition mySearchChild;
private final BaseRuntimeElementDefinition<?> myEntryDef;
private final BaseRuntimeElementDefinition<?> myMetaDef;
private final BaseRuntimeElementDefinition mySearchDef;
private final BaseRuntimeChildDefinition myEntryResourceChild;
private final BaseRuntimeChildDefinition myEntryFullUrlChild;
private final BaseRuntimeChildDefinition myEntryRequestChild;
@ -57,20 +66,23 @@ public class TransactionBuilder {
/**
* Constructor
*/
public TransactionBuilder(FhirContext theContext) {
public BundleBuilder(FhirContext theContext) {
myContext = theContext;
myBundleDef = myContext.getResourceDefinition("Bundle");
myBundle = (IBaseBundle) myBundleDef.newInstance();
BaseRuntimeChildDefinition typeChild = myBundleDef.getChildByName("type");
IPrimitiveType<?> type = (IPrimitiveType<?>) typeChild.getChildByName("type").newInstance(typeChild.getInstanceConstructorArguments());
type.setValueAsString("transaction");
typeChild.getMutator().setValue(myBundle, type);
setBundleField("type", "transaction");
myEntryChild = myBundleDef.getChildByName("entry");
myEntryDef = myEntryChild.getChildByName("entry");
mySearchChild = myEntryDef.getChildByName("search");
mySearchDef = mySearchChild.getChildByName("search");
myMetaChild = myBundleDef.getChildByName("meta");
myMetaDef = myMetaChild.getChildByName("meta");
myEntryResourceChild = myEntryDef.getChildByName("resource");
myEntryFullUrlChild = myEntryDef.getChildByName("fullUrl");
@ -83,7 +95,52 @@ public class TransactionBuilder {
myEntryRequestMethodDef = myEntryRequestMethodChild.getChildByName("method");
myEntryRequestIfNoneExistChild = myEntryRequestDef.getChildByName("ifNoneExist");
}
/**
* Sets the specified primitive field on the bundle with the value provided.
*
* @param theFieldName
* Name of the primitive field.
* @param theFieldValue
* Value of the field to be set.
*/
public BundleBuilder setBundleField(String theFieldName, String theFieldValue) {
BaseRuntimeChildDefinition typeChild = myBundleDef.getChildByName(theFieldName);
Validate.notNull(typeChild, "Unable to find field %s", theFieldName);
IPrimitiveType<?> type = (IPrimitiveType<?>) typeChild.getChildByName(theFieldName).newInstance(typeChild.getInstanceConstructorArguments());
type.setValueAsString(theFieldValue);
typeChild.getMutator().setValue(myBundle, type);
return this;
}
/**
* Sets the specified primitive field on the search entry with the value provided.
*
* @param theSearch
* Search part of the entry
* @param theFieldName
* Name of the primitive field.
* @param theFieldValue
* Value of the field to be set.
*/
public BundleBuilder setSearchField(IBase theSearch, String theFieldName, String theFieldValue) {
BaseRuntimeChildDefinition typeChild = mySearchDef.getChildByName(theFieldName);
Validate.notNull(typeChild, "Unable to find field %s", theFieldName);
IPrimitiveType<?> type = (IPrimitiveType<?>) typeChild.getChildByName(theFieldName).newInstance(typeChild.getInstanceConstructorArguments());
type.setValueAsString(theFieldValue);
typeChild.getMutator().setValue(theSearch, type);
return this;
}
public BundleBuilder setSearchField(IBase theSearch, String theFieldName, IPrimitiveType<?> theFieldValue) {
BaseRuntimeChildDefinition typeChild = mySearchDef.getChildByName(theFieldName);
Validate.notNull(typeChild, "Unable to find field %s", theFieldName);
typeChild.getMutator().setValue(theSearch, theFieldValue);
return this;
}
/**
@ -130,11 +187,35 @@ public class TransactionBuilder {
return new CreateBuilder(request);
}
/**
* Creates new entry and adds it to the bundle
*
* @return
* Returns the new entry.
*/
public IBase addEntry() {
IBase entry = myEntryDef.newInstance();
myEntryChild.getMutator().addValue(myBundle, entry);
return entry;
}
/**
* Creates new search instance for the specified entry
*
* @param entry Entry to create search instance for
* @return
* Returns the search instance
*/
public IBaseBackboneElement addSearch(IBase entry) {
IBase searchInstance = mySearchDef.newInstance();
mySearchChild.getMutator().setValue(entry, searchInstance);
return (IBaseBackboneElement) searchInstance;
}
public IBase addEntryAndReturnRequest(IBaseResource theResource) {
Validate.notNull(theResource, "theResource must not be null");
IBase entry = myEntryDef.newInstance();
myEntryChild.getMutator().addValue(myBundle, entry);
IBase entry = addEntry();
// Bundle.entry.fullUrl
IPrimitiveType<?> fullUrl = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance();
@ -155,6 +236,80 @@ public class TransactionBuilder {
return myBundle;
}
public BundleBuilder setMetaField(String theFieldName, IBase theFieldValue) {
BaseRuntimeChildDefinition.IMutator mutator = myMetaDef.getChildByName(theFieldName).getMutator();
mutator.setValue(myBundle.getMeta(), theFieldValue);
return this;
}
/**
* Sets the specified entry field.
*
* @param theEntry
* The entry instance to set values on
* @param theEntryChildName
* The child field name of the entry instance to be set
* @param theValue
* The field value to set
*/
public void addToEntry(IBase theEntry, String theEntryChildName, IBase theValue) {
addToBase(theEntry, theEntryChildName, theValue, myEntryDef);
}
/**
* Sets the specified search field.
*
* @param theSearch
* The search instance to set values on
* @param theSearchFieldName
* The child field name of the search instance to be set
* @param theSearchFieldValue
* The field value to set
*/
public void addToSearch(IBase theSearch, String theSearchFieldName, IBase theSearchFieldValue) {
addToBase(theSearch, theSearchFieldName, theSearchFieldValue, mySearchDef);
}
private void addToBase(IBase theBase, String theSearchChildName, IBase theValue, BaseRuntimeElementDefinition mySearchDef) {
BaseRuntimeChildDefinition defn = mySearchDef.getChildByName(theSearchChildName);
Validate.notNull(defn, "Unable to get child definition %s from %s", theSearchChildName, theBase);
defn.getMutator().addValue(theBase, theValue);
}
/**
* Creates a new primitive.
*
* @param theTypeName
* The element type for the primitive
* @param <T>
* Actual type of the parameterized primitive type interface
* @return
* Returns the new empty instance of the element definition.
*/
public <T> IPrimitiveType<T> newPrimitive(String theTypeName) {
BaseRuntimeElementDefinition primitiveDefinition = myContext.getElementDefinition(theTypeName);
Validate.notNull(primitiveDefinition, "Unable to find definition for %s", theTypeName);
return (IPrimitiveType<T>) primitiveDefinition.newInstance();
}
/**
* Creates a new primitive instance of the specified element type.
*
* @param theTypeName
* Element type to create
* @param theInitialValue
* Initial value to be set on the new instance
* @param <T>
* Actual type of the parameterized primitive type interface
* @return
* Returns the newly created instance
*/
public <T> IPrimitiveType<T> newPrimitive(String theTypeName, T theInitialValue) {
IPrimitiveType<T> retVal = newPrimitive(theTypeName);
retVal.setValue(theInitialValue);
return retVal;
}
public class UpdateBuilder {
private final IPrimitiveType<?> myUrl;

View File

@ -244,7 +244,16 @@ public class ParametersUtil {
IPrimitiveType<Integer> count = (IPrimitiveType<Integer>) theCtx.getElementDefinition("integer").newInstance();
count.setValue(theValue);
addParameterToParameters(theCtx, theParameters, theName, count);
}
public static void addParameterToParametersLong(FhirContext theCtx, IBaseParameters theParameters, String theName, long theValue) {
addParameterToParametersDecimal(theCtx, theParameters, theName, BigDecimal.valueOf(theValue));
}
public static void addParameterToParametersDecimal(FhirContext theCtx, IBaseParameters theParameters, String theName, BigDecimal theValue) {
IPrimitiveType<BigDecimal> count = (IPrimitiveType<BigDecimal>) theCtx.getElementDefinition("decimal").newInstance();
count.setValue(theValue);
addParameterToParameters(theCtx, theParameters, theName, count);
}
public static void addParameterToParametersReference(FhirContext theCtx, IBaseParameters theParameters, String theName, String theReference) {

View File

@ -38,7 +38,7 @@
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>hapi-fhir-server-empi</artifactId>
<artifactId>hapi-fhir-server-mdm</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
@ -98,7 +98,7 @@
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>hapi-fhir-jpaserver-empi</artifactId>
<artifactId>hapi-fhir-jpaserver-mdm</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>

View File

@ -22,11 +22,16 @@ package ca.uhn.hapi.fhir.docs;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.util.TransactionBuilder;
import ca.uhn.fhir.util.BundleBuilder;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.r4.model.Patient;
import java.math.BigDecimal;
import java.util.Date;
import java.util.UUID;
@SuppressWarnings("unused")
public class TransactionBuilderExamples {
@ -36,7 +41,7 @@ public class TransactionBuilderExamples {
public void update() throws FHIRException {
//START SNIPPET: update
// Create a TransactionBuilder
TransactionBuilder builder = new TransactionBuilder(myFhirContext);
BundleBuilder builder = new BundleBuilder(myFhirContext);
// Create a Patient to update
Patient patient = new Patient();
@ -54,7 +59,7 @@ public class TransactionBuilderExamples {
public void updateConditional() throws FHIRException {
//START SNIPPET: updateConditional
// Create a TransactionBuilder
TransactionBuilder builder = new TransactionBuilder(myFhirContext);
BundleBuilder builder = new BundleBuilder(myFhirContext);
// Create a Patient to update
Patient patient = new Patient();
@ -72,7 +77,7 @@ public class TransactionBuilderExamples {
public void create() throws FHIRException {
//START SNIPPET: create
// Create a TransactionBuilder
TransactionBuilder builder = new TransactionBuilder(myFhirContext);
BundleBuilder builder = new BundleBuilder(myFhirContext);
// Create a Patient to create
Patient patient = new Patient();
@ -89,7 +94,7 @@ public class TransactionBuilderExamples {
public void createConditional() throws FHIRException {
//START SNIPPET: createConditional
// Create a TransactionBuilder
TransactionBuilder builder = new TransactionBuilder(myFhirContext);
BundleBuilder builder = new BundleBuilder(myFhirContext);
// Create a Patient to create
Patient patient = new Patient();
@ -104,4 +109,30 @@ public class TransactionBuilderExamples {
//END SNIPPET: createConditional
}
public void customizeBundle() throws FHIRException {
//START SNIPPET: customizeBundle
// Create a TransactionBuilder
BundleBuilder builder = new BundleBuilder(myFhirContext);
// Set bundle type to be searchset
builder
.setBundleField("type", "searchset")
.setBundleField("id", UUID.randomUUID().toString())
.setMetaField("lastUpdated", builder.newPrimitive("instant", new Date()));
// Create bundle entry
IBase entry = builder.addEntry();
// Create a Patient to create
Patient patient = new Patient();
patient.setActive(true);
patient.addIdentifier().setSystem("http://foo").setValue("bar");
builder.addToEntry(entry, "resource", patient);
// Add search results
IBase search = builder.addSearch(entry);
builder.setSearchField(search, "mode", "match");
builder.setSearchField(search, "score", builder.newPrimitive("decimal", BigDecimal.ONE));
//END SNIPPET: customizeBundle
}
}

View File

@ -1,9 +1,9 @@
---
type: add
issue: 2021
title: "Added [EMPI](https://hapifhir.io/hapi-fhir/docs/server_jpa_empi/empi.html) functionality, including phonetic
title: "Added [EMPI](https://hapifhir.io/hapi-fhir/docs/server_jpa_mdm/mdm.html) functionality, including phonetic
indexing, asynchronous rules-based patient and practitioner matching when resources are created and updated. A number of
[EMPI Operations](https://hapifhir.io/hapi-fhir/docs/server_jpa_empi/empi_operations.html) are provided to
[EMPI Operations](https://hapifhir.io/hapi-fhir/docs/server_jpa_mdm/mdm_operations.html) are provided to
maintain EMPI links (e.g. resolving possible matches and possible duplicates). Also batch operations
are provided to identify links in existing patients and practitioners, and to 'wipe clean' all EMPI data and re-run
the batch as the empi matching rules are refined."

View File

@ -0,0 +1,19 @@
---
type: add
issue: 2177
title: "Redesigning the Enterprise Master Patient Index solution to a Master Data Management solution. The new MDM solution supports other FHIR resources where EMPI only allowed Person resource to be used. For example, if MDM is occurring on a patient, we will create a new Patient, and tag that patient as a Golden Record. This means that several things have changed:
<ul>
<li>Provider methods that pointed to type of Person are now server-level operations in which you specify a resource type.</li>
<li>Link updating and querying no longer rely on Person IDs, but instead on arbitrary resource ids, depending on the resource type you are referring to in MDM.</li>
<li>Change to the EMPI config to require a list of mdmTypes.</li>
</ul>
<br/>
Code-level changes include the following changes:
<ul>
<li>hapi-fhir-server-empi and hapi-fhir-jpaserver-empi Maven projects were renamed to hapi-fhir-server-mdm and hapi-fhir-jpaserver-mdm</li>
<li>All classes were refactored to use correct terms, e.g. Golden Resource in place of Person</li>
<li>Message channel was renamed from `empi` to `mdm`</li>
<li>Subscriptions were renamed to `mdm-RESOURCE_TYPE`, where RESOURCE_TYPE is an MDM type configured in mdmTypes section of the configuration file</li>
<li>Configuration file was renamed from empi-rules.json to mdm-rules.json</li>
<li>Log file was changed from empi-troubleshooting.log to mdm-troubleshooting.log</li>
</ul>"

View File

@ -51,12 +51,12 @@ page.server_jpa.diff=Diff Operation
page.server_jpa.lastn=LastN Operation
page.server_jpa.terminology=Terminology
section.server_jpa_empi.title=JPA Server: EMPI
page.server_jpa_empi.empi=EMPI Getting Started
page.server_jpa_empi.empi_rules=EMPI Rules
page.server_jpa_empi.empi_eid=EMPI Enterprise Identifiers
page.server_jpa_empi.empi_operations=EMPI Operations
page.server_jpa_empi.empi_details=EMPI Technical Details
section.server_jpa_mdm.title=JPA Server: MDM
page.server_jpa_mdm.mdm=MDM Getting Started
page.server_jpa_mdm.mdm_rules=MDM Rules
page.server_jpa_mdm.mdm_eid=MDM Enterprise Identifiers
page.server_jpa_mdm.mdm_operations=MDM Operations
page.server_jpa_mdm.mdm_details=MDM Technical Details
section.server_jpa_partitioning.title=JPA Server: Partitioning and Multitenancy
page.server_jpa_partitioning.partitioning=Partitioning and Multitenancy

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 61 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 61 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 47 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 63 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 76 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -1,6 +1,6 @@
# Transaction Builder
# Bundle Builder
The TransactionBuilder ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/util/TransactionBuilder.html)) can be used to construct FHIR Transaction Bundles.
The BundleBuilder ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-base/ca/uhn/fhir/util/BundleBuilder.html)) can be used to construct FHIR Bundles.
Note that this class is a work in progress! It does not yet support all transaction features. We will add more features over time, and document them here. Pull requests are welcomed.
@ -22,7 +22,7 @@ If you want to perform a conditional create:
# Resource Updates
To add an update (aka PUT) operation to a transaction bundle
To add an update (aka PUT) operation to a transaction bundle:
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/TransactionBuilderExamples.java|update}}
@ -36,4 +36,11 @@ If you want to perform a conditional update:
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/TransactionBuilderExamples.java|updateConditional}}
```
# Customizing bundle
If you want to manipulate a bundle:
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/TransactionBuilderExamples.java|customizeBundle}}
```

View File

@ -1,35 +0,0 @@
# EMPI Getting Started
## Introduction
An Enterprise Master Patient Index (EMPI) allows for links to be created and maintained between different Patient and/or Practitioner resources. These links are used to indicate the fact that different Patient/Practitioner resources are known or believed to refer to the same actual (real world) person.
These links are created and updated using different combinations of automatic linking and manual linking.
Note: This documentation describes EMPI for Patient resources. The same information applies for Practitioner resources. You can substitute "Practitioner" for "Patient" anywhere it appears in this documentation.
## Working Example
A complete working example of HAPI EMPI can be found in the [JPA Server Starter](/hapi-fhir/docs/server_jpa/get_started.html) project. You may wish to browse its source to see how it is set up.
## Overview
To get up and running with HAPI EMPI, either enable it using the `hapi.properties` file in the JPA Server Starter, or follow the instructions below to (enable it in HAPI FHIR directly)[#empi-settings].
Once EMPI is enabled, the next thing you will want to do is configure your [EMPI Rules](/hapi-fhir/docs/server_jpa_empi/empi_rules.html)
HAPI EMPI watches for incoming Patient resources and automatically links them to Person resources based on these rules. For example, if the rules indicate that any two patients with the same ssn, birthdate and first and last name are the same person, then two different Patient resources with matching values for these attributes will automatically be linked to the same Person resource. If no existing resources match the incoming Patient, then a new Person resource will be created and linked to the incoming Patient.
Based on how well two patients match, the EMPI Rules may link the Patient to the Person as a MATCH or a POSSIBLE_MATCH. In the case of a POSSIBLE_MATCH, a user will need to later use [EMPI Operations](/hapi-fhir/docs/server_jpa_empi/empi_operations.html) to either confirm the link as a MATCH, or mark the link as a NO_MATCH in which case HAPI EMPI will create a new Person for them.
Another thing that can happen in the linking process is HAPI EMPI can determine that two Person resources may be duplicates. In this case, it marks them as POSSIBLE_DUPLICATE and the user can use [EMPI Operations](/hapi-fhir/docs/server_jpa_empi/empi_operations.html) to either merge the two Persons or mark them as NO_MATCH in which case HAPI EMPI will know not to mark them as possible duplicates in the future.
HAPI EMPI keeps track of which links were automatically established vs manually verified. Manual links always take precedence over automatic links. Once a link for a patient has been manually verified, HAPI EMPI won't modify or remove it.
## EMPI Settings
Follow these steps to enable EMPI on the server:
The [EmpiSettings](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html) bean contains configuration settings related to EMPI within the server. To enable EMPI, the [setEnabled(boolean)](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html#setEnabled(boolean)) property should be enabled.
See [EMPI EID Settings](/hapi-fhir/docs/server_jpa_empi/empi_eid.html#empi-eid-settings) for a description of the EID-related settings.

View File

@ -1,81 +0,0 @@
# EMPI Implementation Details
This section describes details of how EMPI functionality is implemented in HAPI FHIR.
## Person linking in FHIR
Because HAPI EMPI is implemented on the HAPI JPA Server, it uses the FHIR model to represent roles and links. The following illustration shows an example of how these links work.
<a href="/hapi-fhir/docs/images/empi-links.svg"><img src="/hapi-fhir/docs/images/empi-links.svg" alt="EMPI links" style="margin-left: 15px; margin-bottom: 15px; width: 500px;" /></a>
There are several resources that are used:
* Patient - Represents the record of a person who receives healthcare services
* Person - Represents a master record with links to one or more Patient and/or Practitioner resources that belong to the same person
# Automatic Linking
With EMPI enabled, the default behavior of the EMPI is to create a new Person record for every Patient that is created such that there is a 1:1 relationship between them. Any relinking is then expected to be done manually via the [EMPI Operations](/hapi-fhir/docs/server_jpa_empi/empi_operations.html).
In a typical configuration it is often desirable to have links be created automatically using matching rules. For example, you might decide that if a Patient shares the same name, gender, and date of birth as another Patient, you have at least a little confidence that they are the same Person.
This automatic linking is done via configurable matching rules that create links between Patients and Persons. Based on the strength of the match configured in these rules, the link will be set to either POSSIBLE_MATCH or MATCH.
It is important to note that before a resource is processed by EMPI, it is first checked to ensure that it has at least one attribute that the EMPI system cares about, as defined in the `empi-rules.json` file. If the incoming resource has no such attributes, then EMPI processing does not occur on it. In this case, no Person is created for them. If in the future that Patient is updated to contain attributes the EMPI system does concern itself with, it will be processed at that time.
## Design
Below are some simplifying principles HAPI EMPI follows to reduce complexity and ensure data integrity.
1. When EMPI is enabled on a HAPI FHIR server, any Person resource in the repository that has the "hapi-empi" tag is considered read-only by the FHIR endpoint. These Person resources are managed exclusively by HAPI EMPI. Users can only change them via [EMPI Operations](/hapi-fhir/docs/server_jpa_empi/empi_operations.html). In most cases, users will indirectly change them by creating and updating Patient and Practitioner ("Patient") resources. For the rest of this document, assume "Person" refers to a "hapi-empi" tagged Person resource.
1. Every Patient in the system has a MATCH link to at most one Person resource.
1. The only Patient resources in the system that do not have a MATCH link are those that have the 'no-empi' tag or those that have POSSIBLE_MATCH links pending review.
1. The HAPI EMPI rules define a single identifier system that holds the external enterprise id ("EID"). If a Patient has an external EID, then the Person it links to always has the same EID. If a patient has no EID when it arrives, a unique UUID will be assigned as that Person's EID.
1. A Person can have both an internal EID(auto-created by HAPI), and an external EID (provided by an external system).
1. Two different Person resources cannot have the same EID.
1. Patient resources are only ever compared to Person resources via this EID. For all other matches, Patient resources are only ever compared to Patient resources and Practitioner resources are only ever compared to Practitioner resources.
## Links
1. HAPI EMPI manages empi-link records ("links") that link a Patient resource to a Person resource. When these are created/updated by matching rules, the links are marked as AUTO. When these links are changed manually, they are marked as MANUAL.
1. Once a link has been manually assigned as NO_MATCH or MATCH, the system will not change it.
1. When a new Patient resource is created/updated it is then compared to all other Patient resources in the repository. The outcome of each of these comparisons is either NO_MATCH, POSSIBLE_MATCH or MATCH.
1. Whenever a MATCH link is established between a Patient resource and a Person resource, that Patient is always added to that Person resource links. All MATCH links have corresponding Person resource links and all Person resource links have corresponding MATCH empi-link records. You can think of the fields of the empi-link records as extra meta-data associated with each Person.link.target.
1. HAPI EMPI stores these extra link details in a table called `MPI_LINK`.
1. Each record in the `MPI_LINK` table corresponds to a `link.target` entry on a Person resource unless it is a NO_MATCH record. HAPI EMPI uses the following convention for the Person.link.assurance level:
1. Level 1: POSSIBLE_MATCH
1. Level 2: AUTO MATCH
1. Level 3: MANUAL MATCH
1. Level 4: GOLDEN RECORD
### Possible rule match outcomes:
When a new Patient resource is compared with all other resources of that type in the repository, there are four possible outcomes:
* CASE 1: No MATCH and no POSSIBLE_MATCH outcomes -> a new Person resource is created and linked to that Patient as MATCH. All fields are copied from the Patient to the Person. If the incoming resource has an EID, it is copied to the Person. Otherwise a new UUID is generated and used as the internal EID.
* CASE 2: All of the MATCH Patient resources are already linked to the same Person -> a new Link is created between the new Patient and that Person and is set to MATCH.
* CASE 3: The MATCH Patient resources link to more than one Person -> Mark all links as POSSIBLE_MATCH. All other Person resources are marked as POSSIBLE_DUPLICATE of this first Person. These duplicates are manually reviewed later and either merged or marked as NO_MATCH and the system will no longer consider them as a POSSIBLE_DUPLICATE going forward. POSSIBLE_DUPLICATE is the only link type that can have a Person as both the source and target of the link.
* CASE 4: Only POSSIBLE_MATCH outcomes -> In this case, new POSSIBLE_MATCH links are created and await manual reassignment to either NO_MATCH or MATCH.
# HAPI EMPI Technical Details
When EMPI is enabled, the HAPI FHIR JPA Server does the following things on startup:
1. It enables the MESSAGE subscription type and starts up the internal subscription engine.
1. It creates two MESSAGE subscriptions, called 'empi-patient' and 'empi-practitioner' that match all incoming Patient and Practitioner resources and send them to an internal queue called "empi". The JPA Server listens to this queue and links incoming resources to Persons.
1. The [EMPI Operations](/hapi-fhir/docs/server_jpa_empi/empi_operations.html) are registered with the server.
1. It registers a new dao interceptor that restricts access to EMPI managed Person records.

View File

@ -1,42 +0,0 @@
# EMPI Enterprise Identifiers
An Enterprise Identifier(EID) is a unique identifier that can be attached to Patients or Practitioners. Each implementation is expected to use exactly one EID system for incoming resources, defined in the EMPI Rules file. If a Patient or Practitioner with a valid EID is submitted, that EID will be copied over to the Person that was matched. In the case that the incoming Patient or Practitioner had no EID assigned, an internal EID will be created for it. There are thus two classes of EID. Internal EIDs, created by HAPI-EMPI, and External EIDs, provided by the submitted resources.
## EMPI EID Settings
The [EmpiSettings](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html) bean contains two EID related settings. Both are enabled by default.
* **Prevent EID Updates** ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html#setPreventEidUpdates(boolean))): If this is enabled, then once an EID is set on a resource, it cannot be changed. If disabled, patients may have their EID updated.
* **Prevent multiple EIDs**: ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html#setPreventMultipleEids(boolean))): If this is enabled, then a resource cannot have more than one EID, and incoming resources that break this rule will be rejected.
## EMPI EID Scenarios
EMPI EID management follows a complex set of rules to link related Patient records via their Enterprise Id. The following diagrams outline how EIDs are replicated from Patient resources to their linked Person resources under various scenarios according to the values of the EID Settings.
## EMPI EID Create Scenarios
<a href="/hapi-fhir/docs/images/empi-create-1.svg"><img src="/hapi-fhir/docs/images/empi-create-1.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-create-2.svg"><img src="/hapi-fhir/docs/images/empi-create-2.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-create-3.svg"><img src="/hapi-fhir/docs/images/empi-create-3.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-create-4.svg"><img src="/hapi-fhir/docs/images/empi-create-4.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-create-5.svg"><img src="/hapi-fhir/docs/images/empi-create-5.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
## EMPI EID Update Scenarios
<a href="/hapi-fhir/docs/images/empi-update-1.svg"><img src="/hapi-fhir/docs/images/empi-update-1.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-update-2.svg"><img src="/hapi-fhir/docs/images/empi-update-2.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-update-3.svg"><img src="/hapi-fhir/docs/images/empi-update-3.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-update-4.svg"><img src="/hapi-fhir/docs/images/empi-update-4.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-update-5.svg"><img src="/hapi-fhir/docs/images/empi-update-5.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-update-6.svg"><img src="/hapi-fhir/docs/images/empi-update-6.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>

View File

@ -0,0 +1,41 @@
# MDM Getting Started
<div class="helpInfoCalloutBox">
MDM module replaces the EMPI module. EMPI is now deprecated and can not be used. Please refer to the [Migration Instructions](#migration-instructions) section for more details.
</div>
## Introduction
A Master Data Management (MDM) module allows for links to be created and maintained among FHIR resources. These links indicate the fact that different FHIR resources are known or believed to refer to the same actual (real world) resource. The links are created and updated using different combinations of automatic and manual linking.
The real-world resource is referred to as the Golden Resource in this context. The resource believed to be a duplicate is said to be a source resource.
## Working Example
A complete working example of HAPI MDM can be found in the [JPA Server Starter](/hapi-fhir/docs/server_jpa/get_started.html) project. You may wish to browse its source to see how it is set up.
## Overview
To get up and running with HAPI MDM, either enable it using the `hapi.properties` file in the JPA Server Starter, or follow the instructions below to (enable it in HAPI FHIR directly)[#mdm-settings].
Once MDM is enabled, the next thing you will want to do is configure your [MDM Rules](/hapi-fhir/docs/server_jpa_mdm/mdm_rules.html)
HAPI MDM watches for incoming source resources and automatically links them to the appropriate Golden Resources based on these rules. For example, if the rules indicate that any two patients with the same SSN, birthdate and first and last name are the same patient, then two different Patient resources with matching values for these attributes will automatically be linked to the same Golden Patient resource. If no existing resources match the incoming Patient, then a new Golden Patient resource will be created and linked to the incoming Patient.
Based on how well two patients match, the MDM Rules may link the Patient to the Golden Patient as a MATCH or a POSSIBLE_MATCH. In the case of a POSSIBLE_MATCH, a user will need to later use [MDM Operations](/hapi-fhir/docs/server_jpa_mdm/mdm_operations.html) to either confirm the link as a MATCH, or mark the link as a NO_MATCH in which case HAPI MDM will create a new Golden Resource Patient record for them.
Another thing that can happen in the linking process is HAPI MDM can determine that two Patients resources may be duplicates. In this case, it marks them as POSSIBLE_DUPLICATE and the user can use [MDM Operations](/hapi-fhir/docs/server_jpa_mdm/mdm_operations.html) to either merge the two Patients or mark them as NO_MATCH in which case HAPI MDM will know not to mark them as possible duplicates in the future.
HAPI MDM keeps track of which links were automatically established vs manually verified. Manual links always take precedence over automatic links. Once a link for a patient has been manually verified, HAPI MDM won't modify or remove it.
## MDM Settings
Follow these steps to enable MDM on the server:
The [MdmSettings](/hapi-fhir/apidocs/hapi-fhir-server-mdm/ca/uhn/fhir/mdm/rules/config/MdmSettings.html) bean contains configuration settings related to MDM within the server. To enable MDM, the [setEnabled(boolean)](/hapi-fhir/apidocs/hapi-fhir-server-mdm/ca/uhn/fhir/mdm/rules/config/MdmSettings.html#setEnabled(boolean)) property should be enabled.
See [MDM EID Settings](/hapi-fhir/docs/server_jpa_mdm/mdm_eid.html#mdm-eid-settings) for a description of the EID-related settings.
## Migration Instructions
Please note that EMPI is now deprecated and cannot be used. To switch from EMPI to MDM, please copy over EMPI settings to MDM module settings. Also note that MDM now requires "mdmTypes" in the JSON configuration. This entry should include all FHIR resource types that are supported by MDM. For more details on supported MDM types refer to [MDM Rules](/hapi-fhir/docs/server_jpa_mdm/mdm_rules.html)

View File

@ -0,0 +1,63 @@
# MDM Implementation Details
This section describes details of how MDM functionality is implemented in HAPI FHIR.
# Automatic Linking
With MDM enabled, the default behavior of the MDM is to create a new Golden Record for every source record that is created such that there is a 1:1 relationship between them. Any relinking is then expected to be done manually via the [MDM Operations](/hapi-fhir/docs/server_jpa_mdm/mdm_operations.html).
In a typical configuration it is often desirable to have links created automatically using matching rules. For example, you might decide that if a Patient shares the same name, gender, and date of birth as another Patient, you have at least a little confidence that they are the same Patient.
This automatic linking is done via configurable matching rules that create links between source record and Golden Record. Based on the strength of the match configured in these rules, the link will be set to either POSSIBLE_MATCH or MATCH.
It is important to note that before a resource is processed by MDM, it is first checked to ensure that it has at least one attribute that the MDM system cares about, as defined in the `mdm-rules.json` file. If the incoming resource has no such attributes, then MDM processing does not occur on it. In this case, no Golden Resource is created for this source resource. If in the future the source resource is updated to contain attributes the MDM system does concern itself with, it will be processed at that time.
## Design
Below are some simplifying principles HAPI MDM follows to reduce complexity and ensure data integrity.
1. When MDM is enabled on a HAPI FHIR server, any Golden Resource in the repository that has the "hapi-mdm" tag is considered read-only by the FHIR endpoint. These Golden Resources are managed exclusively by HAPI MDM. Users can only change them via [MDM Operations](/hapi-fhir/docs/server_jpa_mdm/mdm_operations.html). In most cases, users will indirectly change them by creating and updating the corresponding source resources.
1. Every source resource in the system has a MATCH link to at most one Golden Resource.
1. The only source resources in the system that do not have a MATCH link are those that have the 'NO-MDM' tag or those that have POSSIBLE_MATCH links pending review.
1. The HAPI MDM rules define a single identifier system that holds the external enterprise id ("EID"). If a source resource has an external EID, then the Golden Resource it links to always has the same EID. If a source resource has no EID when it arrives, a unique UUID will be assigned as that source resource's EID.
1. A Golden Resource can have both an internal EID (auto-created by HAPI), and an external EID (provided by an
external system).
1. Two different Golden Resources cannot have the same EID.
1. Source resources are only ever compared to Golden Resources via this EID.
## Links
1. HAPI MDM manages mdm-link records ("links") that link a source resource to a Golden Resource. When these are created/updated by matching rules, the links are marked as AUTO. When these links are changed manually, they are marked as MANUAL.
1. Once a link has been manually assigned as NO_MATCH or MATCH, the system will not change it.
1. When a new source resource is created/updated it is then compared to all other source resources of the same type in the repository. The outcome of each of these comparisons is either NO_MATCH, POSSIBLE_MATCH or MATCH.
1. HAPI MDM stores these extra link details in a table called `MPI_LINK`.
### Possible rule match outcomes:
When a new source resource is compared with all other resources of the same type in the repository, there are four possible outcomes:
* CASE 1: No MATCH and no POSSIBLE_MATCH outcomes -> a new Golden Resource is created and linked to that source resource as MATCH. If the incoming resource has an EID, it is copied to the Golden Resource. Otherwise a new UUID is generated and used as the internal EID.
* CASE 2: All of the MATCH source resources are already linked to the same Golden Resource -> a new Link is created between the new source resource and that Golden Resource and is set to MATCH.
* CASE 3: The MATCH source resources link to more than one Golden Resource -> Mark all links as POSSIBLE_MATCH. All other Golden Resources are marked as POSSIBLE_DUPLICATE of this first Golden Resource. These duplicates are manually reviewed later and either merged or marked as NO_MATCH and the system will no longer consider them as a POSSIBLE_DUPLICATE going forward. POSSIBLE_DUPLICATE is the only link type that can have a Golden Resource as both the source and target of the link.
* CASE 4: Only POSSIBLE_MATCH outcomes -> In this case, new POSSIBLE_MATCH links are created and await manual reassignment to either NO_MATCH or MATCH.
# HAPI MDM Technical Details
When MDM is enabled, the HAPI FHIR JPA Server does the following things on startup:
1. It enables the MESSAGE subscription type and starts up the internal subscription engine.
1. It creates MESSAGE subscriptions for each resource type prefixed with 'mdm-'. For example, if MDM supports Patient and Practitioner resource, two subscriptions, called 'mdm-patient' and 'mdm-practitioner' that match all incoming MDM managed resources and send them to an internal queue called "mdm". The JPA Server listens to this queue and links incoming resources to the appropriate Golden Resources.
1. The [MDM Operations](/hapi-fhir/docs/server_jpa_mdm/mdm_operations.html) are registered with the server.
1. It registers a new dao interceptor that restricts access to MDM managed Golden Resource records.

View File

@ -0,0 +1,45 @@
# MDM Enterprise Identifiers
An Enterprise Identifier (EID) is a unique identifier that can be attached to source resources. Each implementation is expected to use exactly one EID system for incoming resources, defined in the MDM Rules file. If a source resource with a valid EID is submitted, that EID will be copied over to the Golden Resource that was matched. In the case that the incoming source resource had no EID assigned, an internal EID will be created for it. There are thus two classes of EID:
* Internal EIDs, created by HAPI-MDM, and
* External EIDs, provided by the submitted resources.
## MDM EID Settings
The [MdmSettings](/hapi-fhir/apidocs/hapi-fhir-server-mdm/ca/uhn/fhir/mdm/rules/config/MdmSettings.html) bean
contains two EID related settings. Both are enabled by default.
* **Prevent EID Updates** ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-server-mdm/ca/uhn/fhir/mdm/rules/config/MdmSettings.html#setPreventEidUpdates(boolean))): If this is enabled, then once an EID is set on a resource, it cannot be changed. If disabled, patients may have their EID updated.
* **Prevent multiple EIDs**: ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-server-mdm/ca/uhn/fhir/mdm/rules/config/MdmSettings.html#setPreventMultipleEids(boolean))): If this is enabled, then a resource cannot have more than one EID, and incoming resources that break this rule will be rejected.
## MDM EID Scenarios
MDM EID management follows a complex set of rules to link related source records via their Enterprise Id. The following diagrams outline how EIDs are replicated from Patient resources to their linked Golden Patient resources under various scenarios according to the values of the EID Settings.
## MDM EID Create Scenarios
<a href="/hapi-fhir/docs/images/empi-create-1.svg"><img src="/hapi-fhir/docs/images/empi-create-1.svg" alt="MDM Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-create-2.svg"><img src="/hapi-fhir/docs/images/empi-create-2.svg" alt="MDM Create 2" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-create-3.svg"><img src="/hapi-fhir/docs/images/empi-create-3.svg" alt="MDM Create 3" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-create-4.svg"><img src="/hapi-fhir/docs/images/empi-create-4.svg" alt="MDM Create 4" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-create-5.svg"><img src="/hapi-fhir/docs/images/empi-create-5.svg" alt="MDM Create 5" style="margin-left: 15px; margin-bottom: 15px;" /></a>
## MDM EID Update Scenarios
<a href="/hapi-fhir/docs/images/empi-update-1.svg"><img src="/hapi-fhir/docs/images/empi-update-1.svg" alt="MDM Update 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-update-2.svg"><img src="/hapi-fhir/docs/images/empi-update-2.svg" alt="MDM Update 2" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-update-3.svg"><img src="/hapi-fhir/docs/images/empi-update-3.svg" alt="MDM Update 3" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-update-4.svg"><img src="/hapi-fhir/docs/images/empi-update-4.svg" alt="MDM Update 4" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-update-5.svg"><img src="/hapi-fhir/docs/images/empi-update-5.svg" alt="MDM Update 5" style="margin-left: 15px; margin-bottom: 15px;" /></a>
<a href="/hapi-fhir/docs/images/empi-update-6.svg"><img src="/hapi-fhir/docs/images/empi-update-6.svg" alt="MDM Update 6" style="margin-left: 15px; margin-bottom: 15px;" /></a>

View File

@ -1,12 +1,12 @@
# EMPI Operations
# MDM Operations
EMPI links are managed by EMPI Operations. These operations are supplied by a [plain provider](/docs/server_plain/resource_providers.html#plain-providers) called [EmpiProvider](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/provider/EmpiProviderR4.html).
MDM links are managed by MDM Operations. These operations are supplied by a [plain provider](/docs/server_plain/resource_providers.html#plain-providers) called [MdmProvider](/hapi-fhir/apidocs/hapi-fhir-server-mdm/ca/uhn/fhir/mdm/provider/MdmProviderDstu3Plus.html).
In cases where the operation changes data, if a resource id parameter contains a version (e.g. `Person/123/_history/1`), then the operation will fail with a 409 CONFLICT if that is not the latest version of that resource. This feature can be used to prevent update conflicts in an environment where multiple users are working on the same set of empi links.
In cases where the operation changes data, if a resource id parameter contains a version (e.g. `Patient/123/_history/1`), then the operation will fail with a 409 CONFLICT if that is not the latest version of that resource. This feature can be used to prevent update conflicts in an environment where multiple users are working on the same set of mdm links.
## Query links
Ue the `$empi-query-links` operation to view empi links. The results returned are based on the parameters provided. All parameters are optional. This operation takes the following parameters:
Use the `$mdm-query-links` operation to view MDM links. The results returned are based on the parameters provided. All parameters are optional. This operation takes the following parameters:
<table class="table table-striped table-condensed">
<thead>
@ -19,19 +19,19 @@ Ue the `$empi-query-links` operation to view empi links. The results returned a
</thead>
<tbody>
<tr>
<td>personId</td>
<td>goldenResourceId</td>
<td>String</td>
<td>0..1</td>
<td>
The id of the Person resource.
The id of the Golden Resource (e.g. Golden Patient Resource).
</td>
</tr>
<tr>
<td>targetId</td>
<td>resourceId</td>
<td>String</td>
<td>0..1</td>
<td>
The id of the Patient or Practitioner resource.
The id of the source resource (e.g. Patient resource).
</td>
</tr>
<tr>
@ -55,10 +55,10 @@ Ue the `$empi-query-links` operation to view empi links. The results returned a
### Example
Use an HTTP GET like `http://example.com/$empi-query-links?matchResult=POSSIBLE_MATCH` or an HTTP POST to the following URL to invoke this operation:
Use an HTTP GET like `http://example.com/$mdm-query-links?matchResult=POSSIBLE_MATCH` or an HTTP POST to the following URL to invoke this operation:
```url
http://example.com/$empi-query-links
http://example.com/$mdm-query-links
```
The following request body could be used to find all POSSIBLE_MATCH links in the system:
@ -81,10 +81,10 @@ This operation returns a `Parameters` resource that looks like the following:
"parameter": [ {
"name": "link",
"part": [ {
"name": "personId",
"valueString": "Person/123"
"name": "goldenResourceId",
"valueString": "Patient/123"
}, {
"name": "targetId",
"name": "sourceResourceId",
"valueString": "Patient/456"
}, {
"name": "matchResult",
@ -96,7 +96,7 @@ This operation returns a `Parameters` resource that looks like the following:
"name": "eidMatch",
"valueBoolean": false
}, {
"name": "newPerson",
"name": "hadToCreateNewResource",
"valueBoolean": false
}, {
"name": "score",
@ -106,61 +106,20 @@ This operation returns a `Parameters` resource that looks like the following:
}
```
## Querying links via the Person resource
## Query Duplicate Golden Resources
Alternatively, you can query Empi links by querying Person resources directly. Empi represents links in Person resources using the following mapping:
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>EMPI matchResult</th>
<th>EMPI linkSource</th>
<th>Person link.assurance</th>
</tr>
</thead>
<tbody>
<tr>
<td>NO_MATCH</td>
<td>MANUAL</td>
<td>No link present</td>
</tr>
<tr>
<td>POSSIBLE_MATCH</td>
<td>AUTO</td>
<td>level2</td>
</tr>
<tr>
<td>MATCH</td>
<td>AUTO</td>
<td>level3</td>
</tr>
<tr>
<td>MATCH</td>
<td>MANUAL</td>
<td>level4</td>
</tr>
</tbody>
</table>
For example, you can use the following HTTP GET to find all Person resources that have POSSIBLE_MATCH links:
```
http://example.com/Person?assurance=level2
```
## Query Duplicate Persons
Use the `$empi-duplicate-persons` operation to request a list of duplicate persons. This operation takes no parameters
Use the `$mdm-duplicate-golden-resources` operation to request a list of duplicate Golden Resources.
This operation takes no parameters.
### Example
Use an HTTP GET to the following URL to invoke this operation:
```url
http://example.com/$empi-duplicate-persons
http://example.com/$mdm-duplicate-golden-resources
```
This operation returns `Parameters` similar to `$empi-query-links`:
This operation returns `Parameters` similar to `$mdm-query-links`:
```json
@ -169,11 +128,11 @@ This operation returns `Parameters` similar to `$empi-query-links`:
"parameter": [ {
"name": "link",
"part": [ {
"name": "personId",
"valueString": "Person/123"
"name": "goldenResourceId",
"valueString": "Patient/123"
}, {
"name": "targetId",
"valueString": "Person/456"
"name": "sourceResourceId",
"valueString": "Patient/456"
}, {
"name": "matchResult",
"valueString": "POSSIBLE_DUPLICATE"
@ -185,9 +144,10 @@ This operation returns `Parameters` similar to `$empi-query-links`:
}
```
## Unduplicate Persons
## Unduplicate Golden Resources
Use the `$empi-not-duplicate` operation to mark duplicate persons as not duplicates. This operation takes the following parameters:
Use the `$mdm-not-duplicate` operation to mark duplicate Golden Resources as not duplicates.
This operation takes the following parameters:
<table class="table table-striped table-condensed">
<thead>
@ -200,19 +160,19 @@ Use the `$empi-not-duplicate` operation to mark duplicate persons as not duplica
</thead>
<tbody>
<tr>
<td>personId</td>
<td>goldenResourceId</td>
<td>String</td>
<td>1..1</td>
<td>
The id of the Person resource.
The id of the Golden Resource.
</td>
</tr>
<tr>
<td>targetId</td>
<td>resourceId</td>
<td>String</td>
<td>1..1</td>
<td>
The id of the Person that personId has a possible duplicate link to.
The id of the source resource that has a possible duplicate link to.
</td>
</tr>
</tbody>
@ -223,7 +183,7 @@ Use the `$empi-not-duplicate` operation to mark duplicate persons as not duplica
Use an HTTP POST to the following URL to invoke this operation:
```url
http://example.com/$empi-not-duplicate
http://example.com/$mdm-not-duplicate
```
The following request body could be used:
@ -232,11 +192,11 @@ The following request body could be used:
{
"resourceType": "Parameters",
"parameter": [ {
"name": "personId",
"valueString": "Person/123"
"name": "goldenResourceId",
"valueString": "Patient/123"
}, {
"name": "targetId",
"valueString": "Person/456"
"name": "resourceId",
"valueString": "Patient/456"
} ]
}
```
@ -255,7 +215,7 @@ When the operation is successful, it returns the following `Parameters`:
## Update Link
Use the `$empi-update-link` operation to change the `matchResult` update of an empi link. This operation takes the following parameters:
Use the `$mdm-update-link` operation to change the `matchResult` update of an mdm link. This operation takes the following parameters:
<table class="table table-striped table-condensed">
<thead>
@ -268,19 +228,19 @@ Use the `$empi-update-link` operation to change the `matchResult` update of an e
</thead>
<tbody>
<tr>
<td>personId</td>
<td>goldenResourceId</td>
<td>String</td>
<td>1..1</td>
<td>
The id of the Person resource.
The id of the Golden Resource.
</td>
</tr>
<tr>
<td>targetId</td>
<td>resourceId</td>
<td>String</td>
<td>1..1</td>
<td>
The id of the Patient or Practitioner resource.
The id of the source resource.
</td>
</tr>
<tr>
@ -294,26 +254,26 @@ Use the `$empi-update-link` operation to change the `matchResult` update of an e
</tbody>
</table>
Empi links updated in this way will automatically have their `linkSource` set to `MANUAL`.
MDM links updated in this way will automatically have their `linkSource` set to `MANUAL`.
### Example
Use an HTTP POST to the following URL to invoke this operation:
```url
http://example.com/$empi-update-link
http://example.com/$mdm-update-link
```
The following request body could be used:
Any supported MDM type can be used. The following request body shows how to update link on the Patient resource type:
```json
{
"resourceType": "Parameters",
"parameter": [ {
"name": "personId",
"valueString": "Person/123"
"name": "goldenResourceId",
"valueString": "Patient/123"
}, {
"name": "targetId",
"name": "resourceId",
"valueString": "Patient/456"
}, {
"name": "matchResult",
@ -322,13 +282,13 @@ The following request body could be used:
}
```
The operation returns the updated `Person` resource. Note that this is the only way to modify EMPI-managed `Person` resources.
The operation returns the updated Golden Resource. For the query above `Patient` resource will be returned. Note that this is the only way to modify MDM-managed Golden Resources.
## Merge Persons
## Merge Golden Resources
The `$empi-merge-persons` operation can be used to merge one Person resource with another. When doing this, you will need to decide which resource to merge from and which one to merge to. In most cases, fields will be merged (e.g. names, identifiers, and links will be the union of two). However when there is a conflict (e.g. birthday), fields in the toPerson will take precedence over fields in the fromPerson
The `$mdm-merge-golden-resources` operation can be used to merge one Golden Resource with another. When doing this, you will need to decide which resource to merge from and which one to merge to.
After the merge is complete, `fromPerson.active` is set to `false`. Also, a new link with assurance level 4 (MANUAL MATCH) will be added pointing from the fromPerson to the toPerson.
After the merge is complete, `fromGoldenResourceId` will be deactivated by assigning a metadata tag `REDIRECTED`.
This operation takes the following parameters:
@ -343,19 +303,19 @@ This operation takes the following parameters:
</thead>
<tbody>
<tr>
<td>fromPersonId</td>
<td>fromGoldenResourceId</td>
<td>String</td>
<td>1..1</td>
<td>
The id of the Person resource to merge data from.
The id of the Golden Resource to merge data from.
</td>
</tr>
<tr>
<td>toPersonId</td>
<td>toGoldenResourceId</td>
<td>String</td>
<td>1..1</td>
<td>
The id of the Person to merge data into.
The id of the Golden Resource to merge data into.
</td>
</tr>
</tbody>
@ -366,7 +326,7 @@ This operation takes the following parameters:
Use an HTTP POST to the following URL to invoke this operation:
```url
http://example.com/$empi-merge-persons
http://example.com/$mdm-merge-golden-resources
```
The following request body could be used:
@ -375,22 +335,24 @@ The following request body could be used:
{
"resourceType": "Parameters",
"parameter": [ {
"name": "fromPersonId",
"valueString": "Person/123"
"name": "fromGoldenResourceId",
"valueString": "Patient/123"
}, {
"name": "toPersonId",
"name": "toGoldenResourceId",
"valueString": "Patient/128"
} ]
}
```
This operation returns the merged Person resource.
This operation returns the merged Golden Resource (`toGoldenResourceId`).
# Querying The EMPI
# Querying The MDM
When EMPI is enabled, the [$match operation](http://hl7.org/fhir/patient-operation-match.html) will be enabled on the JPA Server.
## Querying the Patient Resource
This operation allows a Patient resource to be submitted to the endpoint, and the system will attempt to find and return any Patient resources that match it according to the matching rules.
When MDM is enabled, the [$match operation](http://hl7.org/fhir/patient-operation-match.html) will be enabled on the JPA Server.
This operation allows a Patient resource to be submitted to the endpoint, and the system will attempt to find and return any Patient resources that match it according to the matching rules. The response includes a search score field that is calculated by averaging the number of matched rules against total rules checked for the Patient resource. Appropriate match grade extension is also included.
For example, the following request may be submitted:
@ -414,7 +376,7 @@ Content-Type: application/fhir+json; charset=UTF-8
}
```
This might result in a response such as the following:
Sample response for the Patient match is included below:
```json
{
@ -442,20 +404,51 @@ This might result in a response such as the following:
}
],
"birthDate": "2000-01-01"
},
"search": {
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/match-grade",
"valueCode": "certain"
}],
"mode": "match",
"score": 0.9
}
}
]
}
```
## Clearing EMPI Links
## Querying the Other Supported MDM Resources via `/$mdm-match`
The `$empi-clear` operation is used to batch-delete EMPI links and related persons from the database. This operation is meant to
be used during the rules-tuning phase of the EMPI implementation so that you can quickly test your ruleset.
It permits the user to reset the state of their EMPI system without manual deletion of all related links and Persons.
Query operations on any other supported MDM type is also allowed. This operation will find resources that match the provided parameters according to the matching rules. The response includes a search score field that is calculated by averaging the number of matched rules against total rules checked for the Patient resource. Appropriate match grade extension is also included in the response.
After the operation is complete, all targeted EMPI links are removed from the system, and their related Person resources are deleted and expunged
from the server.
The request below may be submitted to search for `Orgaization` in case it defined as a supported MDM type:
```http
POST /Organization/$mdm-match
Content-Type: application/fhir+json; charset=UTF-8
{
"resourceType":"Parameters",
"parameter": [
{
"name":"resource",
"resource": {
"resourceType":"Orgaization",
"name": "McMaster Family Practice"
}
}
]
}
```
MDM will respond with the appropriate resource bundle.
## Clearing MDM Links
The `$mdm-clear` operation is used to batch-delete MDM links and related Golden Resources from the database. This operation is meant to be used during the rules-tuning phase of the MDM implementation so that you can quickly test your ruleset. It permits the user to reset the state of their MDM system without manual deletion of all related links and Golden Resources.
After the operation is complete, all targeted MDM links are removed from the system, and their related Golden Resources are deleted and expunged from the server.
This operation takes a single optional Parameter.
@ -470,11 +463,11 @@ This operation takes a single optional Parameter.
</thead>
<tbody>
<tr>
<td>resourceType</td>
<td>sourceType</td>
<td>String</td>
<td>0..1</td>
<td>
The target Resource type you would like to clear. Currently limited to Patient/Practitioner. If omitted, will operate over all links.
The Source Resource type you would like to clear. If omitted, will operate over all links.
</td>
</tr>
</tbody>
@ -485,7 +478,7 @@ This operation takes a single optional Parameter.
Use an HTTP POST to the following URL to invoke this operation:
```url
http://example.com/$empi-clear
http://example.com/$mdm-clear
```
The following request body could be used:
@ -494,13 +487,13 @@ The following request body could be used:
{
"resourceType": "Parameters",
"parameter": [ {
"name": "resourceType",
"name": "sourceType",
"valueString": "Patient"
} ]
}
```
This operation returns the number of EMPI links that were cleared. The following is a sample response:
This operation returns the number of MDM links that were cleared. The following is a sample response:
```json
{
@ -512,13 +505,11 @@ This operation returns the number of EMPI links that were cleared. The following
}
```
## Batch-creating EMPI Links
## Batch-creating MDM Links
Call the `$empi-submit` operation to submit patients and practitioners for EMPI processing. In the rules-tuning phase of your setup, you can use `$empi-submit` to apply EMPI rules across multiple Resources.
An important thing to note is that this operation only submits the resources for processing. Actual EMPI processing is run asynchronously, and depending on the size
of the affected bundle of resources, may take some time to complete.
Call the `$mdm-submit` operation to submit patients and practitioners for MDM processing. In the rules-tuning phase of your setup, you can use `$mdm-submit` to apply MDM rules across multiple Resources. An important thing to note is that this operation only submits the resources for processing. Actual MDM processing is run asynchronously, and depending on the size of the affected bundle of resources, may take some time to complete.
After the operation is complete, all resources that matched the criteria will now have at least one empi link attached to them.
After the operation is complete, all resources that matched the criteria will now have at least one MDM link attached to them.
This operation takes a single optional criteria parameter unless it is called on a specific instance.
@ -549,9 +540,9 @@ This operation can be executed at the Server level, Resource level, or Instance
Use an HTTP POST to the following URL to invoke this operation with matching criteria:
```url
http://example.com/$empi-submit
http://example.com/Patient/$empi-submit
http://example.com/Practitioner/$empi-submit
http://example.com/$mdm-submit
http://example.com/Patient/$mdm-submit
http://example.com/Practitioner/$mdm-submit
```
The following request body could be used:
@ -565,7 +556,7 @@ The following request body could be used:
} ]
}
```
This operation returns the number of resources that were submitted for EMPI processing. The following is a sample response:
This operation returns the number of resources that were submitted for MDM processing. The following is a sample response:
```json
{
@ -577,10 +568,9 @@ This operation returns the number of resources that were submitted for EMPI proc
}
```
This operation can also be done at the Instance level. When this is the case, the operations accepts no parameters.
The following are examples of Instance level POSTs, which require no parameters.
This operation can also be done at the Instance level. When this is the case, the operations accepts no parameters. The following are examples of Instance level POSTs, which require no parameters.
```url
http://example.com/Patient/123/$empi-submit
http://example.com/Practitioner/456/$empi-submit
http://example.com/Patient/123/$mdm-submit
http://example.com/Practitioner/456/$mdm-submit
```

View File

@ -1,14 +1,15 @@
# Rules
HAPI EMPI rules are defined in a single json document.
HAPI MDM rules are defined in a single json document.
Note that in all the following configuration, valid options for `resourceType` are `Patient`, `Practitioner`, and `*`. Use `*` if the criteria is identical across both resource types and you would like to apply it to both practitioners and patients.
Note that in all the following configuration, valid options for `resourceType` include any supported resource, such as `Organization`, `Patient`, `Practitioner`, and `*`. Use `*` if the criteria is identical across both resource types and you would like to apply it to all resources.
Here is an example of a full HAPI EMPI rules json document:
Here is an example of a full HAPI MDM rules json document:
```json
{
"version": "1",
"mdmTypes" : ["Organization", "Patient", "Practitioner"],
"candidateSearchParams": [
{
"resourceType": "Patient",
@ -80,6 +81,14 @@ Here is an example of a full HAPI EMPI rules json document:
"algorithm": "JARO_WINKLER",
"matchThreshold": 0.80
}
},
{
"name": "org-name",
"resourceType": "Organization",
"resourcePath": "name",
"matcher": {
"algorithm": "STRING"
}
}
],
"matchResultMap": {
@ -88,7 +97,8 @@ Here is an example of a full HAPI EMPI rules json document:
"firstname-jaro,lastname-jaro,birthday": "POSSIBLE_MATCH",
"firstname-jaro,lastname-jaro,phone": "POSSIBLE_MATCH",
"lastname-jaro,phone,birthday": "POSSIBLE_MATCH",
"firstname-jaro,phone,birthday": "POSSIBLE_MATCH"
"firstname-jaro,phone,birthday": "POSSIBLE_MATCH",
"org-name": "MATCH"
}
}
```
@ -96,24 +106,28 @@ Here is an example of a full HAPI EMPI rules json document:
Here is a description of how each section of this document is configured.
### candidateSearchParams
These define fields which must have at least one exact match before two resources are considered for matching. This is like a list of "pre-searches" that find potential candidates for matches, to avoid the expensive operation of running a match score calculation on all resources in the system. E.g. you may only wish to consider matching two Patients if they either share at least one identifier in common or have the same birthday or the same phone number. The HAPI FHIR server executes each of these searches separately and then takes the union of the results, so you can think of these as `OR` criteria that cast a wide net for potential candidates. In some EMPI systems, these "pre-searches" are called "blocking" searches (since they identify "blocks" of candidates that will be searched for matches).
If a list of searchParams is specified in a given candidateSearchParams item, then these search parameters are treated as `AND` parameters. In the following candidateSearchParams definition, hapi-fhir
will extract given name, family name and identifiers from the incoming Patient and perform two separate
searches, first for all Patient resources that have the same given `AND` the same family name as the incoming Patient, and second for all Patient resources that share at least one identifier as the incoming Patient. Note that if the incoming Patient was missing any of these searchParam values, then that search would be skipped. E.g. if the incoming Patient had a given name but no family name, then only a search for matching identifiers would be performed.
These define fields which must have at least one exact match before two resources are considered for matching. This is like a list of "pre-searches" that find potential candidates for matches, to avoid the expensive operation of running a match score calculation on all resources in the system. E.g. you may only wish to consider matching two Patients if they either share at least one identifier in common or have the same birthday or the same phone number. The HAPI FHIR server executes each of these searches separately and then takes the union of the results, so you can think of these as `OR` criteria that cast a wide net for potential candidates. In some MDM systems, these "pre-searches" are called "blocking" searches (since they identify "blocks" of candidates that will be searched for matches).
If a list of searchParams is specified in a given candidateSearchParams item, then these search parameters are treated as `AND` parameters. In the following candidateSearchParams definition, hapi-fhir will extract given name, family name and identifiers from the incoming Patient and perform two separate searches, first for all Patient resources that have the same given `AND` the same family name as the incoming Patient, and second for all Patient resources that share at least one identifier as the incoming Patient. Note that if the incoming Patient was missing any of these searchParam values, then that search would be skipped. E.g. if the incoming Patient had a given name but no family name, then only a search for matching identifiers would be performed.
```json
"candidateSearchParams": [ {
"resourceType" : "Patient",
"searchParams" : ["given", "family"]
}, {
"resourceType" : "Patient",
"searchParam" : "identifier"
} ]
{
"candidateSearchParams" : [
{
"resourceType" : "Patient",
"searchParams" : ["given", "family"]
}, {
"resourceType" : "Patient",
"searchParam" : "identifier"
}
]
}
```
### candidateFilterSearchParams
When searching for match candidates, only resources that match this filter are considered. E.g. you may wish to only search for Patients for which active=true.
```json
[ {
"resourceType" : "Patient",
@ -140,6 +154,7 @@ For example, if the incoming patient looked like this:
"James"
]
}
]
}
```
@ -386,17 +401,17 @@ The following algorithms are currently supported:
### matchResultMap
These entries convert combinations of successful matchFields into an EMPI Match Result for overall matching of a given pair of resources. MATCH results are evaluated take precedence over POSSIBLE_MATCH results. If the incoming resource matches ALL of the named matchFields listed, then a new match link is created with the assigned matchResult (`MATCH` or `POSSIBLE_MATCH`).
These entries convert combinations of successful matchFields into an MDM Match Result for overall matching of a given pair of resources. MATCH results are evaluated take precedence over POSSIBLE_MATCH results. If the incoming resource matches ALL of the named matchFields listed, then a new match link is created with the assigned matchResult (`MATCH` or `POSSIBLE_MATCH`).
```json
{
"matchResultMap": {
"firstname-meta,lastname-meta,birthday": "MATCH",
"firstname-jaro,lastname-jaro,birthday": "POSSIBLE_MATCH",
"firstname-jaro,lastname-jaro,birthday": "POSSIBLE_MATCH"
}
}
```
### eidSystem
The external EID system that the HAPI EMPI system should expect to see on incoming Patient resources. Must be a valid URI. See [EMPI EID](/hapi-fhir/docs/server_jpa_empi/empi_eid.html) for details on how EIDs are managed by HAPI EMPI.
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

@ -33,7 +33,7 @@
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server-empi</artifactId>
<artifactId>hapi-fhir-server-mdm</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
@ -108,7 +108,7 @@
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-empi</artifactId>
<artifactId>hapi-fhir-jpaserver-mdm</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>

View File

@ -77,7 +77,7 @@
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server-empi</artifactId>
<artifactId>hapi-fhir-server-mdm</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>

View File

@ -20,8 +20,8 @@ package ca.uhn.fhir.jpa.dao.data;
* #L%
*/
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.jpa.entity.MdmLink;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
@ -29,12 +29,12 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface IEmpiLinkDao extends JpaRepository<EmpiLink, Long> {
public interface IMdmLinkDao extends JpaRepository<MdmLink, Long> {
@Modifying
@Query("DELETE FROM EmpiLink f WHERE myPersonPid = :pid OR myTargetPid = :pid")
@Query("DELETE FROM MdmLink f WHERE myGoldenResourcePid = :pid OR mySourcePid = :pid")
int deleteWithAnyReferenceToPid(@Param("pid") Long thePid);
@Modifying
@Query("DELETE FROM EmpiLink f WHERE (myPersonPid = :pid OR myTargetPid = :pid) AND myMatchResult <> :matchResult")
int deleteWithAnyReferenceToPidAndMatchResultNot(@Param("pid") Long thePid, @Param("matchResult")EmpiMatchResultEnum theMatchResult);
@Query("DELETE FROM MdmLink f WHERE (myGoldenResourcePid = :pid OR mySourcePid = :pid) AND myMatchResult <> :matchResult")
int deleteWithAnyReferenceToPidAndMatchResultNot(@Param("pid") Long thePid, @Param("matchResult") MdmMatchResultEnum theMatchResult);
}

View File

@ -54,8 +54,9 @@ public class ResourceTableFKProvider {
retval.add(new ResourceForeignKey("HFJ_SPIDX_TOKEN", "RES_ID"));
retval.add(new ResourceForeignKey("HFJ_SPIDX_URI", "RES_ID"));
retval.add(new ResourceForeignKey("HFJ_SUBSCRIPTION_STATS", "RES_ID"));
retval.add(new ResourceForeignKey("MPI_LINK", "PERSON_PID"));
retval.add(new ResourceForeignKey("MPI_LINK", "GOLDEN_RESOURCE_PID"));
retval.add(new ResourceForeignKey("MPI_LINK", "TARGET_PID"));
retval.add(new ResourceForeignKey("MPI_LINK", "PERSON_PID"));
retval.add(new ResourceForeignKey("NPM_PACKAGE_VER", "BINARY_RES_ID"));
retval.add(new ResourceForeignKey("NPM_PACKAGE_VER_RES", "BINARY_RES_ID"));
retval.add(new ResourceForeignKey("TRM_CODESYSTEM", "RES_ID"));

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.jpa.dao.empi;
package ca.uhn.fhir.jpa.dao.mdm;
/*-
* #%L
@ -20,9 +20,9 @@ package ca.uhn.fhir.jpa.dao.empi;
* #L%
*/
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.jpa.dao.data.IEmpiLinkDao;
import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -30,32 +30,33 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class EmpiLinkDeleteSvc {
private static final Logger ourLog = LoggerFactory.getLogger(EmpiLinkDeleteSvc.class);
public class MdmLinkDeleteSvc {
private static final Logger ourLog = LoggerFactory.getLogger(MdmLinkDeleteSvc.class);
@Autowired
private IEmpiLinkDao myEmpiLinkDao;
private IMdmLinkDao myMdmLinkDao;
@Autowired
private IdHelperService myIdHelperService;
/**
* Delete all EmpiLink records with any reference to this resource. (Used by Expunge.)
* Delete all {@link ca.uhn.fhir.jpa.entity.MdmLink} records with any reference to this resource. (Used by Expunge.)
* @param theResource
* @return the number of records deleted
*/
public int deleteWithAnyReferenceTo(IBaseResource theResource) {
Long pid = myIdHelperService.getPidOrThrowException(theResource.getIdElement());
int removed = myEmpiLinkDao.deleteWithAnyReferenceToPid(pid);
int removed = myMdmLinkDao.deleteWithAnyReferenceToPid(pid);
if (removed > 0) {
ourLog.info("Removed {} EMPI links with references to {}", removed, theResource.getIdElement().toVersionless());
ourLog.info("Removed {} MDM links with references to {}", removed, theResource.getIdElement().toVersionless());
}
return removed;
}
public int deleteNonRedirectWithWithAnyReferenceTo(IBaseResource theResource) {
public int deleteNonRedirectWithAnyReferenceTo(IBaseResource theResource) {
Long pid = myIdHelperService.getPidOrThrowException(theResource.getIdElement());
int removed = myEmpiLinkDao.deleteWithAnyReferenceToPidAndMatchResultNot(pid, EmpiMatchResultEnum.REDIRECT);
int removed = myMdmLinkDao.deleteWithAnyReferenceToPidAndMatchResultNot(pid, MdmMatchResultEnum.REDIRECT);
if (removed > 0) {
ourLog.info("Removed {} non-redirect EMPI links with references to {}", removed, theResource.getIdElement().toVersionless());
ourLog.info("Removed {} non-redirect MDM links with references to {}", removed, theResource.getIdElement().toVersionless());
}
return removed;
}

View File

@ -20,8 +20,8 @@ package ca.uhn.fhir.jpa.entity;
* #L%
*/
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import org.apache.commons.lang3.builder.ToStringBuilder;
@ -47,12 +47,11 @@ import java.util.Date;
@Table(name = "MPI_LINK", uniqueConstraints = {
@UniqueConstraint(name = "IDX_EMPI_PERSON_TGT", columnNames = {"PERSON_PID", "TARGET_PID"}),
})
public class EmpiLink {
public class MdmLink {
public static final int VERSION_LENGTH = 16;
private static final int MATCH_RESULT_LENGTH = 16;
private static final int LINK_SOURCE_LENGTH = 16;
public static final int TARGET_TYPE_LENGTH = 40;
public static final int SOURCE_TYPE_LENGTH = 40;
@SequenceGenerator(name = "SEQ_EMPI_LINK_ID", sequenceName = "SEQ_EMPI_LINK_ID")
@GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_EMPI_LINK_ID")
@ -60,27 +59,36 @@ public class EmpiLink {
@Column(name = "PID")
private Long myId;
@ManyToOne(optional = false, fetch = FetchType.LAZY, cascade = {})
@JoinColumn(name = "GOLDEN_RESOURCE_PID", referencedColumnName = "RES_ID", foreignKey = @ForeignKey(name = "FK_EMPI_LINK_GOLDEN_RESOURCE"), insertable=false, updatable=false, nullable=false)
private ResourceTable myGoldenResource;
@Column(name = "GOLDEN_RESOURCE_PID", nullable=false)
private Long myGoldenResourcePid;
@Deprecated
@ManyToOne(optional = false, fetch = FetchType.LAZY, cascade = {})
@JoinColumn(name = "PERSON_PID", referencedColumnName = "RES_ID", foreignKey = @ForeignKey(name = "FK_EMPI_LINK_PERSON"), insertable=false, updatable=false, nullable=false)
private ResourceTable myPerson;
@Deprecated
@Column(name = "PERSON_PID", nullable=false)
private Long myPersonPid;
@ManyToOne(optional = false, fetch = FetchType.LAZY, cascade = {})
@JoinColumn(name = "TARGET_PID", referencedColumnName = "RES_ID", foreignKey = @ForeignKey(name = "FK_EMPI_LINK_TARGET"), insertable=false, updatable=false, nullable=false)
private ResourceTable myTarget;
private ResourceTable mySource;
@Column(name = "TARGET_PID", updatable=false, nullable=false)
private Long myTargetPid;
private Long mySourcePid;
@Column(name = "MATCH_RESULT", nullable = false)
@Enumerated(EnumType.ORDINAL)
private EmpiMatchResultEnum myMatchResult;
private MdmMatchResultEnum myMatchResult;
@Column(name = "LINK_SOURCE", nullable = false)
@Enumerated(EnumType.ORDINAL)
private EmpiLinkSourceEnum myLinkSource;
private MdmLinkSourceEnum myLinkSource;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "CREATED", nullable = false)
@ -99,7 +107,7 @@ public class EmpiLink {
/** This link created a new person **/
@Column(name = "NEW_PERSON")
private Boolean myNewPerson;
private Boolean myHadToCreateNewGoldenResource;
@Column(name = "VECTOR")
private Long myVector;
@ -107,113 +115,132 @@ public class EmpiLink {
@Column(name = "SCORE")
private Double myScore;
public EmpiLink() {}
//TODO GGG GL-1340
@Column(name = "RULE_COUNT")
private Long myRuleCount;
public EmpiLink(String theVersion) {
public MdmLink() {}
public MdmLink(String theVersion) {
myVersion = theVersion;
}
@Column(name = "TARGET_TYPE", nullable = true, length = TARGET_TYPE_LENGTH)
private String myEmpiTargetType;
@Column(name = "TARGET_TYPE", nullable = true, length = SOURCE_TYPE_LENGTH)
private String myMdmSourceType;
public Long getId() {
return myId;
}
public EmpiLink setId(Long theId) {
public MdmLink setId(Long theId) {
myId = theId;
return this;
}
public ResourceTable getPerson() {
return myPerson;
public ResourceTable getGoldenResource() {
return myGoldenResource;
}
public EmpiLink setPerson(ResourceTable thePerson) {
myPerson = thePerson;
myPersonPid = thePerson.getId();
public MdmLink setGoldenResource(ResourceTable theGoldenResource) {
myGoldenResource = theGoldenResource;
myGoldenResourcePid = theGoldenResource.getId();
myPerson = theGoldenResource;
myPersonPid = theGoldenResource.getId();
return this;
}
public Long getPersonPid() {
return myPersonPid;
public Long getGoldenResourcePid() {
return myGoldenResourcePid;
}
public EmpiLink setPersonPid(Long thePersonPid) {
/**
* @deprecated Use {@link #setGoldenResourcePid(Long)} instead
*/
@Deprecated
public MdmLink setPersonPid(Long thePersonPid) {
myPersonPid = thePersonPid;
return this;
}
public ResourceTable getTarget() {
return myTarget;
}
public MdmLink setGoldenResourcePid(Long theGoldenResourcePid) {
setPersonPid(theGoldenResourcePid);
public EmpiLink setTarget(ResourceTable theTarget) {
myTarget = theTarget;
myTargetPid = theTarget.getId();
myGoldenResourcePid = theGoldenResourcePid;
return this;
}
public Long getTargetPid() {
return myTargetPid;
public ResourceTable getSource() {
return mySource;
}
public EmpiLink setTargetPid(Long theTargetPid) {
myTargetPid = theTargetPid;
public MdmLink setSource(ResourceTable theSource) {
mySource = theSource;
mySourcePid = theSource.getId();
return this;
}
public EmpiMatchResultEnum getMatchResult() {
public Long getSourcePid() {
return mySourcePid;
}
public MdmLink setSourcePid(Long theSourcePid) {
mySourcePid = theSourcePid;
return this;
}
public MdmMatchResultEnum getMatchResult() {
return myMatchResult;
}
public EmpiLink setMatchResult(EmpiMatchResultEnum theMatchResult) {
public MdmLink setMatchResult(MdmMatchResultEnum theMatchResult) {
myMatchResult = theMatchResult;
return this;
}
public boolean isNoMatch() {
return myMatchResult == EmpiMatchResultEnum.NO_MATCH;
return myMatchResult == MdmMatchResultEnum.NO_MATCH;
}
public boolean isMatch() {
return myMatchResult == EmpiMatchResultEnum.MATCH;
return myMatchResult == MdmMatchResultEnum.MATCH;
}
public boolean isPossibleMatch() {
return myMatchResult == EmpiMatchResultEnum.POSSIBLE_MATCH;
return myMatchResult == MdmMatchResultEnum.POSSIBLE_MATCH;
}
public boolean isRedirect() {
return myMatchResult == EmpiMatchResultEnum.REDIRECT;
return myMatchResult == MdmMatchResultEnum.REDIRECT;
}
public boolean isPossibleDuplicate() {
return myMatchResult == EmpiMatchResultEnum.POSSIBLE_DUPLICATE;
return myMatchResult == MdmMatchResultEnum.POSSIBLE_DUPLICATE;
}
public EmpiLinkSourceEnum getLinkSource() {
public MdmLinkSourceEnum getLinkSource() {
return myLinkSource;
}
public EmpiLink setLinkSource(EmpiLinkSourceEnum theLinkSource) {
public MdmLink setLinkSource(MdmLinkSourceEnum theLinkSource) {
myLinkSource = theLinkSource;
return this;
}
public boolean isAuto() {
return myLinkSource == EmpiLinkSourceEnum.AUTO;
return myLinkSource == MdmLinkSourceEnum.AUTO;
}
public boolean isManual() {
return myLinkSource == EmpiLinkSourceEnum.MANUAL;
return myLinkSource == MdmLinkSourceEnum.MANUAL;
}
public Date getCreated() {
return myCreated;
}
public EmpiLink setCreated(Date theCreated) {
public MdmLink setCreated(Date theCreated) {
myCreated = theCreated;
return this;
}
@ -222,7 +249,7 @@ public class EmpiLink {
return myUpdated;
}
public EmpiLink setUpdated(Date theUpdated) {
public MdmLink setUpdated(Date theUpdated) {
myUpdated = theUpdated;
return this;
}
@ -231,7 +258,7 @@ public class EmpiLink {
return myVersion;
}
public EmpiLink setVersion(String theVersion) {
public MdmLink setVersion(String theVersion) {
myVersion = theVersion;
return this;
}
@ -240,7 +267,7 @@ public class EmpiLink {
return myVector;
}
public EmpiLink setVector(Long theVector) {
public MdmLink setVector(Long theVector) {
myVector = theVector;
return this;
}
@ -249,7 +276,7 @@ public class EmpiLink {
return myScore;
}
public EmpiLink setScore(Double theScore) {
public MdmLink setScore(Double theScore) {
myScore = theScore;
return this;
}
@ -262,26 +289,22 @@ public class EmpiLink {
return myEidMatch != null && myEidMatch;
}
public EmpiLink setEidMatch(Boolean theEidMatch) {
public MdmLink setEidMatch(Boolean theEidMatch) {
myEidMatch = theEidMatch;
return this;
}
public Boolean getNewPerson() {
return myNewPerson;
public boolean getHadToCreateNewGoldenResource() {
return myHadToCreateNewGoldenResource != null && myHadToCreateNewGoldenResource;
}
public boolean isNewPerson() {
return myNewPerson != null && myNewPerson;
}
public EmpiLink setNewPerson(Boolean theNewPerson) {
myNewPerson = theNewPerson;
public MdmLink setHadToCreateNewGoldenResource(Boolean theHadToCreateNewResource) {
myHadToCreateNewGoldenResource = theHadToCreateNewResource;
return this;
}
public EmpiLink setEmpiTargetType(String theEmpiTargetType) {
myEmpiTargetType = theEmpiTargetType;
public MdmLink setMdmSourceType(String mdmSourceType) {
myMdmSourceType = mdmSourceType;
return this;
}
@ -289,18 +312,28 @@ public class EmpiLink {
public String toString() {
return new ToStringBuilder(this)
.append("myId", myId)
.append("myPersonPid", myPersonPid)
.append("myTargetPid", myTargetPid)
.append("myEmpiTargetType", myEmpiTargetType)
.append("myGoldenResource", myGoldenResourcePid)
.append("mySourcePid", mySourcePid)
.append("myMdmSourceType", myMdmSourceType)
.append("myMatchResult", myMatchResult)
.append("myLinkSource", myLinkSource)
.append("myEidMatch", myEidMatch)
.append("myNewPerson", myNewPerson)
.append("myHadToCreateNewResource", myHadToCreateNewGoldenResource)
.append("myScore", myScore)
.append("myRuleCount", myRuleCount)
.toString();
}
public String getEmpiTargetType() {
return myEmpiTargetType;
public String getMdmSourceType() {
return myMdmSourceType;
}
public Long getRuleCount() {
return myRuleCount;
}
public void setRuleCount(Long theRuleCount) {
myRuleCount = theRuleCount;
}
}

View File

@ -1,230 +0,0 @@
package ca.uhn.fhir.jpa.empi.config;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.IEmpiControllerSvc;
import ca.uhn.fhir.empi.api.IEmpiExpungeSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.provider.EmpiControllerHelper;
import ca.uhn.fhir.empi.provider.EmpiProviderLoader;
import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator;
import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc;
import ca.uhn.fhir.empi.util.EIDHelper;
import ca.uhn.fhir.empi.util.PersonHelper;
import ca.uhn.fhir.jpa.dao.empi.EmpiLinkDeleteSvc;
import ca.uhn.fhir.jpa.empi.broker.EmpiMessageHandler;
import ca.uhn.fhir.jpa.empi.broker.EmpiQueueConsumerLoader;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkFactory;
import ca.uhn.fhir.jpa.empi.interceptor.EmpiStorageInterceptor;
import ca.uhn.fhir.jpa.empi.interceptor.IEmpiStorageInterceptor;
import ca.uhn.fhir.jpa.empi.svc.EmpiClearSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiControllerSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiEidUpdateService;
import ca.uhn.fhir.jpa.empi.svc.EmpiLinkQuerySvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiLinkSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiLinkUpdaterSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiMatchFinderSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiMatchLinkSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiPersonDeletingSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiPersonMergerSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiResourceDaoSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiResourceFilteringSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiCandidateSearchCriteriaBuilderSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiCandidateSearchSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiPersonFindingSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.FindCandidateByEidSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.FindCandidateByLinkSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.FindCandidateByScoreSvc;
import ca.uhn.fhir.rest.server.util.ISearchParamRetriever;
import ca.uhn.fhir.validation.IResourceLoader;
import org.slf4j.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class EmpiConsumerConfig {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Bean
IEmpiStorageInterceptor empiStorageInterceptor() {
return new EmpiStorageInterceptor();
}
@Bean
EmpiQueueConsumerLoader empiQueueConsumerLoader() {
return new EmpiQueueConsumerLoader();
}
@Bean
EmpiMessageHandler empiMessageHandler() {
return new EmpiMessageHandler();
}
@Bean
EmpiMatchLinkSvc empiMatchLinkSvc() {
return new EmpiMatchLinkSvc();
}
@Bean
EmpiEidUpdateService eidUpdateService() {
return new EmpiEidUpdateService();
}
@Bean
EmpiResourceDaoSvc empiResourceDaoSvc() {
return new EmpiResourceDaoSvc();
}
@Bean
IEmpiLinkSvc empiLinkSvc() {
return new EmpiLinkSvcImpl();
}
@Bean
PersonHelper personHelper(FhirContext theFhirContext) {
return new PersonHelper(theFhirContext);
}
@Bean
EmpiSubscriptionLoader empiSubscriptionLoader() {
return new EmpiSubscriptionLoader();
}
@Bean
EmpiSearchParameterLoader empiSearchParameterLoader() {
return new EmpiSearchParameterLoader();
}
@Bean
EmpiPersonFindingSvc empiPersonFindingSvc() {
return new EmpiPersonFindingSvc();
}
@Bean
FindCandidateByEidSvc findCandidateByEidSvc() {
return new FindCandidateByEidSvc();
}
@Bean
FindCandidateByLinkSvc findCandidateByLinkSvc() {
return new FindCandidateByLinkSvc();
}
@Bean
FindCandidateByScoreSvc findCandidateByScoreSvc() {
return new FindCandidateByScoreSvc();
}
@Bean
EmpiProviderLoader empiProviderLoader() {
return new EmpiProviderLoader();
}
@Bean
EmpiRuleValidator empiRuleValidator(FhirContext theFhirContext, ISearchParamRetriever theSearchParamRetriever) {
return new EmpiRuleValidator(theFhirContext, theSearchParamRetriever);
}
@Bean
IEmpiMatchFinderSvc empiMatchFinderSvc() {
return new EmpiMatchFinderSvcImpl();
}
@Bean
IEmpiPersonMergerSvc empiPersonMergerSvc() {
return new EmpiPersonMergerSvcImpl();
}
@Bean
IEmpiLinkQuerySvc empiLinkQuerySvc() {
return new EmpiLinkQuerySvcImpl();
}
@Bean
IEmpiExpungeSvc empiResetSvc(EmpiLinkDaoSvc theEmpiLinkDaoSvc, EmpiPersonDeletingSvc theEmpiPersonDeletingSvcImpl ) {
return new EmpiClearSvcImpl(theEmpiLinkDaoSvc, theEmpiPersonDeletingSvcImpl);
}
@Bean
EmpiCandidateSearchSvc empiCandidateSearchSvc() {
return new EmpiCandidateSearchSvc();
}
@Bean
EmpiCandidateSearchCriteriaBuilderSvc empiCriteriaBuilderSvc() {
return new EmpiCandidateSearchCriteriaBuilderSvc();
}
@Bean
EmpiResourceMatcherSvc empiResourceComparatorSvc(FhirContext theFhirContext, IEmpiSettings theEmpiConfig) {
return new EmpiResourceMatcherSvc(theFhirContext, theEmpiConfig);
}
@Bean
EIDHelper eidHelper(FhirContext theFhirContext, IEmpiSettings theEmpiConfig) {
return new EIDHelper(theFhirContext, theEmpiConfig);
}
@Bean
EmpiLinkDaoSvc empiLinkDaoSvc() {
return new EmpiLinkDaoSvc();
}
@Bean
EmpiLinkFactory empiLinkFactory(IEmpiSettings theEmpiSettings) {
return new EmpiLinkFactory(theEmpiSettings);
}
@Bean
IEmpiLinkUpdaterSvc manualLinkUpdaterSvc() {
return new EmpiLinkUpdaterSvcImpl();
}
@Bean
EmpiLoader empiLoader() {
return new EmpiLoader();
}
@Bean
EmpiLinkDeleteSvc empiLinkDeleteSvc() {
return new EmpiLinkDeleteSvc();
}
@Bean
EmpiResourceFilteringSvc empiResourceFilteringSvc() {
return new EmpiResourceFilteringSvc();
}
@Bean
EmpiControllerHelper empiProviderHelper(FhirContext theFhirContext, IResourceLoader theResourceLoader) { return new EmpiControllerHelper(theFhirContext, theResourceLoader); }
@Bean
IEmpiControllerSvc empiControllerSvc() {return new EmpiControllerSvcImpl(); }
}

View File

@ -1,115 +0,0 @@
package ca.uhn.fhir.jpa.empi.config;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.SearchParameter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class EmpiSearchParameterLoader {
public static final String EMPI_PERSON_ASSURANCE_SEARCH_PARAMETER_ID = "person-assurance";
public static final String EMPI_PERSON_ACTIVE_SEARCH_PARAMETER_ID = "person-active";
@Autowired
public FhirContext myFhirContext;
@Autowired
public DaoRegistry myDaoRegistry;
synchronized public void daoUpdateEmpiSearchParameters() {
IBaseResource personAssurance;
IBaseResource personActive;
switch (myFhirContext.getVersion().getVersion()) {
case DSTU3:
personAssurance = buildAssuranceEmpiSearchParameterDstu3();
personActive = buildActiveEmpiSearchParameterDstu3();
break;
case R4:
personAssurance = buildAssuranceEmpiSearchParameterR4();
personActive = buildActiveEmpiSearchParameterR4();
break;
default:
throw new ConfigurationException("EMPI not supported for FHIR version " + myFhirContext.getVersion().getVersion());
}
IFhirResourceDao<IBaseResource> searchParameterDao = myDaoRegistry.getResourceDao("SearchParameter");
searchParameterDao.update(personAssurance);
searchParameterDao.update(personActive);
}
private org.hl7.fhir.dstu3.model.SearchParameter buildAssuranceEmpiSearchParameterDstu3() {
org.hl7.fhir.dstu3.model.SearchParameter retval = new org.hl7.fhir.dstu3.model.SearchParameter();
retval.setId(EMPI_PERSON_ASSURANCE_SEARCH_PARAMETER_ID);
retval.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE);
retval.getMeta().addTag().setSystem(EmpiConstants.SYSTEM_EMPI_MANAGED).setCode(EmpiConstants.CODE_HAPI_EMPI_MANAGED);
retval.setCode("assurance");
retval.addBase("Person");
retval.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
retval.setDescription("The assurance level of the link on a Person");
retval.setExpression("Person.link.assurance");
return retval;
}
private SearchParameter buildAssuranceEmpiSearchParameterR4() {
SearchParameter retval = new SearchParameter();
retval.setId(EMPI_PERSON_ASSURANCE_SEARCH_PARAMETER_ID);
retval.setStatus(Enumerations.PublicationStatus.ACTIVE);
retval.getMeta().addTag().setSystem(EmpiConstants.SYSTEM_EMPI_MANAGED).setCode(EmpiConstants.CODE_HAPI_EMPI_MANAGED);
retval.setCode("assurance");
retval.addBase("Person");
retval.setType(Enumerations.SearchParamType.TOKEN);
retval.setDescription("The assurance level of the link on a Person");
retval.setExpression("Person.link.assurance");
return retval;
}
private org.hl7.fhir.dstu3.model.SearchParameter buildActiveEmpiSearchParameterDstu3() {
org.hl7.fhir.dstu3.model.SearchParameter retval = new org.hl7.fhir.dstu3.model.SearchParameter();
retval.setId(EMPI_PERSON_ACTIVE_SEARCH_PARAMETER_ID);
retval.setStatus(org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus.ACTIVE);
retval.getMeta().addTag().setSystem(EmpiConstants.SYSTEM_EMPI_MANAGED).setCode(EmpiConstants.CODE_HAPI_EMPI_MANAGED);
retval.setCode("active");
retval.addBase("Person");
retval.setType(org.hl7.fhir.dstu3.model.Enumerations.SearchParamType.TOKEN);
retval.setDescription("The active status of a Person");
retval.setExpression("Person.active");
return retval;
}
private SearchParameter buildActiveEmpiSearchParameterR4() {
SearchParameter retval = new SearchParameter();
retval.setId(EMPI_PERSON_ACTIVE_SEARCH_PARAMETER_ID);
retval.setStatus(Enumerations.PublicationStatus.ACTIVE);
retval.getMeta().addTag().setSystem(EmpiConstants.SYSTEM_EMPI_MANAGED).setCode(EmpiConstants.CODE_HAPI_EMPI_MANAGED);
retval.setCode("active");
retval.addBase("Person");
retval.setType(Enumerations.SearchParamType.TOKEN);
retval.setDescription("The active status of a Person");
retval.setExpression("Person.active");
return retval;
}
}

View File

@ -1,76 +0,0 @@
package ca.uhn.fhir.jpa.empi.config;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.IEmpiChannelSubmitterSvc;
import ca.uhn.fhir.empi.api.IEmpiSubmitSvc;
import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator;
import ca.uhn.fhir.jpa.dao.empi.EmpiLinkDeleteSvc;
import ca.uhn.fhir.jpa.empi.interceptor.EmpiSubmitterInterceptorLoader;
import ca.uhn.fhir.jpa.empi.svc.EmpiChannelSubmitterSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiPersonDeletingSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiSearchParamSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiSubmitSvcImpl;
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@Configuration
public class EmpiSubmitterConfig {
@Bean
EmpiSubmitterInterceptorLoader empiSubmitterInterceptorLoader() {
return new EmpiSubmitterInterceptorLoader();
}
@Bean
EmpiSearchParamSvc empiSearchParamSvc() {
return new EmpiSearchParamSvc();
}
@Bean
EmpiRuleValidator empiRuleValidator(FhirContext theFhirContext) {
return new EmpiRuleValidator(theFhirContext, empiSearchParamSvc());
}
@Bean
EmpiLinkDeleteSvc empiLinkDeleteSvc() {
return new EmpiLinkDeleteSvc();
}
@Bean
EmpiPersonDeletingSvc empiPersonDeletingSvc() {
return new EmpiPersonDeletingSvc();
}
@Bean
@Lazy
IEmpiChannelSubmitterSvc empiChannelSubmitterSvc(FhirContext theFhirContext, IChannelFactory theChannelFactory) {
return new EmpiChannelSubmitterSvcImpl(theFhirContext, theChannelFactory);
}
@Bean
IEmpiSubmitSvc empiBatchService() {
return new EmpiSubmitSvcImpl();
}
}

View File

@ -1,319 +0,0 @@
package ca.uhn.fhir.jpa.empi.dao;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.jpa.dao.data.IEmpiLinkDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
public class EmpiLinkDaoSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private IEmpiLinkDao myEmpiLinkDao;
@Autowired
private EmpiLinkFactory myEmpiLinkFactory;
@Autowired
private IdHelperService myIdHelperService;
@Autowired
private FhirContext myFhirContext;
@Transactional
public EmpiLink createOrUpdateLinkEntity(IBaseResource thePerson, IBaseResource theTarget, EmpiMatchOutcome theMatchOutcome, EmpiLinkSourceEnum theLinkSource, @Nullable EmpiTransactionContext theEmpiTransactionContext) {
Long personPid = myIdHelperService.getPidOrNull(thePerson);
Long resourcePid = myIdHelperService.getPidOrNull(theTarget);
EmpiLink empiLink = getOrCreateEmpiLinkByPersonPidAndTargetPid(personPid, resourcePid);
empiLink.setLinkSource(theLinkSource);
empiLink.setMatchResult(theMatchOutcome.getMatchResultEnum());
// Preserve these flags for link updates
empiLink.setEidMatch(theMatchOutcome.isEidMatch() | empiLink.isEidMatch());
empiLink.setNewPerson(theMatchOutcome.isNewPerson() | empiLink.isNewPerson());
empiLink.setEmpiTargetType(myFhirContext.getResourceType(theTarget));
if (empiLink.getScore() != null) {
empiLink.setScore(Math.max(theMatchOutcome.score, empiLink.getScore()));
} else {
empiLink.setScore(theMatchOutcome.score);
}
String message = String.format("Creating EmpiLink from %s to %s -> %s", thePerson.getIdElement().toUnqualifiedVersionless(), theTarget.getIdElement().toUnqualifiedVersionless(), theMatchOutcome);
theEmpiTransactionContext.addTransactionLogMessage(message);
ourLog.debug(message);
save(empiLink);
return empiLink;
}
@Nonnull
public EmpiLink getOrCreateEmpiLinkByPersonPidAndTargetPid(Long thePersonPid, Long theResourcePid) {
Optional<EmpiLink> oExisting = getLinkByPersonPidAndTargetPid(thePersonPid, theResourcePid);
if (oExisting.isPresent()) {
return oExisting.get();
} else {
EmpiLink newLink = myEmpiLinkFactory.newEmpiLink();
newLink.setPersonPid(thePersonPid);
newLink.setTargetPid(theResourcePid);
return newLink;
}
}
public Optional<EmpiLink> getLinkByPersonPidAndTargetPid(Long thePersonPid, Long theTargetPid) {
if (theTargetPid == null || thePersonPid == null) {
return Optional.empty();
}
EmpiLink link = myEmpiLinkFactory.newEmpiLink();
link.setTargetPid(theTargetPid);
link.setPersonPid(thePersonPid);
Example<EmpiLink> example = Example.of(link);
return myEmpiLinkDao.findOne(example);
}
/**
* Given a Target Pid, and a match result, return all links that match these criteria.
*
* @param theTargetPid the target of the relationship.
* @param theMatchResult the Match Result of the relationship
*
* @return a list of {@link EmpiLink} entities matching these criteria.
*/
public List<EmpiLink> getEmpiLinksByTargetPidAndMatchResult(Long theTargetPid, EmpiMatchResultEnum theMatchResult) {
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink();
exampleLink.setTargetPid(theTargetPid);
exampleLink.setMatchResult(theMatchResult);
Example<EmpiLink> example = Example.of(exampleLink);
return myEmpiLinkDao.findAll(example);
}
/**
* Given a target Pid, return its Matched EmpiLink. There can only ever be at most one of these, but its possible
* the target has no matches, and may return an empty optional.
*
* @param theTargetPid The Pid of the target you wish to find the matching link for.
* @return the {@link EmpiLink} that contains the Match information for the target.
*/
public Optional<EmpiLink> getMatchedLinkForTargetPid(Long theTargetPid) {
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink();
exampleLink.setTargetPid(theTargetPid);
exampleLink.setMatchResult(EmpiMatchResultEnum.MATCH);
Example<EmpiLink> example = Example.of(exampleLink);
return myEmpiLinkDao.findOne(example);
}
/**
* Given an IBaseResource, return its Matched EmpiLink. There can only ever be at most one of these, but its possible
* the target has no matches, and may return an empty optional.
*
* @param theTarget The IBaseResource representing the target you wish to find the matching link for.
* @return the {@link EmpiLink} that contains the Match information for the target.
*/
public Optional<EmpiLink> getMatchedLinkForTarget(IBaseResource theTarget) {
Long pid = myIdHelperService.getPidOrNull(theTarget);
if (pid == null) {
return Optional.empty();
}
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink();
exampleLink.setTargetPid(pid);
exampleLink.setMatchResult(EmpiMatchResultEnum.MATCH);
Example<EmpiLink> example = Example.of(exampleLink);
return myEmpiLinkDao.findOne(example);
}
/**
* Given a person a target and a match result, return the matching EmpiLink, if it exists.
*
* @param thePersonPid The Pid of the Person in the relationship
* @param theTargetPid The Pid of the target in the relationship
* @param theMatchResult The MatchResult you are looking for.
*
* @return an Optional {@link EmpiLink} containing the matched link if it exists.
*/
public Optional<EmpiLink> getEmpiLinksByPersonPidTargetPidAndMatchResult(Long thePersonPid, Long theTargetPid, EmpiMatchResultEnum theMatchResult) {
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink();
exampleLink.setPersonPid(thePersonPid);
exampleLink.setTargetPid(theTargetPid);
exampleLink.setMatchResult(theMatchResult);
Example<EmpiLink> example = Example.of(exampleLink);
return myEmpiLinkDao.findOne(example);
}
/**
* Get all {@link EmpiLink} which have {@link EmpiMatchResultEnum#POSSIBLE_DUPLICATE} as their match result.
*
* @return A list of EmpiLinks that hold potential duplicate persons.
*/
public List<EmpiLink> getPossibleDuplicates() {
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink();
exampleLink.setMatchResult(EmpiMatchResultEnum.POSSIBLE_DUPLICATE);
Example<EmpiLink> example = Example.of(exampleLink);
return myEmpiLinkDao.findAll(example);
}
public Optional<EmpiLink> findEmpiLinkByTarget(IBaseResource theTargetResource) {
@Nullable Long pid = myIdHelperService.getPidOrNull(theTargetResource);
if (pid == null) {
return Optional.empty();
}
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink().setTargetPid(pid);
Example<EmpiLink> example = Example.of(exampleLink);
return myEmpiLinkDao.findOne(example);
}
/**
* Delete a given EmpiLink. Note that this does not clear out the Person, or the Person's related links.
* It is a simple entity delete.
*
* @param theEmpiLink the EmpiLink to delete.
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void deleteLink(EmpiLink theEmpiLink) {
myEmpiLinkDao.delete(theEmpiLink);
}
/**
* Given a Person, return all links in which they are the source Person of the {@link EmpiLink}
*
* @param thePersonResource The {@link IBaseResource} Person who's links you would like to retrieve.
*
* @return A list of all {@link EmpiLink} entities in which thePersonResource is the source Person.
*/
public List<EmpiLink> findEmpiLinksByPerson(IBaseResource thePersonResource) {
Long pid = myIdHelperService.getPidOrNull(thePersonResource);
if (pid == null) {
return Collections.emptyList();
}
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink().setPersonPid(pid);
Example<EmpiLink> example = Example.of(exampleLink);
return myEmpiLinkDao.findAll(example);
}
/**
* Delete all {@link EmpiLink} entities, and return all resource PIDs from the source of the relationship.
*
* @return A list of Long representing the related Person Pids.
*/
@Transactional
public List<Long> deleteAllEmpiLinksAndReturnPersonPids() {
List<EmpiLink> all = myEmpiLinkDao.findAll();
return deleteEmpiLinksAndReturnPersonPids(all);
}
private List<Long> deleteEmpiLinksAndReturnPersonPids(List<EmpiLink> theLinks) {
Set<Long> persons = theLinks.stream().map(EmpiLink::getPersonPid).collect(Collectors.toSet());
persons.addAll(theLinks.stream().filter(link -> "Person".equals(link.getEmpiTargetType())).map(EmpiLink::getTargetPid).collect(Collectors.toSet()));
ourLog.info("Deleting {} EMPI link records...", theLinks.size());
myEmpiLinkDao.deleteAll(theLinks);
ourLog.info("{} EMPI link records deleted", theLinks.size());
return new ArrayList<>(persons);
}
/**
* Given a valid {@link String}, delete all {@link EmpiLink} entities for that type, and get the Pids
* for the Person resources which were the sources of the links.
*
* @param theTargetType the type of relationship you would like to delete.
*
* @return A list of longs representing the Pids of the Person resources used as the sources of the relationships that were deleted.
*/
public List<Long> deleteAllEmpiLinksOfTypeAndReturnPersonPids(String theTargetType) {
EmpiLink link = new EmpiLink();
link.setEmpiTargetType(theTargetType);
Example<EmpiLink> exampleLink = Example.of(link);
List<EmpiLink> allOfType = myEmpiLinkDao.findAll(exampleLink);
return deleteEmpiLinksAndReturnPersonPids(allOfType);
}
/**
* Persist an EmpiLink to the database.
*
* @param theEmpiLink the link to save.
*
* @return the persisted {@link EmpiLink} entity.
*/
public EmpiLink save(EmpiLink theEmpiLink) {
if (theEmpiLink.getCreated() == null) {
theEmpiLink.setCreated(new Date());
}
theEmpiLink.setUpdated(new Date());
return myEmpiLinkDao.save(theEmpiLink);
}
/**
* Given an example {@link EmpiLink}, return all links from the database which match the example.
*
* @param theExampleLink The EmpiLink containing the data we would like to search for.
*
* @return a list of {@link EmpiLink} entities which match the example.
*/
public List<EmpiLink> findEmpiLinkByExample(Example<EmpiLink> theExampleLink) {
return myEmpiLinkDao.findAll(theExampleLink);
}
/**
* Given a target {@link IBaseResource}, return all {@link EmpiLink} entities in which this target is the target
* of the relationship. This will show you all links for a given Patient/Practitioner.
*
* @param theTargetResource the target resource to find links for.
*
* @return all links for the target.
*/
public List<EmpiLink> findEmpiLinksByTarget(IBaseResource theTargetResource) {
Long pid = myIdHelperService.getPidOrNull(theTargetResource);
if (pid == null) {
return Collections.emptyList();
}
EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink().setTargetPid(pid);
Example<EmpiLink> example = Example.of(exampleLink);
return myEmpiLinkDao.findAll(example);
}
/**
* Factory delegation method, whenever you need a new EmpiLink, use this factory method.
* //TODO Should we make the constructor private for EmpiLink? or work out some way to ensure they can only be instantiated via factory.
* @return A new {@link EmpiLink}.
*/
public EmpiLink newEmpiLink() {
return myEmpiLinkFactory.newEmpiLink();
}
}

View File

@ -1,190 +0,0 @@
package ca.uhn.fhir.jpa.empi.interceptor;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.model.CanonicalEID;
import ca.uhn.fhir.empi.util.EIDHelper;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.empi.util.PersonHelper;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.dao.empi.EmpiLinkDeleteSvc;
import ca.uhn.fhir.jpa.dao.expunge.ExpungeEverythingService;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
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 java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class EmpiStorageInterceptor implements IEmpiStorageInterceptor {
private static final Logger ourLog = LoggerFactory.getLogger(EmpiStorageInterceptor.class);
@Autowired
private ExpungeEverythingService myExpungeEverythingService;
@Autowired
private EmpiLinkDeleteSvc myEmpiLinkDeleteSvc;
@Autowired
private FhirContext myFhirContext;
@Autowired
private EIDHelper myEIDHelper;
@Autowired
private IEmpiSettings myEmpiSettings;
@Autowired
private PersonHelper myPersonHelper;
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
public void blockManualPersonManipulationOnCreate(IBaseResource theBaseResource, RequestDetails theRequestDetails, ServletRequestDetails theServletRequestDetails) {
//If running in single EID mode, forbid multiple eids.
if (myEmpiSettings.isPreventMultipleEids()) {
forbidIfHasMultipleEids(theBaseResource);
}
// TODO EMPI find a better way to identify EMPI calls
if (isInternalRequest(theRequestDetails)) {
return;
}
forbidIfEmpiManagedTagIsPresent(theBaseResource);
}
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
public void blockManualPersonManipulationOnUpdate(IBaseResource theOldResource, IBaseResource theNewResource, RequestDetails theRequestDetails, ServletRequestDetails theServletRequestDetails) {
//If running in single EID mode, forbid multiple eids.
if (myEmpiSettings.isPreventMultipleEids()) {
forbidIfHasMultipleEids(theNewResource);
}
if (EmpiUtil.isEmpiManagedPerson(myFhirContext, theNewResource) &&
myPersonHelper.isDeactivated(theNewResource)) {
ourLog.debug("Deleting empi links to deactivated Person {}", theNewResource.getIdElement().toUnqualifiedVersionless());
int deleted = myEmpiLinkDeleteSvc.deleteNonRedirectWithWithAnyReferenceTo(theNewResource);
if (deleted > 0) {
ourLog.debug("Deleted {} empi links", deleted);
}
}
if (isInternalRequest(theRequestDetails)) {
return;
}
forbidIfEmpiManagedTagIsPresent(theOldResource);
forbidModifyingEmpiTag(theNewResource, theOldResource);
if (myEmpiSettings.isPreventEidUpdates()) {
forbidIfModifyingExternalEidOnTarget(theNewResource, theOldResource);
}
}
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED)
public void deleteEmpiLinks(RequestDetails theRequest, IBaseResource theResource) {
if (!EmpiUtil.isEmpiResourceType(myFhirContext, theResource)) {
return;
}
myEmpiLinkDeleteSvc.deleteWithAnyReferenceTo(theResource);
}
private void forbidIfModifyingExternalEidOnTarget(IBaseResource theNewResource, IBaseResource theOldResource) {
List<CanonicalEID> newExternalEids = myEIDHelper.getExternalEid(theNewResource);
List<CanonicalEID> oldExternalEids = myEIDHelper.getExternalEid(theOldResource);
if (oldExternalEids.isEmpty()) {
return;
}
if (!myEIDHelper.eidMatchExists(newExternalEids, oldExternalEids)) {
throwBlockEidChange();
}
}
private void throwBlockEidChange() {
throw new ForbiddenOperationException("While running with EID updates disabled, EIDs may not be updated on Patient/Practitioner resources");
}
/*
* Will throw a forbidden error if a request attempts to add/remove the EMPI tag on a Person.
*/
private void forbidModifyingEmpiTag(IBaseResource theNewResource, IBaseResource theOldResource) {
if (EmpiUtil.isEmpiManaged(theNewResource) != EmpiUtil.isEmpiManaged(theOldResource)) {
throwBlockEmpiManagedTagChange();
}
}
private void forbidIfHasMultipleEids(IBaseResource theResource) {
String resourceType = extractResourceType(theResource);
if (resourceType.equalsIgnoreCase("Patient") || resourceType.equalsIgnoreCase("Practitioner")) {
if (myEIDHelper.getExternalEid(theResource).size() > 1) {
throwBlockMultipleEids();
}
}
}
/*
* We assume that if we have RequestDetails, then this was an HTTP request and not an internal one.
*/
private boolean isInternalRequest(RequestDetails theRequestDetails) {
return theRequestDetails == null;
}
private void forbidIfEmpiManagedTagIsPresent(IBaseResource theResource) {
if (EmpiUtil.isEmpiManaged(theResource)) {
throwModificationBlockedByEmpi();
}
}
private void throwBlockEmpiManagedTagChange() {
throw new ForbiddenOperationException("The " + EmpiConstants.CODE_HAPI_EMPI_MANAGED + " tag on a resource may not be changed once created.");
}
private void throwModificationBlockedByEmpi() {
throw new ForbiddenOperationException("Cannot create or modify Resources that are managed by EMPI.");
}
private void throwBlockMultipleEids() {
throw new ForbiddenOperationException("While running with multiple EIDs disabled, Patient/Practitioner resources may have at most one EID.");
}
private String extractResourceType(IBaseResource theResource) {
return myFhirContext.getResourceType(theResource);
}
@Hook(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING)
public void expungeAllEmpiLinks(AtomicInteger theCounter) {
ourLog.debug("Expunging all EmpiLink records");
theCounter.addAndGet(myExpungeEverythingService.expungeEverythingByType(EmpiLink.class));
}
@Hook(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE)
public void expungeAllMatchedEmpiLinks(AtomicInteger theCounter, IBaseResource theResource) {
ourLog.debug("Expunging EmpiLink records with reference to {}", theResource.getIdElement());
theCounter.addAndGet(myEmpiLinkDeleteSvc.deleteWithAnyReferenceTo(theResource));
}
}

View File

@ -1,77 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.empi.api.IEmpiExpungeSvc;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
/**
* This class is responsible for clearing out existing EMPI links, as well as deleting all persons related to those EMPI Links.
*
*/
public class EmpiClearSvcImpl implements IEmpiExpungeSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
final EmpiLinkDaoSvc myEmpiLinkDaoSvc;
final EmpiPersonDeletingSvc myEmpiPersonDeletingSvcImpl;
@Autowired
public EmpiClearSvcImpl(EmpiLinkDaoSvc theEmpiLinkDaoSvc, EmpiPersonDeletingSvc theEmpiPersonDeletingSvcImpl) {
myEmpiLinkDaoSvc = theEmpiLinkDaoSvc;
myEmpiPersonDeletingSvcImpl = theEmpiPersonDeletingSvcImpl;
}
@Override
public long expungeAllEmpiLinksOfTargetType(String theResourceType, ServletRequestDetails theRequestDetails) {
throwExceptionIfInvalidTargetType(theResourceType);
ourLog.info("Clearing all EMPI Links for resource type {}...", theResourceType);
List<Long> personPids = myEmpiLinkDaoSvc.deleteAllEmpiLinksOfTypeAndReturnPersonPids(theResourceType);
DeleteMethodOutcome deleteOutcome = myEmpiPersonDeletingSvcImpl.expungePersonPids(personPids, theRequestDetails);
ourLog.info("EMPI clear operation complete. Removed {} EMPI links and {} Person resources.", personPids.size(), deleteOutcome.getExpungedResourcesCount());
return personPids.size();
}
private void throwExceptionIfInvalidTargetType(String theResourceType) {
if (!EmpiUtil.supportedTargetType(theResourceType)) {
throw new InvalidRequestException(ProviderConstants.EMPI_CLEAR + " does not support resource type: " + theResourceType);
}
}
@Override
public long expungeAllEmpiLinks(ServletRequestDetails theRequestDetails) {
ourLog.info("Clearing all EMPI Links...");
List<Long> personPids = myEmpiLinkDaoSvc.deleteAllEmpiLinksAndReturnPersonPids();
DeleteMethodOutcome deleteOutcome = myEmpiPersonDeletingSvcImpl.expungePersonPids(personPids, theRequestDetails);
ourLog.info("EMPI clear operation complete. Removed {} EMPI links and expunged {} Person resources.", personPids.size(), deleteOutcome.getExpungedResourcesCount());
return personPids.size();
}
}

View File

@ -1,100 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.empi.api.EmpiLinkJson;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiControllerSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.provider.EmpiControllerHelper;
import ca.uhn.fhir.empi.provider.EmpiControllerUtil;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.stream.Stream;
/**
* This class acts as a layer between EmpiProviders and EMPI services to support a REST API that's not a FHIR Operation API.
*/
@Service
public class EmpiControllerSvcImpl implements IEmpiControllerSvc {
@Autowired
EmpiControllerHelper myEmpiControllerHelper;
@Autowired
IEmpiPersonMergerSvc myEmpiPersonMergerSvc;
@Autowired
IEmpiLinkQuerySvc myEmpiLinkQuerySvc;
@Autowired
IEmpiLinkUpdaterSvc myIEmpiLinkUpdaterSvc;
@Override
public IAnyResource mergePersons(String theFromPersonId, String theToPersonId, EmpiTransactionContext theEmpiTransactionContext) {
IAnyResource fromPerson = myEmpiControllerHelper.getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_MERGE_PERSONS_FROM_PERSON_ID, theFromPersonId);
IAnyResource toPerson = myEmpiControllerHelper.getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, theToPersonId);
myEmpiControllerHelper.validateMergeResources(fromPerson, toPerson);
myEmpiControllerHelper.validateSameVersion(fromPerson, theFromPersonId);
myEmpiControllerHelper.validateSameVersion(toPerson, theToPersonId);
return myEmpiPersonMergerSvc.mergePersons(fromPerson, toPerson, theEmpiTransactionContext);
}
@Override
public Stream<EmpiLinkJson> queryLinks(@Nullable String thePersonId, @Nullable String theTargetId, @Nullable String theMatchResult, @Nullable String theLinkSource, EmpiTransactionContext theEmpiContext) {
IIdType personId = EmpiControllerUtil.extractPersonIdDtOrNull(ProviderConstants.EMPI_QUERY_LINKS_PERSON_ID, thePersonId);
IIdType targetId = EmpiControllerUtil.extractTargetIdDtOrNull(ProviderConstants.EMPI_QUERY_LINKS_TARGET_ID, theTargetId);
EmpiMatchResultEnum matchResult = EmpiControllerUtil.extractMatchResultOrNull(theMatchResult);
EmpiLinkSourceEnum linkSource = EmpiControllerUtil.extractLinkSourceOrNull(theLinkSource);
return myEmpiLinkQuerySvc.queryLinks(personId, targetId, matchResult, linkSource, theEmpiContext);
}
@Override
public Stream<EmpiLinkJson> getDuplicatePersons(EmpiTransactionContext theEmpiContext) {
return myEmpiLinkQuerySvc.getDuplicatePersons(theEmpiContext);
}
@Override
public IAnyResource updateLink(String thePersonId, String theTargetId, String theMatchResult, EmpiTransactionContext theEmpiContext) {
EmpiMatchResultEnum matchResult = EmpiControllerUtil.extractMatchResultOrNull(theMatchResult);
IAnyResource person = myEmpiControllerHelper.getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_PERSON_ID, thePersonId);
IAnyResource target = myEmpiControllerHelper.getLatestTargetFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_TARGET_ID, theTargetId);
myEmpiControllerHelper.validateSameVersion(person, thePersonId);
myEmpiControllerHelper.validateSameVersion(target, theTargetId);
return myIEmpiLinkUpdaterSvc.updateLink(person, target, matchResult, theEmpiContext);
}
@Override
public void notDuplicatePerson(String thePersonId, String theTargetPersonId, EmpiTransactionContext theEmpiContext) {
IAnyResource person = myEmpiControllerHelper.getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_PERSON_ID, thePersonId);
IAnyResource target = myEmpiControllerHelper.getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_TARGET_ID, theTargetPersonId);
myIEmpiLinkUpdaterSvc.notDuplicatePerson(person, target, theEmpiContext);
}
}

View File

@ -1,178 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.model.CanonicalEID;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.util.EIDHelper;
import ca.uhn.fhir.empi.util.PersonHelper;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiPersonFindingSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.MatchedPersonCandidate;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class EmpiEidUpdateService {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private EmpiResourceDaoSvc myEmpiResourceDaoSvc;
@Autowired
private IEmpiLinkSvc myEmpiLinkSvc;
@Autowired
private EmpiPersonFindingSvc myEmpiPersonFindingSvc;
@Autowired
private PersonHelper myPersonHelper;
@Autowired
private EIDHelper myEIDHelper;
@Autowired
private EmpiLinkDaoSvc myEmpiLinkDaoSvc;
@Autowired
private IEmpiSettings myEmpiSettings;
void handleEmpiUpdate(IAnyResource theResource, MatchedPersonCandidate theMatchedPersonCandidate, EmpiTransactionContext theEmpiTransactionContext) {
EmpiUpdateContext updateContext = new EmpiUpdateContext(theMatchedPersonCandidate, theResource);
if (updateContext.isRemainsMatchedToSamePerson()) {
myPersonHelper.updatePersonFromUpdatedEmpiTarget(updateContext.getMatchedPerson(), theResource, theEmpiTransactionContext);
if (!updateContext.isIncomingResourceHasAnEid() || updateContext.isHasEidsInCommon()) {
//update to patient that uses internal EIDs only.
myEmpiLinkSvc.updateLink(updateContext.getMatchedPerson(), theResource, theMatchedPersonCandidate.getMatchResult(), EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
} else if (!updateContext.isHasEidsInCommon()) {
handleNoEidsInCommon(theResource, theMatchedPersonCandidate, theEmpiTransactionContext, updateContext);
}
} else {
//This is a new linking scenario. we have to break the existing link and link to the new person. For now, we create duplicate.
//updated patient has an EID that matches to a new candidate. Link them, and set the persons possible duplicates
linkToNewPersonAndFlagAsDuplicate(theResource, updateContext.getExistingPerson(), updateContext.getMatchedPerson(), theEmpiTransactionContext);
}
}
private void handleNoEidsInCommon(IAnyResource theResource, MatchedPersonCandidate theMatchedPersonCandidate, EmpiTransactionContext theEmpiTransactionContext, EmpiUpdateContext theUpdateContext) {
// the user is simply updating their EID. We propagate this change to the Person.
//overwrite. No EIDS in common, but still same person.
if (myEmpiSettings.isPreventMultipleEids()) {
if (myPersonHelper.getLinkCount(theUpdateContext.getMatchedPerson()) <= 1) { // If there is only 0/1 link on the person, we can safely overwrite the EID.
handleExternalEidOverwrite(theUpdateContext.getMatchedPerson(), theResource, theEmpiTransactionContext);
} else { // If the person has multiple patients tied to it, we can't just overwrite the EID, so we split the person.
createNewPersonAndFlagAsDuplicate(theResource, theEmpiTransactionContext, theUpdateContext.getExistingPerson());
}
} else {
myPersonHelper.handleExternalEidAddition(theUpdateContext.getMatchedPerson(), theResource, theEmpiTransactionContext);
}
myEmpiLinkSvc.updateLink(theUpdateContext.getMatchedPerson(), theResource, theMatchedPersonCandidate.getMatchResult(), EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
}
private void handleExternalEidOverwrite(IAnyResource thePerson, IAnyResource theResource, EmpiTransactionContext theEmpiTransactionContext) {
List<CanonicalEID> eidFromResource = myEIDHelper.getExternalEid(theResource);
if (!eidFromResource.isEmpty()) {
myPersonHelper.overwriteExternalEids(thePerson, eidFromResource);
}
}
private boolean candidateIsSameAsEmpiLinkPerson(EmpiLink theExistingMatchLink, MatchedPersonCandidate thePersonCandidate) {
return theExistingMatchLink.getPersonPid().equals(thePersonCandidate.getCandidatePersonPid().getIdAsLong());
}
private void createNewPersonAndFlagAsDuplicate(IAnyResource theResource, EmpiTransactionContext theEmpiTransactionContext, IAnyResource theOldPerson) {
log(theEmpiTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs.");
IAnyResource newPerson = myPersonHelper.createPersonFromEmpiTarget(theResource);
myEmpiLinkSvc.updateLink(newPerson, theResource, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
myEmpiLinkSvc.updateLink(newPerson, theOldPerson, EmpiMatchOutcome.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
}
private void linkToNewPersonAndFlagAsDuplicate(IAnyResource theResource, IAnyResource theOldPerson, IAnyResource theNewPerson, EmpiTransactionContext theEmpiTransactionContext) {
log(theEmpiTransactionContext, "Changing a match link!");
myEmpiLinkSvc.deleteLink(theOldPerson, theResource, theEmpiTransactionContext);
myEmpiLinkSvc.updateLink(theNewPerson, theResource, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
log(theEmpiTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs.");
myEmpiLinkSvc.updateLink(theNewPerson, theOldPerson, EmpiMatchOutcome.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
}
private void log(EmpiTransactionContext theEmpiTransactionContext, String theMessage) {
theEmpiTransactionContext.addTransactionLogMessage(theMessage);
ourLog.debug(theMessage);
}
/**
* Data class to hold context surrounding an update operation for an EMPI target.
*/
class EmpiUpdateContext {
private final boolean myHasEidsInCommon;
private final boolean myIncomingResourceHasAnEid;
private IAnyResource myExistingPerson;
private boolean myRemainsMatchedToSamePerson;
public IAnyResource getMatchedPerson() {
return myMatchedPerson;
}
private final IAnyResource myMatchedPerson;
EmpiUpdateContext(MatchedPersonCandidate theMatchedPersonCandidate, IAnyResource theResource) {
myMatchedPerson = myEmpiPersonFindingSvc.getPersonFromMatchedPersonCandidate(theMatchedPersonCandidate);
myHasEidsInCommon = myEIDHelper.hasEidOverlap(myMatchedPerson, theResource);
myIncomingResourceHasAnEid = !myEIDHelper.getExternalEid(theResource).isEmpty();
Optional<EmpiLink> theExistingMatchLink = myEmpiLinkDaoSvc.getMatchedLinkForTarget(theResource);
myExistingPerson = null;
if (theExistingMatchLink.isPresent()) {
Long existingPersonPid = theExistingMatchLink.get().getPersonPid();
myExistingPerson = myEmpiResourceDaoSvc.readPersonByPid(new ResourcePersistentId(existingPersonPid));
myRemainsMatchedToSamePerson = candidateIsSameAsEmpiLinkPerson(theExistingMatchLink.get(), theMatchedPersonCandidate);
} else {
myRemainsMatchedToSamePerson = false;
}
}
public boolean isHasEidsInCommon() {
return myHasEidsInCommon;
}
public boolean isIncomingResourceHasAnEid() {
return myIncomingResourceHasAnEid;
}
public IAnyResource getExistingPerson() {
return myExistingPerson;
}
public boolean isRemainsMatchedToSamePerson() {
return myRemainsMatchedToSamePerson;
}
}
}

View File

@ -1,97 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.empi.api.EmpiLinkJson;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import java.util.stream.Stream;
public class EmpiLinkQuerySvcImpl implements IEmpiLinkQuerySvc {
private static final Logger ourLog = LoggerFactory.getLogger(EmpiLinkQuerySvcImpl.class);
@Autowired
IdHelperService myIdHelperService;
@Autowired
EmpiLinkDaoSvc myEmpiLinkDaoSvc;
@Override
public Stream<EmpiLinkJson> queryLinks(IIdType thePersonId, IIdType theTargetId, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiContext) {
Example<EmpiLink> exampleLink = exampleLinkFromParameters(thePersonId, theTargetId, theMatchResult, theLinkSource);
return myEmpiLinkDaoSvc.findEmpiLinkByExample(exampleLink).stream()
.filter(empiLink -> empiLink.getMatchResult() != EmpiMatchResultEnum.POSSIBLE_DUPLICATE)
.map(this::toJson);
}
@Override
public Stream<EmpiLinkJson> getDuplicatePersons(EmpiTransactionContext theEmpiContext) {
Example<EmpiLink> exampleLink = exampleLinkFromParameters(null, null, EmpiMatchResultEnum.POSSIBLE_DUPLICATE, null);
return myEmpiLinkDaoSvc.findEmpiLinkByExample(exampleLink).stream().map(this::toJson);
}
private EmpiLinkJson toJson(EmpiLink theLink) {
EmpiLinkJson retval = new EmpiLinkJson();
String targetId = myIdHelperService.resourceIdFromPidOrThrowException(theLink.getTargetPid()).toVersionless().getValue();
retval.setTargetId(targetId);
String personId = myIdHelperService.resourceIdFromPidOrThrowException(theLink.getPersonPid()).toVersionless().getValue();
retval.setPersonId(personId);
retval.setCreated(theLink.getCreated());
retval.setEidMatch(theLink.getEidMatch());
retval.setLinkSource(theLink.getLinkSource());
retval.setMatchResult(theLink.getMatchResult());
retval.setNewPerson(theLink.getNewPerson());
retval.setScore(theLink.getScore());
retval.setUpdated(theLink.getUpdated());
retval.setVector(theLink.getVector());
retval.setVersion(theLink.getVersion());
return retval;
}
private Example<EmpiLink> exampleLinkFromParameters(IIdType thePersonId, IIdType theTargetId, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource) {
EmpiLink empiLink = myEmpiLinkDaoSvc.newEmpiLink();
if (thePersonId != null) {
empiLink.setPersonPid(myIdHelperService.getPidOrThrowException(thePersonId));
}
if (theTargetId != null) {
empiLink.setTargetPid(myIdHelperService.getPidOrThrowException(theTargetId));
}
if (theMatchResult != null) {
empiLink.setMatchResult(theMatchResult);
}
if (theLinkSource != null) {
empiLink.setLinkSource(theLinkSource);
}
return Example.of(empiLink);
}
}

View File

@ -1,189 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.model.CanonicalIdentityAssuranceLevel;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.util.AssuranceLevelUtil;
import ca.uhn.fhir.empi.util.PersonHelper;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseBackboneElement;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* This class is in charge of managing EmpiLinks between Persons and target resources
*/
@Service
public class EmpiLinkSvcImpl implements IEmpiLinkSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private EmpiResourceDaoSvc myEmpiResourceDaoSvc;
@Autowired
private EmpiLinkDaoSvc myEmpiLinkDaoSvc;
@Autowired
private PersonHelper myPersonHelper;
@Autowired
private IdHelperService myIdHelperService;
@Override
@Transactional
public void updateLink(IAnyResource thePerson, IAnyResource theTarget, EmpiMatchOutcome theMatchOutcome, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiTransactionContext) {
IIdType resourceId = theTarget.getIdElement().toUnqualifiedVersionless();
if (theMatchOutcome.isPossibleDuplicate() && personsLinkedAsNoMatch(thePerson, theTarget)) {
log(theEmpiTransactionContext, thePerson.getIdElement().toUnqualifiedVersionless() +
" is linked as NO_MATCH with " +
theTarget.getIdElement().toUnqualifiedVersionless() +
" not linking as POSSIBLE_DUPLICATE.");
return;
}
EmpiMatchResultEnum matchResultEnum = theMatchOutcome.getMatchResultEnum();
validateRequestIsLegal(thePerson, theTarget, matchResultEnum, theLinkSource);
switch (matchResultEnum) {
case MATCH:
myPersonHelper.addOrUpdateLink(thePerson, resourceId, AssuranceLevelUtil.getAssuranceLevel(matchResultEnum, theLinkSource), theEmpiTransactionContext);
myEmpiResourceDaoSvc.updatePerson(thePerson);
break;
case POSSIBLE_MATCH:
myPersonHelper.addOrUpdateLink(thePerson, resourceId, AssuranceLevelUtil.getAssuranceLevel(matchResultEnum, theLinkSource), theEmpiTransactionContext);
break;
case NO_MATCH:
myPersonHelper.removeLink(thePerson, resourceId, theEmpiTransactionContext);
break;
case POSSIBLE_DUPLICATE:
break;
}
myEmpiResourceDaoSvc.updatePerson(thePerson);
createOrUpdateLinkEntity(thePerson, theTarget, theMatchOutcome, theLinkSource, theEmpiTransactionContext);
}
private boolean personsLinkedAsNoMatch(IAnyResource thePerson, IAnyResource theTarget) {
Long personId = myIdHelperService.getPidOrThrowException(thePerson);
Long targetId = myIdHelperService.getPidOrThrowException(theTarget);
// TODO perf collapse into one query
return myEmpiLinkDaoSvc.getEmpiLinksByPersonPidTargetPidAndMatchResult(personId, targetId, EmpiMatchResultEnum.NO_MATCH).isPresent() ||
myEmpiLinkDaoSvc.getEmpiLinksByPersonPidTargetPidAndMatchResult(targetId, personId, EmpiMatchResultEnum.NO_MATCH).isPresent();
}
@Override
@Transactional
public void syncEmpiLinksToPersonLinks(IAnyResource thePersonResource, EmpiTransactionContext theEmpiTransactionContext) {
int origLinkCount = myPersonHelper.getLinkCount(thePersonResource);
List<EmpiLink> empiLinks = myEmpiLinkDaoSvc.findEmpiLinksByPerson(thePersonResource);
List<IBaseBackboneElement> newLinks = empiLinks.stream()
.filter(link -> link.isMatch() || link.isPossibleMatch() || link.isRedirect())
.map(this::personLinkFromEmpiLink)
.collect(Collectors.toList());
myPersonHelper.setLinks(thePersonResource, newLinks);
if (newLinks.size() > origLinkCount) {
log(theEmpiTransactionContext, thePersonResource.getIdElement().toVersionless() + " links increased from " + origLinkCount + " to " + newLinks.size());
} else if (newLinks.size() < origLinkCount) {
log(theEmpiTransactionContext, thePersonResource.getIdElement().toVersionless() + " links decreased from " + origLinkCount + " to " + newLinks.size());
}
}
@Override
public void deleteLink(IAnyResource theExistingPerson, IAnyResource theResource, EmpiTransactionContext theEmpiTransactionContext) {
myPersonHelper.removeLink(theExistingPerson, theResource.getIdElement(), theEmpiTransactionContext);
Optional<EmpiLink> oEmpiLink = getEmpiLinkForPersonTargetPair(theExistingPerson, theResource);
if (oEmpiLink.isPresent()) {
EmpiLink empiLink = oEmpiLink.get();
log(theEmpiTransactionContext, "Deleting EmpiLink [" + theExistingPerson.getIdElement().toVersionless() + " -> " + theResource.getIdElement().toVersionless() + "] with result: " + empiLink.getMatchResult());
myEmpiLinkDaoSvc.deleteLink(empiLink);
}
}
private IBaseBackboneElement personLinkFromEmpiLink(EmpiLink empiLink) {
IIdType resourceId = myIdHelperService.resourceIdFromPidOrThrowException(empiLink.getTargetPid());
CanonicalIdentityAssuranceLevel assuranceLevel = AssuranceLevelUtil.getAssuranceLevel(empiLink.getMatchResult(), empiLink.getLinkSource());
return myPersonHelper.newPersonLink(resourceId, assuranceLevel);
}
/**
* Helper function which runs various business rules about what types of requests are allowed.
*/
private void validateRequestIsLegal(IAnyResource thePerson, IAnyResource theResource, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource) {
Optional<EmpiLink> oExistingLink = getEmpiLinkForPersonTargetPair(thePerson, theResource);
if (oExistingLink.isPresent() && systemIsAttemptingToModifyManualLink(theLinkSource, oExistingLink.get())) {
throw new InternalErrorException("EMPI system is not allowed to modify links on manually created links");
}
if (systemIsAttemptingToAddNoMatch(theLinkSource, theMatchResult)) {
throw new InternalErrorException("EMPI system is not allowed to automatically NO_MATCH a resource");
}
}
/**
* Helper function which detects when the EMPI system is attempting to add a NO_MATCH link, which is not allowed.
*/
private boolean systemIsAttemptingToAddNoMatch(EmpiLinkSourceEnum theLinkSource, EmpiMatchResultEnum theMatchResult) {
return theLinkSource == EmpiLinkSourceEnum.AUTO && theMatchResult == EmpiMatchResultEnum.NO_MATCH;
}
/**
* Helper function to let us catch when System EMPI rules are attempting to override a manually defined link.
*/
private boolean systemIsAttemptingToModifyManualLink(EmpiLinkSourceEnum theIncomingSource, EmpiLink theExistingSource) {
return theIncomingSource == EmpiLinkSourceEnum.AUTO && theExistingSource.isManual();
}
private Optional<EmpiLink> getEmpiLinkForPersonTargetPair(IAnyResource thePerson, IAnyResource theCandidate) {
if (thePerson.getIdElement().getIdPart() == null || theCandidate.getIdElement().getIdPart() == null) {
return Optional.empty();
} else {
return myEmpiLinkDaoSvc.getLinkByPersonPidAndTargetPid(
myIdHelperService.getPidOrNull(thePerson),
myIdHelperService.getPidOrNull(theCandidate)
);
}
}
private void createOrUpdateLinkEntity(IBaseResource thePerson, IBaseResource theResource, EmpiMatchOutcome theMatchOutcome, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiTransactionContext) {
myEmpiLinkDaoSvc.createOrUpdateLinkEntity(thePerson, theResource, theMatchOutcome, theLinkSource, theEmpiTransactionContext);
}
private void log(EmpiTransactionContext theEmpiTransactionContext, String theMessage) {
theEmpiTransactionContext.addTransactionLogMessage(theMessage);
ourLog.debug(theMessage);
}
}

View File

@ -1,153 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
public class EmpiLinkUpdaterSvcImpl implements IEmpiLinkUpdaterSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
FhirContext myFhirContext;
@Autowired
IdHelperService myIdHelperService;
@Autowired
EmpiLinkDaoSvc myEmpiLinkDaoSvc;
@Autowired
IEmpiLinkSvc myEmpiLinkSvc;
@Autowired
EmpiResourceDaoSvc myEmpiResourceDaoSvc;
@Autowired
EmpiMatchLinkSvc myEmpiMatchLinkSvc;
@Transactional
@Override
public IAnyResource updateLink(IAnyResource thePerson, IAnyResource theTarget, EmpiMatchResultEnum theMatchResult, EmpiTransactionContext theEmpiContext) {
String targetType = myFhirContext.getResourceType(theTarget);
validateUpdateLinkRequest(thePerson, theTarget, theMatchResult, targetType);
Long personId = myIdHelperService.getPidOrThrowException(thePerson);
Long targetId = myIdHelperService.getPidOrThrowException(theTarget);
Optional<EmpiLink> oEmpiLink = myEmpiLinkDaoSvc.getLinkByPersonPidAndTargetPid(personId, targetId);
if (!oEmpiLink.isPresent()) {
throw new InvalidRequestException("No link exists between " + thePerson.getIdElement().toVersionless() + " and " + theTarget.getIdElement().toVersionless());
}
EmpiLink empiLink = oEmpiLink.get();
if (empiLink.getMatchResult() == theMatchResult) {
ourLog.warn("EMPI Link for " + thePerson.getIdElement().toVersionless() + ", " + theTarget.getIdElement().toVersionless() + " already has value " + theMatchResult + ". Nothing to do.");
return thePerson;
}
ourLog.info("Manually updating EMPI Link for " + thePerson.getIdElement().toVersionless() + ", " + theTarget.getIdElement().toVersionless() + " from " + empiLink.getMatchResult() + " to " + theMatchResult + ".");
empiLink.setMatchResult(theMatchResult);
empiLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
myEmpiLinkDaoSvc.save(empiLink);
myEmpiLinkSvc.syncEmpiLinksToPersonLinks(thePerson, theEmpiContext);
myEmpiResourceDaoSvc.updatePerson(thePerson);
if (theMatchResult == EmpiMatchResultEnum.NO_MATCH) {
// Need to find a new Person to link this target to
myEmpiMatchLinkSvc.updateEmpiLinksForEmpiTarget(theTarget, theEmpiContext);
}
return thePerson;
}
private void validateUpdateLinkRequest(IAnyResource thePerson, IAnyResource theTarget, EmpiMatchResultEnum theMatchResult, String theTargetType) {
String personType = myFhirContext.getResourceType(thePerson);
if (theMatchResult != EmpiMatchResultEnum.NO_MATCH &&
theMatchResult != EmpiMatchResultEnum.MATCH) {
throw new InvalidRequestException("Match Result may only be set to " + EmpiMatchResultEnum.NO_MATCH + " or " + EmpiMatchResultEnum.MATCH);
}
if (!"Person".equals(personType)) {
throw new InvalidRequestException("First argument to " + ProviderConstants.EMPI_UPDATE_LINK + " must be a Person. Was " + personType);
}
if (!EmpiUtil.supportedTargetType(theTargetType)) {
throw new InvalidRequestException("Second argument to " + ProviderConstants.EMPI_UPDATE_LINK + " must be a Patient or Practitioner. Was " + theTargetType);
}
if (!EmpiUtil.isEmpiManaged(thePerson)) {
throw new InvalidRequestException("Only EMPI Managed Person resources may be updated via this operation. The Person resource provided is not tagged as managed by hapi-empi");
}
if (!EmpiUtil.isEmpiAccessible(theTarget)) {
throw new InvalidRequestException("The target is marked with the " + EmpiConstants.CODE_NO_EMPI_MANAGED + " tag which means it may not be EMPI linked.");
}
}
@Transactional
@Override
public void notDuplicatePerson(IAnyResource thePerson, IAnyResource theTarget, EmpiTransactionContext theEmpiContext) {
validateNotDuplicatePersonRequest(thePerson, theTarget);
Long personId = myIdHelperService.getPidOrThrowException(thePerson);
Long targetId = myIdHelperService.getPidOrThrowException(theTarget);
Optional<EmpiLink> oEmpiLink = myEmpiLinkDaoSvc.getLinkByPersonPidAndTargetPid(personId, targetId);
if (!oEmpiLink.isPresent()) {
throw new InvalidRequestException("No link exists between " + thePerson.getIdElement().toVersionless() + " and " + theTarget.getIdElement().toVersionless());
}
EmpiLink empiLink = oEmpiLink.get();
if (!empiLink.isPossibleDuplicate()) {
throw new InvalidRequestException(thePerson.getIdElement().toVersionless() + " and " + theTarget.getIdElement().toVersionless() + " are not linked as POSSIBLE_DUPLICATE.");
}
empiLink.setMatchResult(EmpiMatchResultEnum.NO_MATCH);
empiLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
myEmpiLinkDaoSvc.save(empiLink);
}
private void validateNotDuplicatePersonRequest(IAnyResource thePerson, IAnyResource theTarget) {
String personType = myFhirContext.getResourceType(thePerson);
String targetType = myFhirContext.getResourceType(theTarget);
if (!"Person".equals(personType)) {
throw new InvalidRequestException("First argument to " + ProviderConstants.EMPI_UPDATE_LINK + " must be a Person. Was " + personType);
}
if (!"Person".equals(targetType)) {
throw new InvalidRequestException("Second argument to " + ProviderConstants.EMPI_UPDATE_LINK + " must be a Person . Was " + targetType);
}
if (!EmpiUtil.isEmpiManaged(thePerson) || !EmpiUtil.isEmpiManaged(theTarget)) {
throw new InvalidRequestException("Only EMPI Managed Person resources may be updated via this operation. The Person resource provided is not tagged as managed by hapi-empi");
}
}
}

View File

@ -1,158 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.empi.util.PersonHelper;
import ca.uhn.fhir.jpa.empi.svc.candidate.CandidateList;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiPersonFindingSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.MatchedPersonCandidate;
import ca.uhn.fhir.rest.server.TransactionLogMessages;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* EmpiMatchLinkSvc is the entrypoint for HAPI's EMPI system. An incoming resource can call
* updateEmpiLinksForEmpiTarget and the underlying EMPI system will take care of matching it to a person, or creating a
* new Person if a suitable one was not found.
*/
@Service
public class EmpiMatchLinkSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private IEmpiLinkSvc myEmpiLinkSvc;
@Autowired
private EmpiPersonFindingSvc myEmpiPersonFindingSvc;
@Autowired
private PersonHelper myPersonHelper;
@Autowired
private EmpiEidUpdateService myEidUpdateService;
/**
* Given an Empi Target (consisting of either a Patient or a Practitioner), find a suitable Person candidate for them,
* or create one if one does not exist. Performs matching based on rules defined in empi-rules.json.
* Does nothing if resource is determined to be not managed by EMPI.
*
* @param theResource the incoming EMPI target, which is either a Patient or Practitioner.
* @param theEmpiTransactionContext
* @return an {@link TransactionLogMessages} which contains all informational messages related to EMPI processing of this resource.
*/
public EmpiTransactionContext updateEmpiLinksForEmpiTarget(IAnyResource theResource, EmpiTransactionContext theEmpiTransactionContext) {
if (EmpiUtil.isEmpiAccessible(theResource)) {
return doEmpiUpdate(theResource, theEmpiTransactionContext);
} else {
return null;
}
}
private EmpiTransactionContext doEmpiUpdate(IAnyResource theResource, EmpiTransactionContext theEmpiTransactionContext) {
CandidateList candidateList = myEmpiPersonFindingSvc.findPersonCandidates(theResource);
if (candidateList.isEmpty()) {
handleEmpiWithNoCandidates(theResource, theEmpiTransactionContext);
} else if (candidateList.exactlyOneMatch()) {
handleEmpiWithSingleCandidate(theResource, candidateList.getOnlyMatch(), theEmpiTransactionContext);
} else {
handleEmpiWithMultipleCandidates(theResource, candidateList, theEmpiTransactionContext);
}
return theEmpiTransactionContext;
}
private void handleEmpiWithMultipleCandidates(IAnyResource theResource, CandidateList theCandidateList, EmpiTransactionContext theEmpiTransactionContext) {
MatchedPersonCandidate firstMatch = theCandidateList.getFirstMatch();
Long samplePersonPid = firstMatch.getCandidatePersonPid().getIdAsLong();
boolean allSamePerson = theCandidateList.stream()
.allMatch(candidate -> candidate.getCandidatePersonPid().getIdAsLong().equals(samplePersonPid));
if (allSamePerson) {
log(theEmpiTransactionContext, "EMPI received multiple match candidates, but they are all linked to the same person.");
handleEmpiWithSingleCandidate(theResource, firstMatch, theEmpiTransactionContext);
} else {
log(theEmpiTransactionContext, "EMPI received multiple match candidates, that were linked to different Persons. Setting POSSIBLE_DUPLICATES and POSSIBLE_MATCHES.");
//Set them all as POSSIBLE_MATCH
List<IAnyResource> persons = new ArrayList<>();
for (MatchedPersonCandidate matchedPersonCandidate : theCandidateList.getCandidates()) {
IAnyResource person = myEmpiPersonFindingSvc.getPersonFromMatchedPersonCandidate(matchedPersonCandidate);
EmpiMatchOutcome outcome = EmpiMatchOutcome.POSSIBLE_MATCH;
outcome.setEidMatch(theCandidateList.isEidMatch());
myEmpiLinkSvc.updateLink(person, theResource, outcome, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
persons.add(person);
}
//Set all Persons as POSSIBLE_DUPLICATE of the last person.
IAnyResource firstPerson = persons.get(0);
persons.subList(1, persons.size())
.forEach(possibleDuplicatePerson -> {
EmpiMatchOutcome outcome = EmpiMatchOutcome.POSSIBLE_DUPLICATE;
outcome.setEidMatch(theCandidateList.isEidMatch());
myEmpiLinkSvc.updateLink(firstPerson, possibleDuplicatePerson, outcome, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
});
}
}
private void handleEmpiWithNoCandidates(IAnyResource theResource, EmpiTransactionContext theEmpiTransactionContext) {
log(theEmpiTransactionContext, "There were no matched candidates for EMPI, creating a new Person.");
IAnyResource newPerson = myPersonHelper.createPersonFromEmpiTarget(theResource);
myEmpiLinkSvc.updateLink(newPerson, theResource, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
}
private void handleEmpiCreate(IAnyResource theResource, MatchedPersonCandidate thePersonCandidate, EmpiTransactionContext theEmpiTransactionContext) {
log(theEmpiTransactionContext, "EMPI has narrowed down to one candidate for matching.");
IAnyResource person = myEmpiPersonFindingSvc.getPersonFromMatchedPersonCandidate(thePersonCandidate);
if (myPersonHelper.isPotentialDuplicate(person, theResource)) {
log(theEmpiTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs.");
IAnyResource newPerson = myPersonHelper.createPersonFromEmpiTarget(theResource);
myEmpiLinkSvc.updateLink(newPerson, theResource, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
myEmpiLinkSvc.updateLink(newPerson, person, EmpiMatchOutcome.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
} else {
if (thePersonCandidate.isMatch()) {
myPersonHelper.handleExternalEidAddition(person, theResource, theEmpiTransactionContext);
myPersonHelper.updatePersonFromNewlyCreatedEmpiTarget(person, theResource, theEmpiTransactionContext);
}
myEmpiLinkSvc.updateLink(person, theResource, thePersonCandidate.getMatchResult(), EmpiLinkSourceEnum.AUTO, theEmpiTransactionContext);
}
}
private void handleEmpiWithSingleCandidate(IAnyResource theResource, MatchedPersonCandidate thePersonCandidate, EmpiTransactionContext theEmpiTransactionContext) {
log(theEmpiTransactionContext, "EMPI has narrowed down to one candidate for matching.");
if (theEmpiTransactionContext.getRestOperation().equals(EmpiTransactionContext.OperationType.UPDATE_RESOURCE)) {
myEidUpdateService.handleEmpiUpdate(theResource, thePersonCandidate, theEmpiTransactionContext);
} else {
handleEmpiCreate(theResource, thePersonCandidate, theEmpiTransactionContext);
}
}
private void log(EmpiTransactionContext theEmpiTransactionContext, String theMessage) {
theEmpiTransactionContext.addTransactionLogMessage(theMessage);
ourLog.debug(theMessage);
}
}

View File

@ -1,135 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.util.PersonHelper;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
public class EmpiPersonMergerSvcImpl implements IEmpiPersonMergerSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
PersonHelper myPersonHelper;
@Autowired
EmpiLinkDaoSvc myEmpiLinkDaoSvc;
@Autowired
IEmpiLinkSvc myEmpiLinkSvc;
@Autowired
IdHelperService myIdHelperService;
@Autowired
EmpiResourceDaoSvc myEmpiResourceDaoSvc;
@Override
@Transactional
public IAnyResource mergePersons(IAnyResource theFromPerson, IAnyResource theToPerson, EmpiTransactionContext theEmpiTransactionContext) {
Long toPersonPid = myIdHelperService.getPidOrThrowException(theToPerson);
myPersonHelper.mergePersonFields(theFromPerson, theToPerson);
mergeLinks(theFromPerson, theToPerson, toPersonPid, theEmpiTransactionContext);
refreshLinksAndUpdatePerson(theToPerson, theEmpiTransactionContext);
Long fromPersonPid = myIdHelperService.getPidOrThrowException(theFromPerson);
addMergeLink(fromPersonPid, toPersonPid);
myPersonHelper.deactivatePerson(theFromPerson);
refreshLinksAndUpdatePerson(theFromPerson, theEmpiTransactionContext);
log(theEmpiTransactionContext, "Merged " + theFromPerson.getIdElement().toVersionless() + " into " + theToPerson.getIdElement().toVersionless());
return theToPerson;
}
private void addMergeLink(Long theDeactivatedPersonPid, Long theActivePersonPid) {
EmpiLink empiLink = myEmpiLinkDaoSvc.getOrCreateEmpiLinkByPersonPidAndTargetPid(theDeactivatedPersonPid, theActivePersonPid);
empiLink
.setEmpiTargetType("Person")
.setMatchResult(EmpiMatchResultEnum.REDIRECT)
.setLinkSource(EmpiLinkSourceEnum.MANUAL);
myEmpiLinkDaoSvc.save(empiLink);
}
private void refreshLinksAndUpdatePerson(IAnyResource theToPerson, EmpiTransactionContext theEmpiTransactionContext) {
myEmpiLinkSvc.syncEmpiLinksToPersonLinks(theToPerson, theEmpiTransactionContext);
myEmpiResourceDaoSvc.updatePerson(theToPerson);
}
private void mergeLinks(IAnyResource theFromPerson, IAnyResource theToPerson, Long theToPersonPid, EmpiTransactionContext theEmpiTransactionContext) {
List<EmpiLink> fromLinks = myEmpiLinkDaoSvc.findEmpiLinksByPerson(theFromPerson);
List<EmpiLink> toLinks = myEmpiLinkDaoSvc.findEmpiLinksByPerson(theToPerson);
// For each incomingLink, either ignore it, move it, or replace the original one
for (EmpiLink fromLink : fromLinks) {
Optional<EmpiLink> optionalToLink = findFirstLinkWithMatchingTarget(toLinks, fromLink);
if (optionalToLink.isPresent()) {
// The original links already contain this target, so move it over to the toPerson
EmpiLink toLink = optionalToLink.get();
if (fromLink.isManual()) {
switch (toLink.getLinkSource()) {
case AUTO:
ourLog.trace("MANUAL overrides AUT0. Deleting link {}", toLink);
myEmpiLinkDaoSvc.deleteLink(toLink);
break;
case MANUAL:
if (fromLink.getMatchResult() != toLink.getMatchResult()) {
throw new InvalidRequestException("A MANUAL " + fromLink.getMatchResult() + " link may not be merged into a MANUAL " + toLink.getMatchResult() + " link for the same target");
}
}
} else {
// Ignore the case where the incoming link is AUTO
continue;
}
}
// The original links didn't contain this target, so move it over to the toPerson
fromLink.setPersonPid(theToPersonPid);
ourLog.trace("Saving link {}", fromLink);
myEmpiLinkDaoSvc.save(fromLink);
}
}
private Optional<EmpiLink> findFirstLinkWithMatchingTarget(List<EmpiLink> theEmpiLinks, EmpiLink theLinkWithTargetToMatch) {
return theEmpiLinks.stream()
.filter(empiLink -> empiLink.getTargetPid().equals(theLinkWithTargetToMatch.getTargetPid()))
.findFirst();
}
private void log(EmpiTransactionContext theEmpiTransactionContext, String theMessage) {
theEmpiTransactionContext.addTransactionLogMessage(theMessage);
ourLog.debug(theMessage);
}
}

View File

@ -1,117 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class EmpiResourceDaoSvc {
private static final int MAX_MATCHING_PERSONS = 1000;
@Autowired
DaoRegistry myDaoRegistry;
@Autowired
IEmpiSettings myEmpiConfig;
private IFhirResourceDao<IBaseResource> myPatientDao;
private IFhirResourceDao<IBaseResource> myPersonDao;
private IFhirResourceDao<IBaseResource> myPractitionerDao;
@PostConstruct
public void postConstruct() {
myPatientDao = myDaoRegistry.getResourceDao("Patient");
myPersonDao = myDaoRegistry.getResourceDao("Person");
myPractitionerDao = myDaoRegistry.getResourceDao("Practitioner");
}
public IAnyResource readPatient(IIdType theId) {
return (IAnyResource) myPatientDao.read(theId);
}
public IAnyResource readPerson(IIdType theId) {
return (IAnyResource) myPersonDao.read(theId);
}
public IAnyResource readPractitioner(IIdType theId) {
return (IAnyResource) myPractitionerDao.read(theId);
}
public DaoMethodOutcome updatePerson(IAnyResource thePerson) {
if (thePerson.getIdElement().hasIdPart()) {
return myPersonDao.update(thePerson);
} else {
return myPersonDao.create(thePerson);
}
}
public IAnyResource readPersonByPid(ResourcePersistentId thePersonPid) {
return (IAnyResource) myPersonDao.readByPid(thePersonPid);
}
public Optional<IAnyResource> searchPersonByEid(String theEid) {
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add("identifier", new TokenParam(myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem(), theEid));
map.add("active", new TokenParam("true"));
IBundleProvider search = myPersonDao.search(map);
// Could add the meta tag to the query, but it's probably more efficient to filter on it afterwards since in practice
// it will always be present.
List<IBaseResource> list = search.getResources(0, MAX_MATCHING_PERSONS).stream()
.filter(EmpiUtil::isEmpiManaged)
.collect(Collectors.toList());
if (list.isEmpty()) {
return Optional.empty();
} else if (list.size() > 1) {
throw new InternalErrorException("Found more than one active " +
EmpiConstants.CODE_HAPI_EMPI_MANAGED +
" Person with EID " +
theEid +
": " +
list.get(0).getIdElement().getValue() +
", " +
list.get(1).getIdElement().getValue()
);
} else {
return Optional.of((IAnyResource) list.get(0));
}
}
}

View File

@ -1,79 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc.candidate;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.jpa.empi.svc.EmpiResourceDaoSvc;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class EmpiPersonFindingSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private EmpiResourceDaoSvc myEmpiResourceDaoSvc;
@Autowired
private FindCandidateByEidSvc myFindCandidateByEidSvc;
@Autowired
private FindCandidateByLinkSvc myFindCandidateByLinkSvc;
@Autowired
private FindCandidateByScoreSvc myFindCandidateByScoreSvc;
/**
* Given an incoming IBaseResource, limited to Patient/Practitioner, return a list of {@link MatchedPersonCandidate}
* indicating possible candidates for a matching Person. Uses several separate methods for finding candidates:
* <p>
* 0. First, check the incoming Resource for an EID. If it is present, and we can find a Person with this EID, it automatically matches.
* 1. First, check link table for any entries where this baseresource is the target of a person. If found, return.
* 2. If none are found, attempt to find Person Resources which link to this theResource.
* 3. If none are found, attempt to find Person Resources similar to our incoming resource based on the EMPI rules and field matchers.
* 4. If none are found, attempt to find Persons that are linked to Patients/Practitioners that are similar to our incoming resource based on the EMPI rules and
* field matchers.
*
* @param theResource the {@link IBaseResource} we are attempting to find matching candidate Persons for.
* @return A list of {@link MatchedPersonCandidate} indicating all potential Person matches.
*/
public CandidateList findPersonCandidates(IAnyResource theResource) {
CandidateList matchedPersonCandidates = myFindCandidateByEidSvc.findCandidates(theResource);
if (matchedPersonCandidates.isEmpty()) {
matchedPersonCandidates = myFindCandidateByLinkSvc.findCandidates(theResource);
}
if (matchedPersonCandidates.isEmpty()) {
//OK, so we have not found any links in the EmpiLink table with us as a target. Next, let's find possible Patient/Practitioner
//matches by following EMPI rules.
matchedPersonCandidates = myFindCandidateByScoreSvc.findCandidates(theResource);
}
return matchedPersonCandidates;
}
public IAnyResource getPersonFromMatchedPersonCandidate(MatchedPersonCandidate theMatchedPersonCandidate) {
ResourcePersistentId personPid = theMatchedPersonCandidate.getCandidatePersonPid();
return myEmpiResourceDaoSvc.readPersonByPid(personPid);
}
}

View File

@ -1,110 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc.candidate;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.MatchedTarget;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class FindCandidateByScoreSvc extends BaseCandidateFinder {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private FhirContext myFhirContext;
@Autowired
IdHelperService myIdHelperService;
@Autowired
private EmpiLinkDaoSvc myEmpiLinkDaoSvc;
@Autowired
private IEmpiMatchFinderSvc myEmpiMatchFinderSvc;
/**
* Attempt to find matching Persons by resolving them from similar Matching target resources, where target resource
* can be either Patient or Practitioner. Runs EMPI logic over the existing Patient/Practitioners, then finds their
* entries in the EmpiLink table, and returns all the matches found therein.
*
* @param theTarget the {@link IBaseResource} which we want to find candidate Persons for.
* @return an Optional list of {@link MatchedPersonCandidate} indicating matches.
*/
@Override
protected List<MatchedPersonCandidate> findMatchPersonCandidates(IAnyResource theTarget) {
List<MatchedPersonCandidate> retval = new ArrayList<>();
List<Long> personPidsToExclude = getNoMatchPersonPids(theTarget);
List<MatchedTarget> matchedCandidates = myEmpiMatchFinderSvc.getMatchedTargets(myFhirContext.getResourceType(theTarget), theTarget);
//Convert all possible match targets to their equivalent Persons by looking up in the EmpiLink table,
//while ensuring that the matches aren't in our NO_MATCH list.
// The data flow is as follows ->
// MatchedTargetCandidate -> Person -> EmpiLink -> MatchedPersonCandidate
matchedCandidates = matchedCandidates.stream().filter(mc -> mc.isMatch() || mc.isPossibleMatch()).collect(Collectors.toList());
for (MatchedTarget match : matchedCandidates) {
Optional<EmpiLink> optMatchEmpiLink = myEmpiLinkDaoSvc.getMatchedLinkForTargetPid(myIdHelperService.getPidOrNull(match.getTarget()));
if (!optMatchEmpiLink.isPresent()) {
continue;
}
EmpiLink matchEmpiLink = optMatchEmpiLink.get();
if (personPidsToExclude.contains(matchEmpiLink.getPersonPid())) {
ourLog.info("Skipping EMPI on candidate person with PID {} due to manual NO_MATCH", matchEmpiLink.getPersonPid());
continue;
}
MatchedPersonCandidate candidate = new MatchedPersonCandidate(getResourcePersistentId(matchEmpiLink.getPersonPid()), match.getMatchResult());
retval.add(candidate);
}
return retval;
}
private List<Long> getNoMatchPersonPids(IBaseResource theBaseResource) {
Long targetPid = myIdHelperService.getPidOrNull(theBaseResource);
return myEmpiLinkDaoSvc.getEmpiLinksByTargetPidAndMatchResult(targetPid, EmpiMatchResultEnum.NO_MATCH)
.stream()
.map(EmpiLink::getPersonPid)
.collect(Collectors.toList());
}
private ResourcePersistentId getResourcePersistentId(Long thePersonPid) {
return new ResourcePersistentId(thePersonPid);
}
@Override
protected CandidateStrategyEnum getStrategy() {
return CandidateStrategyEnum.SCORE;
}
}

View File

@ -1,52 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc.candidate;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
public class MatchedPersonCandidate {
private final ResourcePersistentId myCandidatePersonPid;
private final EmpiMatchOutcome myEmpiMatchOutcome;
public MatchedPersonCandidate(ResourcePersistentId theCandidate, EmpiMatchOutcome theEmpiMatchOutcome) {
myCandidatePersonPid = theCandidate;
myEmpiMatchOutcome = theEmpiMatchOutcome;
}
public MatchedPersonCandidate(ResourcePersistentId thePersonPid, EmpiLink theEmpiLink) {
myCandidatePersonPid = thePersonPid;
myEmpiMatchOutcome = new EmpiMatchOutcome(theEmpiLink.getVector(), theEmpiLink.getScore()).setMatchResultEnum(theEmpiLink.getMatchResult());
}
public ResourcePersistentId getCandidatePersonPid() {
return myCandidatePersonPid;
}
public EmpiMatchOutcome getMatchResult() {
return myEmpiMatchOutcome;
}
public boolean isMatch() {
return myEmpiMatchOutcome.isMatch();
}
}

View File

@ -1,432 +0,0 @@
package ca.uhn.fhir.jpa.empi;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc;
import ca.uhn.fhir.empi.util.EIDHelper;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.data.IEmpiLinkDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.config.EmpiConsumerConfig;
import ca.uhn.fhir.jpa.empi.config.EmpiSearchParameterLoader;
import ca.uhn.fhir.jpa.empi.config.EmpiSubmitterConfig;
import ca.uhn.fhir.jpa.empi.config.TestEmpiConfigR4;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.empi.matcher.IsLinkedTo;
import ca.uhn.fhir.jpa.empi.matcher.IsMatchedToAPerson;
import ca.uhn.fhir.jpa.empi.matcher.IsPossibleDuplicateOf;
import ca.uhn.fhir.jpa.empi.matcher.IsPossibleLinkedTo;
import ca.uhn.fhir.jpa.empi.matcher.IsPossibleMatchWith;
import ca.uhn.fhir.jpa.empi.matcher.IsSamePersonAs;
import ca.uhn.fhir.jpa.empi.svc.EmpiMatchLinkSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.hamcrest.Matcher;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.ContactPoint;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Person;
import org.hl7.fhir.r4.model.Practitioner;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.slf4j.LoggerFactory.getLogger;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {EmpiSubmitterConfig.class, EmpiConsumerConfig.class, TestEmpiConfigR4.class, SubscriptionProcessorConfig.class})
abstract public class BaseEmpiR4Test extends BaseJpaR4Test {
private static final Logger ourLog = getLogger(BaseEmpiR4Test.class);
public static final String NAME_GIVEN_JANE = "Jane";
public static final String NAME_GIVEN_PAUL = "Paul";
public static final String TEST_NAME_FAMILY = "Doe";
protected static final String TEST_ID_SYSTEM = "http://a.tv/";
protected static final String JANE_ID = "ID.JANE.123";
protected static final String PAUL_ID = "ID.PAUL.456";
private static final ContactPoint TEST_TELECOM = new ContactPoint()
.setSystem(ContactPoint.ContactPointSystem.PHONE)
.setValue("555-555-5555");
private static final String NAME_GIVEN_FRANK = "Frank";
protected static final String FRANK_ID = "ID.FRANK.789";
@Autowired
protected FhirContext myFhirContext;
@Autowired
protected IFhirResourceDao<Person> myPersonDao;
@Autowired
protected IFhirResourceDao<Patient> myPatientDao;
@Autowired
protected IFhirResourceDao<Practitioner> myPractitionerDao;
@Autowired
protected EmpiResourceMatcherSvc myEmpiResourceMatcherSvc;
@Autowired
protected IEmpiLinkDao myEmpiLinkDao;
@Autowired
protected EmpiLinkDaoSvc myEmpiLinkDaoSvc;
@Autowired
protected IdHelperService myIdHelperService;
@Autowired
protected IEmpiSettings myEmpiConfig;
@Autowired
protected EmpiMatchLinkSvc myEmpiMatchLinkSvc;
@Autowired
protected EIDHelper myEIDHelper;
@Autowired
EmpiSearchParameterLoader myEmpiSearchParameterLoader;
@Autowired
SearchParamRegistryImpl mySearchParamRegistry;
@Autowired
private IInterceptorBroadcaster myInterceptorBroadcaster;
protected ServletRequestDetails myRequestDetails;
@BeforeEach
public void beforeSetRequestDetails() {
myRequestDetails = new ServletRequestDetails(myInterceptorBroadcaster);
}
@Override
@AfterEach
public void after() throws IOException {
myEmpiLinkDao.deleteAll();
assertEquals(0, myEmpiLinkDao.count());
super.after();
}
protected void saveLink(EmpiLink theEmpiLink) {
myEmpiLinkDaoSvc.save(theEmpiLink);
}
@Nonnull
protected Person createUnmanagedPerson() {
return createPerson(new Person(), false);
}
@Nonnull
protected Person createPerson() {
return createPerson(new Person(), true);
}
@Nonnull
protected Patient createPatient() {
return createPatient(new Patient());
}
@Nonnull
protected Person createPerson(Person thePerson) {
return createPerson(thePerson, true);
}
@Nonnull
protected Person createPerson(Person thePerson, boolean theEmpiManaged) {
if (theEmpiManaged) {
thePerson.getMeta().addTag().setSystem(EmpiConstants.SYSTEM_EMPI_MANAGED).setCode(EmpiConstants.CODE_HAPI_EMPI_MANAGED);
thePerson.setActive(true);
}
DaoMethodOutcome outcome = myPersonDao.create(thePerson);
Person person = (Person) outcome.getResource();
person.setId(outcome.getId());
return person;
}
@Nonnull
protected Patient createPatient(Patient thePatient) {
//Note that since our empi-rules block on active=true, all patients must be active.
thePatient.setActive(true);
DaoMethodOutcome outcome = myPatientDao.create(thePatient);
Patient patient = (Patient) outcome.getResource();
patient.setId(outcome.getId());
return patient;
}
@Nonnull
protected Practitioner createPractitioner(Practitioner thePractitioner) {
//Note that since our empi-rules block on active=true, all patients must be active.
thePractitioner.setActive(true);
DaoMethodOutcome daoMethodOutcome = myPractitionerDao.create(thePractitioner);
thePractitioner.setId(daoMethodOutcome.getId());
return thePractitioner;
}
@Nonnull
protected Patient buildPatientWithNameAndId(String theGivenName, String theId) {
return buildPatientWithNameIdAndBirthday(theGivenName, theId, null);
}
@Nonnull
protected Practitioner buildPractitionerWithNameAndId(String theGivenName, String theId) {
return buildPractitionerWithNameIdAndBirthday(theGivenName, theId, null);
}
@Nonnull
protected Person buildPersonWithNameAndId(String theGivenName, String theId) {
return buildPersonWithNameIdAndBirthday(theGivenName, theId, null);
}
@Nonnull
protected Patient buildPatientWithNameIdAndBirthday(String theGivenName, String theId, Date theBirthday) {
Patient patient = new Patient();
patient.getNameFirstRep().addGiven(theGivenName);
patient.getNameFirstRep().setFamily(TEST_NAME_FAMILY);
patient.addIdentifier().setSystem(TEST_ID_SYSTEM).setValue(theId);
patient.setBirthDate(theBirthday);
patient.setTelecom(Collections.singletonList(TEST_TELECOM));
DateType dateType = new DateType(theBirthday);
dateType.setPrecision(TemporalPrecisionEnum.DAY);
patient.setBirthDateElement(dateType);
return patient;
}
@Nonnull
protected Practitioner buildPractitionerWithNameIdAndBirthday(String theGivenName, String theId, Date theBirthday) {
Practitioner practitioner = new Practitioner();
practitioner.addName().addGiven(theGivenName);
practitioner.addName().setFamily(TEST_NAME_FAMILY);
practitioner.addIdentifier().setSystem(TEST_ID_SYSTEM).setValue(theId);
practitioner.setBirthDate(theBirthday);
practitioner.setTelecom(Collections.singletonList(TEST_TELECOM));
DateType dateType = new DateType(theBirthday);
dateType.setPrecision(TemporalPrecisionEnum.DAY);
practitioner.setBirthDateElement(dateType);
return practitioner;
}
@Nonnull
protected Person buildPersonWithNameIdAndBirthday(String theGivenName, String theId, Date theBirthday) {
Person person = new Person();
person.addName().addGiven(theGivenName);
person.addName().setFamily(TEST_NAME_FAMILY);
person.addIdentifier().setSystem(TEST_ID_SYSTEM).setValue(theId);
person.setBirthDate(theBirthday);
DateType dateType = new DateType(theBirthday);
dateType.setPrecision(TemporalPrecisionEnum.DAY);
person.setBirthDateElement(dateType);
return person;
}
@Nonnull
protected Patient buildJanePatient() {
return buildPatientWithNameAndId(NAME_GIVEN_JANE, JANE_ID);
}
@Nonnull
protected Practitioner buildJanePractitioner() {
return buildPractitionerWithNameAndId(NAME_GIVEN_JANE, JANE_ID);
}
@Nonnull
protected Person buildJanePerson() {
return buildPersonWithNameAndId(NAME_GIVEN_JANE, JANE_ID);
}
@Nonnull
protected Patient buildPaulPatient() {
return buildPatientWithNameAndId(NAME_GIVEN_PAUL, PAUL_ID);
}
@Nonnull
protected Patient buildFrankPatient() {
return buildPatientWithNameAndId(NAME_GIVEN_FRANK, FRANK_ID);
}
@Nonnull
protected Patient buildJaneWithBirthday(Date theToday) {
return buildPatientWithNameIdAndBirthday(NAME_GIVEN_JANE, JANE_ID, theToday);
}
protected void assertLinkCount(long theExpectedCount) {
assertEquals(theExpectedCount, myEmpiLinkDao.count());
}
protected Person getPersonFromTarget(IAnyResource theBaseResource) {
Optional<EmpiLink> matchedLinkForTargetPid = myEmpiLinkDaoSvc.getMatchedLinkForTargetPid(myIdHelperService.getPidOrNull(theBaseResource));
if (matchedLinkForTargetPid.isPresent()) {
Long personPid = matchedLinkForTargetPid.get().getPersonPid();
return (Person) myPersonDao.readByPid(new ResourcePersistentId(personPid));
} else {
return null;
}
}
protected Person getPersonFromEmpiLink(EmpiLink theEmpiLink) {
return (Person) myPersonDao.readByPid(new ResourcePersistentId(theEmpiLink.getPersonPid()));
}
protected Patient addExternalEID(Patient thePatient, String theEID) {
thePatient.addIdentifier().setSystem(myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem()).setValue(theEID);
return thePatient;
}
protected Person addExternalEID(Person thePerson, String theEID) {
thePerson.addIdentifier().setSystem(myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem()).setValue(theEID);
return thePerson;
}
protected Patient clearExternalEIDs(Patient thePatient) {
thePatient.getIdentifier().removeIf(theIdentifier -> theIdentifier.getSystem().equalsIgnoreCase(myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem()));
return thePatient;
}
protected Patient createPatientAndUpdateLinks(Patient thePatient) {
thePatient = createPatient(thePatient);
myEmpiMatchLinkSvc.updateEmpiLinksForEmpiTarget(thePatient, createContextForCreate());
return thePatient;
}
protected EmpiTransactionContext createContextForCreate() {
EmpiTransactionContext ctx = new EmpiTransactionContext();
ctx.setRestOperation(EmpiTransactionContext.OperationType.CREATE_RESOURCE);
ctx.setTransactionLogMessages(null);
return ctx;
}
protected EmpiTransactionContext createContextForUpdate() {
EmpiTransactionContext ctx = new EmpiTransactionContext();
ctx.setRestOperation(EmpiTransactionContext.OperationType.UPDATE_RESOURCE);
ctx.setTransactionLogMessages(null);
return ctx;
}
protected Patient updatePatientAndUpdateLinks(Patient thePatient) {
thePatient = (Patient) myPatientDao.update(thePatient).getResource();
myEmpiMatchLinkSvc.updateEmpiLinksForEmpiTarget(thePatient, createContextForUpdate());
return thePatient;
}
protected Practitioner createPractitionerAndUpdateLinks(Practitioner thePractitioner) {
thePractitioner.setActive(true);
DaoMethodOutcome daoMethodOutcome = myPractitionerDao.create(thePractitioner);
thePractitioner.setId(daoMethodOutcome.getId());
myEmpiMatchLinkSvc.updateEmpiLinksForEmpiTarget(thePractitioner, createContextForCreate());
return thePractitioner;
}
protected Matcher<IAnyResource> samePersonAs(IAnyResource... theBaseResource) {
return IsSamePersonAs.samePersonAs(myIdHelperService, myEmpiLinkDaoSvc, theBaseResource);
}
protected Matcher<IAnyResource> linkedTo(IAnyResource... theBaseResource) {
return IsLinkedTo.linkedTo(myIdHelperService, myEmpiLinkDaoSvc, theBaseResource);
}
protected Matcher<IAnyResource> possibleLinkedTo(IAnyResource... theBaseResource) {
return IsPossibleLinkedTo.possibleLinkedTo(myIdHelperService, myEmpiLinkDaoSvc, theBaseResource);
}
protected Matcher<IAnyResource> possibleMatchWith(IAnyResource... theBaseResource) {
return IsPossibleMatchWith.possibleMatchWith(myIdHelperService, myEmpiLinkDaoSvc, theBaseResource);
}
protected Matcher<IAnyResource> possibleDuplicateOf(IAnyResource... theBaseResource) {
return IsPossibleDuplicateOf.possibleDuplicateOf(myIdHelperService, myEmpiLinkDaoSvc, theBaseResource);
}
protected Matcher<IAnyResource> matchedToAPerson() {
return IsMatchedToAPerson.matchedToAPerson(myIdHelperService, myEmpiLinkDaoSvc);
}
protected Person getOnlyActivePerson() {
List<IBaseResource> resources = getAllActivePersons();
assertEquals(1, resources.size());
return (Person) resources.get(0);
}
@Nonnull
protected List<IBaseResource> getAllActivePersons() {
return getAllPersons(true);
}
@Nonnull
protected List<IBaseResource> getAllPersons() {
return getAllPersons(false);
}
@Nonnull
private List<IBaseResource> getAllPersons(boolean theOnlyActive) {
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
if (theOnlyActive) {
map.add("active", new TokenParam().setValue("true"));
}
IBundleProvider bundle = myPersonDao.search(map);
return bundle.getResources(0, 999);
}
@Nonnull
protected EmpiLink createResourcesAndBuildTestEmpiLink() {
Person person = createPerson();
Patient patient = createPatient();
EmpiLink empiLink = myEmpiLinkDaoSvc.newEmpiLink();
empiLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
empiLink.setMatchResult(EmpiMatchResultEnum.MATCH);
empiLink.setPersonPid(myIdHelperService.getPidOrNull(person));
empiLink.setTargetPid(myIdHelperService.getPidOrNull(patient));
return empiLink;
}
protected void loadEmpiSearchParameters() {
myEmpiSearchParameterLoader.daoUpdateEmpiSearchParameters();
mySearchParamRegistry.forceRefresh();
}
protected void logAllLinks() {
ourLog.info("Logging all EMPI Links:");
List<EmpiLink> links = myEmpiLinkDao.findAll();
for (EmpiLink link : links) {
ourLog.info(link.toString());
}
}
protected void assertLinksMatchResult(EmpiMatchResultEnum... theExpectedValues) {
assertFields(EmpiLink::getMatchResult, theExpectedValues);
}
protected void assertLinksNewPerson(Boolean... theExpectedValues) {
assertFields(EmpiLink::getNewPerson, theExpectedValues);
}
protected void assertLinksMatchedByEid(Boolean... theExpectedValues) {
assertFields(EmpiLink::getEidMatch, theExpectedValues);
}
private <T> void assertFields(Function<EmpiLink, T> theAccessor, T... theExpectedValues) {
List<EmpiLink> links = myEmpiLinkDao.findAll();
assertEquals(theExpectedValues.length, links.size());
for (int i = 0; i < links.size(); ++i) {
assertEquals(theExpectedValues[i], theAccessor.apply(links.get(i)), "Value at index " + i + " was not equal");
}
}
}

View File

@ -1,55 +0,0 @@
package ca.uhn.fhir.jpa.empi.dao;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.rules.json.EmpiRulesJson;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.jpa.util.TestUtil;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class EmpiLinkDaoSvcTest extends BaseEmpiR4Test {
@Autowired
EmpiLinkDaoSvc myEmpiLinkDaoSvc;
@Autowired
IEmpiSettings myEmpiSettings;
@Test
public void testCreate() {
EmpiLink empiLink = createResourcesAndBuildTestEmpiLink();
assertThat(empiLink.getCreated(), is(nullValue()));
assertThat(empiLink.getUpdated(), is(nullValue()));
myEmpiLinkDaoSvc.save(empiLink);
assertThat(empiLink.getCreated(), is(notNullValue()));
assertThat(empiLink.getUpdated(), is(notNullValue()));
assertTrue(empiLink.getUpdated().getTime() - empiLink.getCreated().getTime() < 1000);
}
@Test
public void testUpdate() {
EmpiLink createdLink = myEmpiLinkDaoSvc.save(createResourcesAndBuildTestEmpiLink());
assertThat(createdLink.getLinkSource(), is(EmpiLinkSourceEnum.MANUAL));
TestUtil.sleepOneClick();
createdLink.setLinkSource(EmpiLinkSourceEnum.AUTO);
EmpiLink updatedLink = myEmpiLinkDaoSvc.save(createdLink);
assertNotEquals(updatedLink.getCreated(), updatedLink.getUpdated());
}
@Test
public void testNew() {
EmpiLink newLink = myEmpiLinkDaoSvc.newEmpiLink();
EmpiRulesJson rules = myEmpiSettings.getEmpiRules();
assertEquals("1", rules.getVersion());
assertEquals(rules.getVersion(), newLink.getVersion());
}
}

View File

@ -1,20 +0,0 @@
package ca.uhn.fhir.jpa.empi.entity;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class EmpiEnumTest {
@Test
public void empiEnumOrdinals() {
// This test is here to enforce that new values in these enums are always added to the end
assertEquals(6, EmpiMatchResultEnum.values().length);
assertEquals(EmpiMatchResultEnum.REDIRECT, EmpiMatchResultEnum.values()[EmpiMatchResultEnum.values().length - 1]);
assertEquals(2, EmpiLinkSourceEnum.values().length);
assertEquals(EmpiLinkSourceEnum.MANUAL, EmpiLinkSourceEnum.values()[EmpiLinkSourceEnum.values().length - 1]);
}
}

View File

@ -1,31 +0,0 @@
package ca.uhn.fhir.jpa.empi.helper;
import ca.uhn.fhir.jpa.dao.data.IEmpiLinkDao;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.model.primitive.IdDt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class EmpiLinkHelper {
private static final Logger ourLog = LoggerFactory.getLogger(EmpiLinkHelper.class);
@Autowired
IEmpiLinkDao myEmpiLinkDao;
@Transactional
public void logEmpiLinks() {
List<EmpiLink> links = myEmpiLinkDao.findAll();
ourLog.info("All EMPI Links:");
for (EmpiLink link : links) {
IdDt personId = link.getPerson().getIdDt().toVersionless();
IdDt targetId = link.getTarget().getIdDt().toVersionless();
ourLog.info("{}: {}, {}, {}, {}", link.getId(), personId, targetId, link.getMatchResult(), link.getLinkSource());
}
}
}

View File

@ -1,299 +0,0 @@
package ca.uhn.fhir.jpa.empi.interceptor;
import ca.uhn.fhir.empi.model.CanonicalEID;
import ca.uhn.fhir.empi.rules.config.EmpiSettings;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.jpa.empi.helper.EmpiHelperConfig;
import ca.uhn.fhir.jpa.empi.helper.EmpiHelperR4;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.server.TransactionLogMessages;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Person;
import org.hl7.fhir.r4.model.SearchParameter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import java.util.Date;
import java.util.List;
import static ca.uhn.fhir.empi.api.EmpiConstants.CODE_HAPI_EMPI_MANAGED;
import static ca.uhn.fhir.empi.api.EmpiConstants.SYSTEM_EMPI_MANAGED;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import static org.slf4j.LoggerFactory.getLogger;
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
@ContextConfiguration(classes = {EmpiHelperConfig.class})
public class EmpiStorageInterceptorIT extends BaseEmpiR4Test {
private static final Logger ourLog = getLogger(EmpiStorageInterceptorIT.class);
@RegisterExtension
@Autowired
public EmpiHelperR4 myEmpiHelper;
@Autowired
private IdHelperService myIdHelperService;
@BeforeEach
public void before() {
super.loadEmpiSearchParameters();
}
@Test
public void testCreatePractitioner() throws InterruptedException {
myEmpiHelper.createWithLatch(buildPractitionerWithNameAndId("somename", "some_id"));
assertLinkCount(1);
}
@Test
public void testCreatePerson() {
myPersonDao.create(new Person());
assertLinkCount(0);
}
@Test
public void testDeletePersonDeletesLinks() throws InterruptedException {
myEmpiHelper.createWithLatch(buildPaulPatient());
assertLinkCount(1);
Person person = getOnlyActivePerson();
myPersonDao.delete(person.getIdElement());
assertLinkCount(0);
}
@Test
public void testCreatePersonWithEmpiTagForbidden() throws InterruptedException {
//Creating a person with the EMPI-MANAGED tag should fail
Person person = new Person();
person.getMeta().addTag(SYSTEM_EMPI_MANAGED, CODE_HAPI_EMPI_MANAGED, "User is managed by EMPI");
try {
myEmpiHelper.doCreateResource(person, true);
fail();
} catch (ForbiddenOperationException e) {
assertEquals("Cannot create or modify Resources that are managed by EMPI.", e.getMessage());
}
}
@Test
public void testCreatingPersonWithInsufficentEMPIAttributesIsNotEMPIProcessed() throws InterruptedException {
myEmpiHelper.doCreateResource(new Patient(), true);
assertLinkCount(0);
}
@Test
public void testCreatingPatientWithOneOrMoreMatchingAttributesIsEMPIProcessed() throws InterruptedException {
myEmpiHelper.createWithLatch(buildPaulPatient());
assertLinkCount(1);
}
@Test
public void testCreateOrganizationWithEmpiTagForbidden() throws InterruptedException {
//Creating a organization with the EMPI-MANAGED tag should fail
Organization organization = new Organization();
organization.getMeta().addTag(SYSTEM_EMPI_MANAGED, CODE_HAPI_EMPI_MANAGED, "User is managed by EMPI");
try {
myEmpiHelper.doCreateResource(organization, true);
fail();
} catch (ForbiddenOperationException e) {
assertEquals("Cannot create or modify Resources that are managed by EMPI.", e.getMessage());
}
}
@Test
public void testUpdateOrganizationWithEmpiTagForbidden() throws InterruptedException {
//Creating a organization with the EMPI-MANAGED tag should fail
Organization organization = new Organization();
myEmpiHelper.doCreateResource(organization, true);
organization.getMeta().addTag(SYSTEM_EMPI_MANAGED, CODE_HAPI_EMPI_MANAGED, "User is managed by EMPI");
try {
myEmpiHelper.doUpdateResource(organization, true);
fail();
} catch (ForbiddenOperationException e) {
assertEquals("The HAPI-EMPI tag on a resource may not be changed once created.", e.getMessage());
}
}
@Test
public void testPersonRecordsManagedByEmpiAllShareSameTag() throws InterruptedException {
myEmpiHelper.createWithLatch(buildJanePatient());
myEmpiHelper.createWithLatch(buildPaulPatient());
IBundleProvider search = myPersonDao.search(new SearchParameterMap().setLoadSynchronous(true));
List<IBaseResource> resources = search.getResources(0, search.size());
for (IBaseResource person : resources) {
assertThat(person.getMeta().getTag(SYSTEM_EMPI_MANAGED, CODE_HAPI_EMPI_MANAGED), is(notNullValue()));
}
}
@Test
public void testNonEmpiManagedPersonCannotHaveEmpiManagedTagAddedToThem() {
//Person created manually.
Person person = new Person();
DaoMethodOutcome daoMethodOutcome = myEmpiHelper.doCreateResource(person, true);
assertNotNull(daoMethodOutcome.getId());
//Updating that person to set them as EMPI managed is not allowed.
person.getMeta().addTag(SYSTEM_EMPI_MANAGED, CODE_HAPI_EMPI_MANAGED, "User is managed by EMPI");
try {
myEmpiHelper.doUpdateResource(person, true);
fail();
} catch (ForbiddenOperationException e) {
assertEquals("The HAPI-EMPI tag on a resource may not be changed once created.", e.getMessage());
}
}
@Test
public void testEmpiManagedPersonCannotBeModifiedByPersonUpdateRequest() throws InterruptedException {
// When EMPI is enabled, only the EMPI system is allowed to modify Person links of Persons with the EMPI-MANAGED tag.
Patient patient = new Patient();
IIdType patientId = myEmpiHelper.createWithLatch(buildPaulPatient()).getDaoMethodOutcome().getId().toUnqualifiedVersionless();
patient.setId(patientId);
//Updating a Person who was created via EMPI should fail.
EmpiLink empiLink = myEmpiLinkDaoSvc.getMatchedLinkForTargetPid(myIdHelperService.getPidOrNull(patient)).get();
Long personPid = empiLink.getPersonPid();
Person empiPerson = (Person) myPersonDao.readByPid(new ResourcePersistentId(personPid));
empiPerson.setGender(Enumerations.AdministrativeGender.MALE);
try {
myEmpiHelper.doUpdateResource(empiPerson, true);
fail();
} catch (ForbiddenOperationException e) {
assertEquals("Cannot create or modify Resources that are managed by EMPI.", e.getMessage());
}
}
@Test
public void testEmpiPointcutReceivesTransactionLogMessages() throws InterruptedException {
EmpiHelperR4.OutcomeAndLogMessageWrapper wrapper = myEmpiHelper.createWithLatch(buildJanePatient());
TransactionLogMessages empiTransactionLogMessages = wrapper.getLogMessages();
//There is no TransactionGuid here as there is no TransactionLog in this context.
assertThat(empiTransactionLogMessages.getTransactionGuid(), is(nullValue()));
List<String> messages = empiTransactionLogMessages.getValues();
assertThat(messages.isEmpty(), is(false));
}
@Test
public void testWhenASingularPatientUpdatesExternalEidThatPersonEidIsUpdated() throws InterruptedException {
Patient jane = addExternalEID(buildJanePatient(), "some_eid");
EmpiHelperR4.OutcomeAndLogMessageWrapper latch = myEmpiHelper.createWithLatch(jane);
jane.setId(latch.getDaoMethodOutcome().getId());
clearExternalEIDs(jane);
jane = addExternalEID(jane, "some_new_eid");
EmpiHelperR4.OutcomeAndLogMessageWrapper outcomeWrapper = myEmpiHelper.updateWithLatch(jane);
Person person = getPersonFromTarget(jane);
List<CanonicalEID> externalEids = myEIDHelper.getExternalEid(person);
assertThat(externalEids, hasSize(1));
assertThat("some_new_eid", is(equalTo(externalEids.get(0).getValue())));
}
@Test
public void testWhenEidUpdatesAreDisabledForbidsUpdatesToEidsOnTargets() throws InterruptedException {
setPreventEidUpdates(true);
Patient jane = addExternalEID(buildJanePatient(), "some_eid");
EmpiHelperR4.OutcomeAndLogMessageWrapper latch = myEmpiHelper.createWithLatch(jane);
jane.setId(latch.getDaoMethodOutcome().getId());
clearExternalEIDs(jane);
jane = addExternalEID(jane, "some_new_eid");
try {
myEmpiHelper.doUpdateResource(jane, true);
fail();
} catch (ForbiddenOperationException e) {
assertThat(e.getMessage(), is(equalTo("While running with EID updates disabled, EIDs may not be updated on Patient/Practitioner resources")));
}
setPreventEidUpdates(false);
}
@Test
public void testWhenMultipleEidsAreDisabledThatTheInterceptorRejectsCreatesWithThem() {
setPreventMultipleEids(true);
Patient patient = buildJanePatient();
addExternalEID(patient, "123");
addExternalEID(patient, "456");
try {
myEmpiHelper.doCreateResource(patient, true);
fail();
} catch (ForbiddenOperationException e) {
assertThat(e.getMessage(), is(equalTo("While running with multiple EIDs disabled, Patient/Practitioner resources may have at most one EID.")));
}
setPreventMultipleEids(false);
}
@Test
public void testInterceptorHandlesNonEmpiResources() {
setPreventEidUpdates(true);
//Create some arbitrary resource.
SearchParameter fooSp = new SearchParameter();
fooSp.setCode("foo");
fooSp.addBase("Bundle");
fooSp.setType(Enumerations.SearchParamType.REFERENCE);
fooSp.setTitle("FOO SP");
fooSp.setExpression("Bundle.entry[0].resource.as(Composition).encounter");
fooSp.setXpathUsage(org.hl7.fhir.r4.model.SearchParameter.XPathUsageType.NORMAL);
fooSp.setStatus(org.hl7.fhir.r4.model.Enumerations.PublicationStatus.ACTIVE);
myEmpiHelper.doCreateResource(fooSp, true);
fooSp.setXpathUsage(SearchParameter.XPathUsageType.PHONETIC);
myEmpiHelper.doUpdateResource(fooSp, true);
}
@Test
public void testPatientsWithNoEIDCanBeUpdated() throws InterruptedException {
setPreventEidUpdates(true);
Patient p = buildPaulPatient();
EmpiHelperR4.OutcomeAndLogMessageWrapper wrapper = myEmpiHelper.createWithLatch(p);
p.setId(wrapper.getDaoMethodOutcome().getId());
p.setBirthDate(new Date());
myEmpiHelper.updateWithLatch(p);
setPreventEidUpdates(false);
}
@Test
public void testPatientsCanHaveEIDAddedInStrictMode() throws InterruptedException {
setPreventEidUpdates(true);
Patient p = buildPaulPatient();
EmpiHelperR4.OutcomeAndLogMessageWrapper messageWrapper = myEmpiHelper.createWithLatch(p);
p.setId(messageWrapper.getDaoMethodOutcome().getId());
addExternalEID(p, "external eid");
myEmpiHelper.updateWithLatch(p);
setPreventEidUpdates(false);
}
private void setPreventEidUpdates(boolean thePrevent) {
((EmpiSettings) myEmpiConfig).setPreventEidUpdates(thePrevent);
}
private void setPreventMultipleEids(boolean thePrevent) {
((EmpiSettings) myEmpiConfig).setPreventMultipleEids(thePrevent);
}
}

View File

@ -1,79 +0,0 @@
package ca.uhn.fhir.jpa.empi.matcher;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import org.hamcrest.TypeSafeMatcher;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
public abstract class BasePersonMatcher extends TypeSafeMatcher<IAnyResource> {
private static final Logger ourLog = LoggerFactory.getLogger(BasePersonMatcher.class);
protected IdHelperService myIdHelperService;
protected EmpiLinkDaoSvc myEmpiLinkDaoSvc;
protected Collection<IAnyResource> myBaseResources;
protected BasePersonMatcher(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) {
myIdHelperService = theIdHelperService;
myEmpiLinkDaoSvc = theEmpiLinkDaoSvc;
myBaseResources = Arrays.stream(theBaseResource).collect(Collectors.toList());
}
@Nullable
protected Long getMatchedPersonPidFromResource(IAnyResource theResource) {
Long retval;
if (isPatientOrPractitioner(theResource)) {
EmpiLink matchLink = getMatchedEmpiLink(theResource);
retval = matchLink == null ? null : matchLink.getPersonPid();
} else if (isPerson(theResource)) {
retval = myIdHelperService.getPidOrNull(theResource);
} else {
throw new IllegalArgumentException("Resources of type " + theResource.getIdElement().getResourceType() + " cannot be persons!");
}
return retval;
}
protected List<Long> getPossibleMatchedPersonPidsFromTarget(IAnyResource theBaseResource) {
return getEmpiLinksForTarget(theBaseResource, EmpiMatchResultEnum.POSSIBLE_MATCH).stream().map(EmpiLink::getPersonPid).collect(Collectors.toList());
}
protected boolean isPatientOrPractitioner(IAnyResource theResource) {
String resourceType = theResource.getIdElement().getResourceType();
return (resourceType.equalsIgnoreCase("Patient") || resourceType.equalsIgnoreCase("Practitioner"));
}
protected EmpiLink getMatchedEmpiLink(IAnyResource thePatientOrPractitionerResource) {
List<EmpiLink> empiLinks = getEmpiLinksForTarget(thePatientOrPractitionerResource, EmpiMatchResultEnum.MATCH);
if (empiLinks.size() == 0) {
return null;
} else if (empiLinks.size() == 1) {
return empiLinks.get(0);
} else {
throw new IllegalStateException("Its illegal to have more than 1 match for a given target! we found " + empiLinks.size() + " for resource with id: " + thePatientOrPractitionerResource.getIdElement().toUnqualifiedVersionless());
}
}
protected boolean isPerson(IAnyResource theIncomingResource) {
return (theIncomingResource.getIdElement().getResourceType().equalsIgnoreCase("Person"));
}
protected List<EmpiLink> getEmpiLinksForTarget(IAnyResource thePatientOrPractitionerResource, EmpiMatchResultEnum theMatchResult) {
Long pidOrNull = myIdHelperService.getPidOrNull(thePatientOrPractitionerResource);
List<EmpiLink> matchLinkForTarget = myEmpiLinkDaoSvc.getEmpiLinksByTargetPidAndMatchResult(pidOrNull, theMatchResult);
if (!matchLinkForTarget.isEmpty()) {
return matchLinkForTarget;
} else {
return new ArrayList<>();
}
}
}

View File

@ -1,48 +0,0 @@
package ca.uhn.fhir.jpa.empi.matcher;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hl7.fhir.instance.model.api.IAnyResource;
import java.util.List;
import java.util.stream.Collectors;
/**
* A Matcher which allows us to check that a target patient/practitioner at a given link level.
* is linked to a set of patients/practitioners via a person.
*
*/
public class IsLinkedTo extends BasePersonMatcher {
private List<Long> baseResourcePersonPids;
private Long incomingResourcePersonPid;
protected IsLinkedTo(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) {
super(theIdHelperService, theEmpiLinkDaoSvc, theBaseResource);
}
@Override
protected boolean matchesSafely(IAnyResource theIncomingResource) {
incomingResourcePersonPid = getMatchedPersonPidFromResource(theIncomingResource);
//OK, lets grab all the person pids of the resources passed in via the constructor.
baseResourcePersonPids = myBaseResources.stream()
.map(this::getMatchedPersonPidFromResource)
.collect(Collectors.toList());
//The resources are linked if all person pids match the incoming person pid.
return baseResourcePersonPids.stream()
.allMatch(pid -> pid.equals(incomingResourcePersonPid));
}
@Override
public void describeTo(Description theDescription) {
}
public static Matcher<IAnyResource> linkedTo(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) {
return new IsLinkedTo(theIdHelperService, theEmpiLinkDaoSvc, theBaseResource);
}
}

View File

@ -1,37 +0,0 @@
package ca.uhn.fhir.jpa.empi.matcher;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.hl7.fhir.instance.model.api.IAnyResource;
import java.util.Optional;
public class IsMatchedToAPerson extends TypeSafeMatcher<IAnyResource> {
private final IdHelperService myIdHelperService;
private final EmpiLinkDaoSvc myEmpiLinkDaoSvc;
public IsMatchedToAPerson(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc) {
myIdHelperService = theIdHelperService;
myEmpiLinkDaoSvc = theEmpiLinkDaoSvc;
}
@Override
protected boolean matchesSafely(IAnyResource theIncomingResource) {
Optional<EmpiLink> matchedLinkForTargetPid = myEmpiLinkDaoSvc.getMatchedLinkForTargetPid(myIdHelperService.getPidOrNull(theIncomingResource));
return matchedLinkForTargetPid.isPresent();
}
@Override
public void describeTo(Description theDescription) {
theDescription.appendText("patient/practitioner was not linked to a Person.");
}
public static Matcher<IAnyResource> matchedToAPerson(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc) {
return new IsMatchedToAPerson(theIdHelperService, theEmpiLinkDaoSvc);
}
}

View File

@ -1,61 +0,0 @@
package ca.uhn.fhir.jpa.empi.matcher;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hl7.fhir.instance.model.api.IAnyResource;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class IsPossibleDuplicateOf extends BasePersonMatcher {
/**
* Matcher with tells us if there is an EmpiLink with between these two resources that are considered POSSIBLE DUPLICATE.
* For use only on persons.
*/
private Long incomingPersonPid;
protected IsPossibleDuplicateOf(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) {
super(theIdHelperService, theEmpiLinkDaoSvc, theBaseResource);
}
@Override
protected boolean matchesSafely(IAnyResource theIncomingResource) {
incomingPersonPid = getMatchedPersonPidFromResource(theIncomingResource);
List<Long> personPidsToMatch = myBaseResources.stream()
.map(this::getMatchedPersonPidFromResource)
.collect(Collectors.toList());
//Returns true if there is a POSSIBLE_DUPLICATE between the incoming resource, and all of the resources passed in via the constructor.
return personPidsToMatch.stream()
.map(baseResourcePid -> {
Optional<EmpiLink> duplicateLink = myEmpiLinkDaoSvc.getEmpiLinksByPersonPidTargetPidAndMatchResult(baseResourcePid, incomingPersonPid, EmpiMatchResultEnum.POSSIBLE_DUPLICATE);
if (!duplicateLink.isPresent()) {
duplicateLink = myEmpiLinkDaoSvc.getEmpiLinksByPersonPidTargetPidAndMatchResult(incomingPersonPid, baseResourcePid, EmpiMatchResultEnum.POSSIBLE_DUPLICATE);
}
return duplicateLink;
}).allMatch(Optional::isPresent);
}
@Override
public void describeTo(Description theDescription) {
theDescription.appendText("Person was not duplicate of Person/" + incomingPersonPid);
}
@Override
protected void describeMismatchSafely(IAnyResource item, Description mismatchDescription) {
super.describeMismatchSafely(item, mismatchDescription);
mismatchDescription.appendText("No Empi Link With POSSIBLE_DUPLICATE was found");
}
public static Matcher<IAnyResource> possibleDuplicateOf(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) {
return new IsPossibleDuplicateOf(theIdHelperService, theEmpiLinkDaoSvc, theBaseResource);
}
}

View File

@ -1,47 +0,0 @@
package ca.uhn.fhir.jpa.empi.matcher;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hl7.fhir.instance.model.api.IAnyResource;
import java.util.List;
import java.util.stream.Collectors;
/**
* A Matcher which allows us to check that a target patient/practitioner at a given link level.
* is linked to a set of patients/practitioners via a person.
*
*/
public class IsPossibleLinkedTo extends BasePersonMatcher {
private List<Long> baseResourcePersonPids;
private Long incomingResourcePersonPid;
protected IsPossibleLinkedTo(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theTargetResources) {
super(theIdHelperService, theEmpiLinkDaoSvc, theTargetResources);
}
@Override
protected boolean matchesSafely(IAnyResource thePersonResource) {
incomingResourcePersonPid = myIdHelperService.getPidOrNull(thePersonResource);;
//OK, lets grab all the person pids of the resources passed in via the constructor.
baseResourcePersonPids = myBaseResources.stream()
.flatMap(iBaseResource -> getPossibleMatchedPersonPidsFromTarget(iBaseResource).stream())
.collect(Collectors.toList());
//The resources are linked if all person pids match the incoming person pid.
return baseResourcePersonPids.stream()
.allMatch(pid -> pid.equals(incomingResourcePersonPid));
}
@Override
public void describeTo(Description theDescription) {
}
public static Matcher<IAnyResource> possibleLinkedTo(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) {
return new IsPossibleLinkedTo(theIdHelperService, theEmpiLinkDaoSvc, theBaseResource);
}
}

View File

@ -1,58 +0,0 @@
package ca.uhn.fhir.jpa.empi.matcher;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hl7.fhir.instance.model.api.IAnyResource;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Matcher with tells us if there is an EmpiLink with between these two resources that are considered POSSIBLE_MATCH
*/
public class IsPossibleMatchWith extends BasePersonMatcher {
protected IsPossibleMatchWith(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) {
super(theIdHelperService, theEmpiLinkDaoSvc, theBaseResource);
}
@Override
protected boolean matchesSafely(IAnyResource theIncomingResource) {
List<EmpiLink> empiLinks = getEmpiLinksForTarget(theIncomingResource, EmpiMatchResultEnum.POSSIBLE_MATCH);
List<Long> personPidsToMatch = myBaseResources.stream()
.map(this::getMatchedPersonPidFromResource)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (personPidsToMatch.isEmpty()) {
personPidsToMatch = myBaseResources.stream()
.flatMap(iBaseResource -> getPossibleMatchedPersonPidsFromTarget(iBaseResource).stream())
.collect(Collectors.toList());
}
List<Long> empiLinkSourcePersonPids = empiLinks.stream().map(EmpiLink::getPersonPid).collect(Collectors.toList());
return empiLinkSourcePersonPids.containsAll(personPidsToMatch);
}
@Override
public void describeTo(Description theDescription) {
theDescription.appendText(" no link found with POSSIBLE_MATCH to the requested PIDS");
}
@Override
protected void describeMismatchSafely(IAnyResource item, Description mismatchDescription) {
super.describeMismatchSafely(item, mismatchDescription);
mismatchDescription.appendText("No Empi Link With POSSIBLE_MATCH was found");
}
public static Matcher<IAnyResource> possibleMatchWith(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) {
return new IsPossibleMatchWith(theIdHelperService, theEmpiLinkDaoSvc, theBaseResource);
}
}

View File

@ -1,46 +0,0 @@
package ca.uhn.fhir.jpa.empi.matcher;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hl7.fhir.instance.model.api.IAnyResource;
import java.util.List;
import java.util.stream.Collectors;
public class IsSamePersonAs extends BasePersonMatcher {
private List<Long> personPidsToMatch;
private Long incomingPersonPid;
public IsSamePersonAs(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) {
super(theIdHelperService, theEmpiLinkDaoSvc, theBaseResource);
}
@Override
protected boolean matchesSafely(IAnyResource theIncomingResource) {
incomingPersonPid = getMatchedPersonPidFromResource(theIncomingResource);
personPidsToMatch = myBaseResources.stream().map(this::getMatchedPersonPidFromResource).collect(Collectors.toList());
boolean allToCheckAreSame = personPidsToMatch.stream().allMatch(pid -> pid.equals(personPidsToMatch.get(0)));
if (!allToCheckAreSame) {
throw new IllegalStateException("You wanted to do a person comparison, but the pool of persons you submitted for checking don't match! We won't even check the incoming person against them.");
}
return personPidsToMatch.contains(incomingPersonPid);
}
@Override
public void describeTo(Description theDescription) {
theDescription.appendText("patient/practitioner linked to Person/" + personPidsToMatch);
}
@Override
protected void describeMismatchSafely(IAnyResource item, Description mismatchDescription) {
super.describeMismatchSafely(item, mismatchDescription);
mismatchDescription.appendText(" was actually linked to Person/" + incomingPersonPid);
}
public static Matcher<IAnyResource> samePersonAs(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) {
return new IsSamePersonAs(theIdHelperService, theEmpiLinkDaoSvc, theBaseResource);
}
}

View File

@ -1,52 +0,0 @@
package ca.uhn.fhir.jpa.empi.provider;
import ca.uhn.fhir.empi.api.IEmpiControllerSvc;
import ca.uhn.fhir.empi.api.IEmpiExpungeSvc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.IEmpiSubmitSvc;
import ca.uhn.fhir.empi.provider.EmpiProviderR4;
import ca.uhn.fhir.empi.rules.config.EmpiSettings;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import java.io.IOException;
public abstract class BaseProviderR4Test extends BaseEmpiR4Test {
EmpiProviderR4 myEmpiProviderR4;
@Autowired
private IEmpiMatchFinderSvc myEmpiMatchFinderSvc;
@Autowired
private IEmpiControllerSvc myEmpiControllerSvc;
@Autowired
private IEmpiExpungeSvc myEmpiResetSvc;
@Autowired
private IEmpiSubmitSvc myEmpiBatchSvc;
@Autowired
private EmpiSettings myEmpiSettings;
private String defaultScript;
protected void setEmpiRuleJson(String theString) throws IOException {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
Resource resource = resourceLoader.getResource(theString);
String json = IOUtils.toString(resource.getInputStream(), Charsets.UTF_8);
myEmpiSettings.setScriptText(json);
}
@BeforeEach
public void before() {
myEmpiProviderR4 = new EmpiProviderR4(myFhirContext, myEmpiControllerSvc, myEmpiMatchFinderSvc, myEmpiResetSvc, myEmpiBatchSvc);
defaultScript = myEmpiSettings.getScriptText();
}
@AfterEach
public void after() throws IOException {
super.after();
myEmpiSettings.setScriptText(defaultScript);
}
}

View File

@ -1,127 +0,0 @@
package ca.uhn.fhir.jpa.empi.provider;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.test.concurrency.PointcutLatch;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Person;
import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.fail;
public class EmpiProviderBatchR4Test extends BaseLinkR4Test {
protected Practitioner myPractitioner;
protected StringType myPractitionerId;
protected Person myPractitionerPerson;
protected StringType myPractitionerPersonId;
@Autowired
IInterceptorService myInterceptorService;
PointcutLatch afterEmpiLatch = new PointcutLatch(Pointcut.EMPI_AFTER_PERSISTED_RESOURCE_CHECKED);
@BeforeEach
public void before() {
super.before();
myPractitioner = createPractitionerAndUpdateLinks(buildPractitionerWithNameAndId("some_pract", "some_pract_id"));
myPractitionerId = new StringType(myPractitioner.getIdElement().getValue());
myPractitionerPerson = getPersonFromTarget(myPractitioner);
myPractitionerPersonId = new StringType(myPractitionerPerson.getIdElement().getValue());
myInterceptorService.registerAnonymousInterceptor(Pointcut.EMPI_AFTER_PERSISTED_RESOURCE_CHECKED, afterEmpiLatch);
}
@AfterEach
public void after() throws IOException {
myInterceptorService.unregisterInterceptor(afterEmpiLatch);
super.after();
}
@Test
public void testBatchRunOnAllPractitioners() throws InterruptedException {
StringType criteria = null;
myEmpiProviderR4.clearEmpiLinks(null, myRequestDetails);
afterEmpiLatch.runWithExpectedCount(1, () -> myEmpiProviderR4.empiBatchPractitionerType(criteria, null));
assertLinkCount(1);
}
@Test
public void testBatchRunOnSpecificPractitioner() throws InterruptedException {
myEmpiProviderR4.clearEmpiLinks(null, myRequestDetails);
afterEmpiLatch.runWithExpectedCount(1, () -> myEmpiProviderR4.empiBatchPractitionerInstance(myPractitioner.getIdElement(), null));
assertLinkCount(1);
}
@Test
public void testBatchRunOnNonExistentSpecificPractitioner() {
myEmpiProviderR4.clearEmpiLinks(null, myRequestDetails);
try {
myEmpiProviderR4.empiBatchPractitionerInstance(new IdType("Practitioner/999"), null);
fail();
} catch (ResourceNotFoundException e){}
}
@Test
public void testBatchRunOnAllPatients() throws InterruptedException {
assertLinkCount(2);
StringType criteria = null;
myEmpiProviderR4.clearEmpiLinks(null, myRequestDetails);
afterEmpiLatch.runWithExpectedCount(1, () -> myEmpiProviderR4.empiBatchPatientType(criteria, null));
assertLinkCount(1);
}
@Test
public void testBatchRunOnSpecificPatient() throws InterruptedException {
assertLinkCount(2);
myEmpiProviderR4.clearEmpiLinks(null, myRequestDetails);
afterEmpiLatch.runWithExpectedCount(1, () -> myEmpiProviderR4.empiBatchPatientInstance(myPatient.getIdElement(), null));
assertLinkCount(1);
}
@Test
public void testBatchRunOnNonExistentSpecificPatient() {
assertLinkCount(2);
myEmpiProviderR4.clearEmpiLinks(null, myRequestDetails);
try {
myEmpiProviderR4.empiBatchPatientInstance(new IdType("Patient/999"), null);
fail();
} catch (ResourceNotFoundException e){}
}
@Test
public void testBatchRunOnAllTypes() throws InterruptedException {
assertLinkCount(2);
StringType criteria = new StringType("");
myEmpiProviderR4.clearEmpiLinks(null, myRequestDetails);
afterEmpiLatch.runWithExpectedCount(2, () -> {
myEmpiProviderR4.empiBatchOnAllTargets(criteria, null);
});
assertLinkCount(2);
}
@Test
public void testBatchRunOnAllTypesWithInvalidCriteria() {
assertLinkCount(2);
StringType criteria = new StringType("death-date=2020-06-01");
myEmpiProviderR4.clearEmpiLinks(null, myRequestDetails);
try {
myEmpiProviderR4.empiBatchPractitionerType(criteria, null);
fail();
} catch(InvalidRequestException e) {
assertThat(e.getMessage(), is(equalTo("Failed to parse match URL[death-date=2020-06-01] - Resource type Practitioner does not have a parameter with name: death-date")));
}
}
}

View File

@ -1,168 +0,0 @@
package ca.uhn.fhir.jpa.empi.provider;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Person;
import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.annotation.Nonnull;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.fail;
public class EmpiProviderClearLinkR4Test extends BaseLinkR4Test {
protected Practitioner myPractitioner;
protected StringType myPractitionerId;
protected Person myPractitionerPerson;
protected StringType myPractitionerPersonId;
@BeforeEach
public void before() {
super.before();
myPractitioner = createPractitionerAndUpdateLinks(new Practitioner());
myPractitionerId = new StringType(myPractitioner.getIdElement().getValue());
myPractitionerPerson = getPersonFromTarget(myPractitioner);
myPractitionerPersonId = new StringType(myPractitionerPerson.getIdElement().getValue());
}
@Test
public void testClearAllLinks() {
assertLinkCount(2);
myEmpiProviderR4.clearEmpiLinks(null, myRequestDetails);
assertNoLinksExist();
}
private void assertNoLinksExist() {
assertNoPatientLinksExist();
assertNoPractitionerLinksExist();
}
private void assertNoPatientLinksExist() {
assertThat(getPatientLinks(), hasSize(0));
}
private void assertNoPractitionerLinksExist() {
assertThat(getPractitionerLinks(), hasSize(0));
}
@Test
public void testClearPatientLinks() {
assertLinkCount(2);
Person read = myPersonDao.read(new IdDt(myPersonId.getValueAsString()).toVersionless());
assertThat(read, is(notNullValue()));
myEmpiProviderR4.clearEmpiLinks(new StringType("Patient"), myRequestDetails);
assertNoPatientLinksExist();
try {
myPersonDao.read(new IdDt(myPersonId.getValueAsString()).toVersionless());
fail();
} catch (ResourceNotFoundException e) {}
}
@Test
public void testPersonsWithMultipleHistoricalVersionsCanBeDeleted() {
createPatientAndUpdateLinks(buildJanePatient());
createPatientAndUpdateLinks(buildJanePatient());
createPatientAndUpdateLinks(buildJanePatient());
createPatientAndUpdateLinks(buildJanePatient());
Patient patientAndUpdateLinks = createPatientAndUpdateLinks(buildJanePatient());
Person person = getPersonFromTarget(patientAndUpdateLinks);
assertThat(person, is(notNullValue()));
myEmpiProviderR4.clearEmpiLinks(null, myRequestDetails);
assertNoPatientLinksExist();
person = getPersonFromTarget(patientAndUpdateLinks);
assertThat(person, is(nullValue()));
}
@Test
public void testPersonWithLinksToOtherPersonsCanBeDeleted() {
createPatientAndUpdateLinks(buildJanePatient());
Patient patientAndUpdateLinks1 = createPatientAndUpdateLinks(buildJanePatient());
Patient patientAndUpdateLinks = createPatientAndUpdateLinks(buildPaulPatient());
Person personFromTarget = getPersonFromTarget(patientAndUpdateLinks);
Person personFromTarget2 = getPersonFromTarget(patientAndUpdateLinks1);
linkPersons(personFromTarget, personFromTarget2);
//SUT
myEmpiProviderR4.clearEmpiLinks(null, myRequestDetails);
assertNoPatientLinksExist();
IBundleProvider search = myPersonDao.search(new SearchParameterMap().setLoadSynchronous(true));
assertThat(search.size(), is(equalTo(0)));
}
@Test
public void testPersonsWithCircularReferenceCanBeCleared() {
Patient patientAndUpdateLinks = createPatientAndUpdateLinks(buildPaulPatient());
Patient patientAndUpdateLinks1 = createPatientAndUpdateLinks(buildJanePatient());
Patient patientAndUpdateLinks2 = createPatientAndUpdateLinks(buildFrankPatient());
Person personFromTarget = getPersonFromTarget(patientAndUpdateLinks);
Person personFromTarget1 = getPersonFromTarget(patientAndUpdateLinks1);
Person personFromTarget2 = getPersonFromTarget(patientAndUpdateLinks2);
// A -> B -> C -> A linkages.
linkPersons(personFromTarget, personFromTarget1);
linkPersons(personFromTarget1, personFromTarget2);
linkPersons(personFromTarget2, personFromTarget);
//SUT
Parameters parameters = myEmpiProviderR4.clearEmpiLinks(null, myRequestDetails);
assertNoPatientLinksExist();
IBundleProvider search = myPersonDao.search(new SearchParameterMap().setLoadSynchronous(true));
assertThat(search.size(), is(equalTo(0)));
}
private void linkPersons(Person theSourcePerson, Person theTargetPerson) {
Person.PersonLinkComponent plc1 = new Person.PersonLinkComponent();
plc1.setAssurance(Person.IdentityAssuranceLevel.LEVEL2);
plc1.setTarget(new Reference(theTargetPerson.getIdElement().toUnqualifiedVersionless()));
theSourcePerson.getLink().add(plc1);
myPersonDao.update(theSourcePerson);
}
@Test
public void testClearPractitionerLinks() {
assertLinkCount(2);
Person read = myPersonDao.read(new IdDt(myPractitionerPersonId.getValueAsString()).toVersionless());
assertThat(read, is(notNullValue()));
myEmpiProviderR4.clearEmpiLinks(new StringType("Practitioner"), myRequestDetails);
assertNoPractitionerLinksExist();
try {
myPersonDao.read(new IdDt(myPractitionerPersonId.getValueAsString()).toVersionless());
fail();
} catch (ResourceNotFoundException e) {}
}
@Test
public void testClearInvalidTargetType() {
try {
myEmpiProviderR4.clearEmpiLinks(new StringType("Observation"), myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), is(equalTo("$empi-clear does not support resource type: Observation")));
}
}
@Nonnull
protected List<EmpiLink> getPractitionerLinks() {
return myEmpiLinkDaoSvc.findEmpiLinksByTarget(myPractitioner);
}
}

View File

@ -1,106 +0,0 @@
package ca.uhn.fhir.jpa.empi.provider;
import ca.uhn.fhir.empi.api.EmpiConstants;
import com.google.common.collect.Ordering;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.codesystems.MatchGrade;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
public class EmpiProviderMatchR4Test extends BaseProviderR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(EmpiProviderMatchR4Test.class);
public static final String NAME_GIVEN_JANET = NAME_GIVEN_JANE + "t";
@Override
@BeforeEach
public void before() {
super.before();
super.loadEmpiSearchParameters();
}
@Test
public void testMatch() throws Exception {
Patient jane = buildJanePatient();
jane.setActive(true);
Patient createdJane = createPatient(jane);
Patient newJane = buildJanePatient();
Bundle result = myEmpiProviderR4.match(newJane);
assertEquals(1, result.getEntry().size());
Bundle.BundleEntryComponent entry0 = result.getEntry().get(0);
assertEquals(createdJane.getId(), entry0.getResource().getId());
Bundle.BundleEntrySearchComponent searchComponent = entry0.getSearch();
assertEquals(Bundle.SearchEntryMode.MATCH, searchComponent.getMode());
assertEquals(2.0 / 3.0, searchComponent.getScore().doubleValue(), 0.01);
Extension matchGradeExtension = searchComponent.getExtensionByUrl(EmpiConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE);
assertNotNull(matchGradeExtension);
assertEquals(MatchGrade.CERTAIN.toCode(), matchGradeExtension.getValue().toString());
}
@Test
public void testMatchOrder() throws Exception {
Patient jane0 = buildJanePatient();
Patient createdJane1 = createPatient(jane0);
Patient jane1 = buildPatientWithNameAndId(NAME_GIVEN_JANET, JANE_ID);
jane1.setActive(true);
Patient createdJane2 = createPatient(jane1);
Patient newJane = buildJanePatient();
Bundle result = myEmpiProviderR4.match(newJane);
assertEquals(2, result.getEntry().size());
Bundle.BundleEntryComponent entry0 = result.getEntry().get(0);
assertTrue(jane0.getId().equals(((Patient) entry0.getResource()).getId()), "First match should be Jane");
Bundle.BundleEntryComponent entry1 = result.getEntry().get(1);
assertTrue(jane1.getId().equals(((Patient) entry1.getResource()).getId()), "Second match should be Janet");
List<Double> scores = result.getEntry()
.stream()
.map(bec -> bec.getSearch().getScore().doubleValue())
.collect(Collectors.toList());
assertTrue(Ordering.<Double>natural().reverse().isOrdered(scores), "Match scores must be descending");
}
@Test
public void testMismatch() throws Exception {
Patient jane = buildJanePatient();
jane.setActive(true);
Patient createdJane = createPatient(jane);
Patient paul = buildPaulPatient();
paul.setActive(true);
Bundle result = myEmpiProviderR4.match(paul);
assertEquals(0, result.getEntry().size());
}
@Test
public void testMatchWithEmptySearchParamCandidates() throws Exception {
setEmpiRuleJson("empi/empty-candidate-search-params.json");
Patient jane = buildJanePatient();
jane.setActive(true);
Patient createdJane = createPatient(jane);
Patient newJane = buildJanePatient();
Bundle result = myEmpiProviderR4.match(newJane);
assertEquals(1, result.getEntry().size());
assertEquals(createdJane.getId(), result.getEntryFirstRep().getResource().getId());
}
}

View File

@ -1,137 +0,0 @@
package ca.uhn.fhir.jpa.empi.provider;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.util.AssuranceLevelUtil;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.r4.model.Person;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class EmpiProviderMergePersonsR4Test extends BaseProviderR4Test {
private Person myFromPerson;
private StringType myFromPersonId;
private Person myToPerson;
private StringType myToPersonId;
@Override
@BeforeEach
public void before() {
super.before();
super.loadEmpiSearchParameters();
myFromPerson = createPerson();
myFromPersonId = new StringType(myFromPerson.getIdElement().getValue());
myToPerson = createPerson();
myToPersonId = new StringType(myToPerson.getIdElement().getValue());
}
@Test
public void testMerge() {
Person mergedPerson = myEmpiProviderR4.mergePersons(myFromPersonId, myToPersonId, myRequestDetails);
assertEquals(myToPerson.getIdElement(), mergedPerson.getIdElement());
assertThat(mergedPerson, is(samePersonAs(myToPerson)));
assertEquals(2, getAllPersons().size());
assertEquals(1, getAllActivePersons().size());
Person fromPerson = myPersonDao.read(myFromPerson.getIdElement().toUnqualifiedVersionless());
assertThat(fromPerson.getActive(), is(false));
List<Person.PersonLinkComponent> links = fromPerson.getLink();
assertThat(links, hasSize(1));
assertThat(links.get(0).getTarget().getReference(), is (myToPerson.getIdElement().toUnqualifiedVersionless().getValue()));
assertThat(links.get(0).getAssurance(), is (AssuranceLevelUtil.getAssuranceLevel(EmpiMatchResultEnum.REDIRECT, EmpiLinkSourceEnum.MANUAL).toR4()));
}
@Test
public void testUnmanagedMerge() {
StringType fromPersonId = new StringType(createUnmanagedPerson().getIdElement().getValue());
StringType toPersonId = new StringType(createUnmanagedPerson().getIdElement().getValue());
try {
myEmpiProviderR4.mergePersons(fromPersonId, toPersonId, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("Only EMPI managed resources can be merged. Empi managed resource have the HAPI-EMPI tag.", e.getMessage());
}
}
@Test
public void testMergePatients() {
try {
StringType patientId = new StringType(createPatient().getIdElement().getValue());
StringType otherPatientId = new StringType(createPatient().getIdElement().getValue());
myEmpiProviderR4.mergePersons(patientId, otherPatientId, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), endsWith("must have form Person/<id> where <id> is the id of the person"));
}
}
@Test
public void testNullParams() {
try {
myEmpiProviderR4.mergePersons(null, null, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("fromPersonId cannot be null", e.getMessage());
}
try {
myEmpiProviderR4.mergePersons(null, myToPersonId, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("fromPersonId cannot be null", e.getMessage());
}
try {
myEmpiProviderR4.mergePersons(myFromPersonId, null, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("toPersonId cannot be null", e.getMessage());
}
}
@Test
public void testBadParams() {
try {
myEmpiProviderR4.mergePersons(new StringType("Patient/1"), new StringType("Patient/2"), myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), endsWith(" must have form Person/<id> where <id> is the id of the person"));
}
try {
myEmpiProviderR4.mergePersons(myFromPersonId, new StringType("Patient/2"), myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), endsWith(" must have form Person/<id> where <id> is the id of the person"));
}
try {
myEmpiProviderR4.mergePersons(new StringType("Person/1"), new StringType("Person/1"), myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("fromPersonId must be different from toPersonId", e.getMessage());
}
try {
myEmpiProviderR4.mergePersons(new StringType("Person/abc"), myToPersonId, myRequestDetails);
fail();
} catch (ResourceNotFoundException e) {
assertEquals("Resource Person/abc is not known", e.getMessage());
}
try {
myEmpiProviderR4.mergePersons(myFromPersonId, new StringType("Person/abc"), myRequestDetails);
fail();
} catch (ResourceNotFoundException e) {
assertEquals("Resource Person/abc is not known", e.getMessage());
}
}
}

View File

@ -1,142 +0,0 @@
package ca.uhn.fhir.jpa.empi.provider;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Person;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.matchesPattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class EmpiProviderUpdateLinkR4Test extends BaseLinkR4Test {
@Test
public void testUpdateLinkNoMatch() {
assertLinkCount(1);
myEmpiProviderR4.updateLink(myPersonId, myPatientId, NO_MATCH_RESULT, myRequestDetails);
assertLinkCount(2);
List<EmpiLink> links = getPatientLinks();
assertEquals(EmpiLinkSourceEnum.MANUAL, links.get(0).getLinkSource());
assertEquals(EmpiMatchResultEnum.NO_MATCH, links.get(0).getMatchResult());
assertEquals(EmpiLinkSourceEnum.AUTO, links.get(1).getLinkSource());
assertEquals(EmpiMatchResultEnum.MATCH, links.get(1).getMatchResult());
assertNotEquals(links.get(0).getPersonPid(), links.get(1).getPersonPid());
}
@Test
public void testUpdateLinkMatch() {
assertLinkCount(1);
myEmpiProviderR4.updateLink(myPersonId, myPatientId, MATCH_RESULT, myRequestDetails);
assertLinkCount(1);
List<EmpiLink> links = getPatientLinks();
assertEquals(EmpiLinkSourceEnum.MANUAL, links.get(0).getLinkSource());
assertEquals(EmpiMatchResultEnum.MATCH, links.get(0).getMatchResult());
}
@Test
public void testUpdateLinkTwiceFailsDueToWrongVersion() {
myEmpiProviderR4.updateLink(myPersonId, myPatientId, MATCH_RESULT, myRequestDetails);
try {
myEmpiProviderR4.updateLink(myPersonId, myPatientId, NO_MATCH_RESULT, myRequestDetails);
fail();
} catch (ResourceVersionConflictException e) {
assertThat(e.getMessage(), matchesPattern("Requested resource Person/\\d+/_history/1 is not the latest version. Latest version is Person/\\d+/_history/2"));
}
}
@Test
public void testUpdateLinkTwiceWorksWhenNoVersionProvided() {
myEmpiProviderR4.updateLink(myPersonId, myPatientId, MATCH_RESULT, myRequestDetails);
Person person = myEmpiProviderR4.updateLink(myVersionlessPersonId, myPatientId, NO_MATCH_RESULT, myRequestDetails);
assertThat(person.getLink(), hasSize(0));
}
@Test
public void testUnlinkLink() {
myEmpiProviderR4.updateLink(myPersonId, myPatientId, NO_MATCH_RESULT, myRequestDetails);
try {
myEmpiProviderR4.updateLink(myPersonId, myPatientId, MATCH_RESULT, myRequestDetails);
fail();
} catch (ResourceVersionConflictException e) {
assertThat(e.getMessage(), matchesPattern("Requested resource Person/\\d+/_history/1 is not the latest version. Latest version is Person/\\d+/_history/2"));
}
}
@Test
public void testUpdateIllegalResultPM() {
try {
myEmpiProviderR4.updateLink(myPersonId, myPatientId, POSSIBLE_MATCH_RESULT, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("$empi-update-link illegal matchResult value 'POSSIBLE_MATCH'. Must be NO_MATCH or MATCH", e.getMessage());
}
}
@Test
public void testUpdateIllegalResultPD() {
try {
myEmpiProviderR4.updateLink(myPersonId, myPatientId, POSSIBLE_DUPLICATE_RESULT, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("$empi-update-link illegal matchResult value 'POSSIBLE_DUPLICATE'. Must be NO_MATCH or MATCH", e.getMessage());
}
}
@Test
public void testUpdateIllegalFirstArg() {
try {
myEmpiProviderR4.updateLink(myPatientId, myPatientId, NO_MATCH_RESULT, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), endsWith(" must have form Person/<id> where <id> is the id of the person"));
}
}
@Test
public void testUpdateIllegalSecondArg() {
try {
myEmpiProviderR4.updateLink(myPersonId, myPersonId, NO_MATCH_RESULT, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), endsWith("must have form Patient/<id> or Practitioner/<id> where <id> is the id of the resource"));
}
}
@Test
public void testUpdateStrangePerson() {
Person person = createUnmanagedPerson();
try {
myEmpiProviderR4.updateLink(new StringType(person.getIdElement().getValue()), myPatientId, NO_MATCH_RESULT, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("Only EMPI Managed Person resources may be updated via this operation. The Person resource provided is not tagged as managed by hapi-empi", e.getMessage());
}
}
@Test
public void testExcludedPerson() {
Patient patient = new Patient();
patient.getMeta().addTag().setSystem(EmpiConstants.SYSTEM_EMPI_MANAGED).setCode(EmpiConstants.CODE_NO_EMPI_MANAGED);
createPatient(patient);
try {
myEmpiProviderR4.updateLink(myPersonId, new StringType(patient.getIdElement().getValue()), NO_MATCH_RESULT, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("The target is marked with the " + EmpiConstants.CODE_NO_EMPI_MANAGED + " tag which means it may not be EMPI linked.", e.getMessage());
}
}
}

View File

@ -1,56 +0,0 @@
package ca.uhn.fhir.jpa.empi.searchparam;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.TokenParam;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Person;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class SearchParameterTest extends BaseEmpiR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(SearchParameterTest.class);
@BeforeEach
public void before() {
super.loadEmpiSearchParameters();
}
@Test
public void testCanFindPossibleMatches() {
// Create a possible match
Patient patient = buildJanePatient();
patient.getNameFirstRep().setFamily("familyone");
patient = createPatientAndUpdateLinks(patient);
Patient patient2 = buildJanePatient();
patient2.getNameFirstRep().setFamily("pleasedonotmatchatall");
patient2 = createPatientAndUpdateLinks(patient2);
assertThat(patient2, is(possibleMatchWith(patient)));
// Now confirm we can find it using our custom search parameter
SearchParameterMap map = new SearchParameterMap();
map.setLoadSynchronous(true);
map.add("assurance", new TokenParam(Person.IdentityAssuranceLevel.LEVEL2.toCode()));
IBundleProvider result = myPersonDao.search(map);
assertEquals(1, result.size().intValue());
Person person = (Person) result.getResources(0, 1).get(0);
String encoded = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(person);
ourLog.info("Search result: {}", encoded);
List<Person.PersonLinkComponent> links = person.getLink();
assertEquals(2, links.size());
assertEquals(Person.IdentityAssuranceLevel.LEVEL2, links.get(0).getAssurance());
assertEquals(Person.IdentityAssuranceLevel.LEVEL1, links.get(1).getAssurance());
}
}

View File

@ -1,99 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.empi.api.IEmpiSubmitSvc;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.test.concurrency.PointcutLatch;
import org.apache.commons.lang3.time.DateUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.util.Date;
class EmpiBatchSvcImplTest extends BaseEmpiR4Test {
@Autowired
IEmpiSubmitSvc myEmpiSubmitSvc;
@Autowired
IInterceptorService myInterceptorService;
PointcutLatch afterEmpiLatch = new PointcutLatch(Pointcut.EMPI_AFTER_PERSISTED_RESOURCE_CHECKED);
@BeforeEach
public void before() {
myInterceptorService.registerAnonymousInterceptor(Pointcut.EMPI_AFTER_PERSISTED_RESOURCE_CHECKED, afterEmpiLatch);
}
@AfterEach
public void after() throws IOException {
myInterceptorService.unregisterInterceptor(afterEmpiLatch);
afterEmpiLatch.clear();
super.after();
}
@Test
public void testEmpiBatchRunWorksOverMultipleTargetTypes() throws InterruptedException {
for (int i =0; i < 10; i++) {
createPatient(buildJanePatient());
}
for(int i = 0; i< 10; i++) {
createPractitioner(buildPractitionerWithNameAndId("test", "id"));
}
assertLinkCount(0);
//SUT
afterEmpiLatch.runWithExpectedCount(20, () -> myEmpiSubmitSvc.submitAllTargetTypesToEmpi(null));
assertLinkCount(20);
}
@Test
public void testEmpiBatchOnPatientType() throws Exception {
for (int i =0; i < 10; i++) {
createPatient(buildPatientWithNameAndId("test", "id"));
}
assertLinkCount(0);
//SUT
afterEmpiLatch.runWithExpectedCount(10, () -> myEmpiSubmitSvc.submitTargetTypeToEmpi("Patient", null));
assertLinkCount(10);
}
@Test
public void testEmpiBatchOnPractitionerType() throws Exception {
for (int i =0; i < 10; i++) {
createPractitioner(buildPractitionerWithNameAndId("test", "id"));
}
assertLinkCount(0);
//SUT
afterEmpiLatch.runWithExpectedCount(10, () -> myEmpiSubmitSvc.submitAllTargetTypesToEmpi(null));
assertLinkCount(10);
}
@Test
public void testEmpiOnTargetTypeWithCriteria() throws InterruptedException {
createPatient(buildPatientWithNameIdAndBirthday("gary", "gary_id", new Date()));
createPatient(buildPatientWithNameIdAndBirthday("john", "john_id", DateUtils.addDays(new Date(), -300)));
assertLinkCount(0);
//SUT
afterEmpiLatch.runWithExpectedCount(1, () -> myEmpiSubmitSvc.submitAllTargetTypesToEmpi("Patient?name=gary"));
assertLinkCount(1);
}
}

View File

@ -1,167 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Person;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class EmpiLinkSvcTest extends BaseEmpiR4Test {
private static final EmpiMatchOutcome POSSIBLE_MATCH = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.POSSIBLE_MATCH);
@Autowired
IEmpiLinkSvc myEmpiLinkSvc;
@Override
@AfterEach
public void after() throws IOException {
myExpungeEverythingService.expungeEverythingByType(EmpiLink.class);
super.after();
}
@Test
public void compareEmptyPatients() {
Patient patient = new Patient();
patient.setId("Patient/1");
EmpiMatchResultEnum result = myEmpiResourceMatcherSvc.getMatchResult(patient, patient).getMatchResultEnum();
assertEquals(EmpiMatchResultEnum.NO_MATCH, result);
}
@Test
public void testCreateRemoveLink() {
assertLinkCount(0);
Person person = createPerson();
IdType personId = person.getIdElement().toUnqualifiedVersionless();
assertEquals(0, person.getLink().size());
Patient patient = createPatient();
{
myEmpiLinkSvc.updateLink(person, patient, POSSIBLE_MATCH, EmpiLinkSourceEnum.AUTO, createContextForCreate());
assertLinkCount(1);
Person newPerson = myPersonDao.read(personId);
assertEquals(1, newPerson.getLink().size());
}
{
myEmpiLinkSvc.updateLink(person, patient, EmpiMatchOutcome.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
assertLinkCount(1);
Person newPerson = myPersonDao.read(personId);
assertEquals(0, newPerson.getLink().size());
}
}
@Test
public void testPossibleDuplicate() {
assertLinkCount(0);
Person person = createPerson();
Person target = createPerson();
myEmpiLinkSvc.updateLink(person, target, EmpiMatchOutcome.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, createContextForCreate());
assertLinkCount(1);
}
@Test
public void testNoMatchBlocksPossibleDuplicate() {
assertLinkCount(0);
Person person = createPerson();
Person target = createPerson();
Long personPid = myIdHelperService.getPidOrNull(person);
Long targetPid = myIdHelperService.getPidOrNull(target);
assertFalse(myEmpiLinkDaoSvc.getLinkByPersonPidAndTargetPid(personPid, targetPid).isPresent());
assertFalse(myEmpiLinkDaoSvc.getLinkByPersonPidAndTargetPid(targetPid, personPid).isPresent());
saveNoMatchLink(personPid, targetPid);
myEmpiLinkSvc.updateLink(person, target, EmpiMatchOutcome.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, createContextForCreate());
assertFalse(myEmpiLinkDaoSvc.getEmpiLinksByPersonPidTargetPidAndMatchResult(personPid, targetPid, EmpiMatchResultEnum.POSSIBLE_DUPLICATE).isPresent());
assertLinkCount(1);
}
@Test
public void testNoMatchBlocksPossibleDuplicateReversed() {
assertLinkCount(0);
Person person = createPerson();
Person target = createPerson();
Long personPid = myIdHelperService.getPidOrNull(person);
Long targetPid = myIdHelperService.getPidOrNull(target);
assertFalse(myEmpiLinkDaoSvc.getLinkByPersonPidAndTargetPid(personPid, targetPid).isPresent());
assertFalse(myEmpiLinkDaoSvc.getLinkByPersonPidAndTargetPid(targetPid, personPid).isPresent());
saveNoMatchLink(targetPid, personPid);
myEmpiLinkSvc.updateLink(person, target, EmpiMatchOutcome.POSSIBLE_DUPLICATE, EmpiLinkSourceEnum.AUTO, createContextForCreate());
assertFalse(myEmpiLinkDaoSvc.getEmpiLinksByPersonPidTargetPidAndMatchResult(personPid, targetPid, EmpiMatchResultEnum.POSSIBLE_DUPLICATE).isPresent());
assertLinkCount(1);
}
private void saveNoMatchLink(Long thePersonPid, Long theTargetPid) {
EmpiLink noMatchLink = myEmpiLinkDaoSvc.newEmpiLink()
.setPersonPid(thePersonPid)
.setTargetPid(theTargetPid)
.setLinkSource(EmpiLinkSourceEnum.MANUAL)
.setMatchResult(EmpiMatchResultEnum.NO_MATCH);
saveLink(noMatchLink);
}
@Test
public void testManualEmpiLinksCannotBeModifiedBySystem() {
Person person = createPerson(buildJanePerson());
Patient patient = createPatient(buildJanePatient());
myEmpiLinkSvc.updateLink(person, patient, EmpiMatchOutcome.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
try {
myEmpiLinkSvc.updateLink(person, patient, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.AUTO, null);
fail();
} catch (InternalErrorException e) {
assertThat(e.getMessage(), is(equalTo("EMPI system is not allowed to modify links on manually created links")));
}
}
@Test
public void testAutomaticallyAddedNO_MATCHEmpiLinksAreNotAllowed() {
Person person = createPerson(buildJanePerson());
Patient patient = createPatient(buildJanePatient());
// Test: it should be impossible to have a AUTO NO_MATCH record. The only NO_MATCH records in the system must be MANUAL.
try {
myEmpiLinkSvc.updateLink(person, patient, EmpiMatchOutcome.NO_MATCH, EmpiLinkSourceEnum.AUTO, null);
fail();
} catch (InternalErrorException e) {
assertThat(e.getMessage(), is(equalTo("EMPI system is not allowed to automatically NO_MATCH a resource")));
}
}
@Test
public void testSyncDoesNotSyncNoMatchLinks() {
Person person = createPerson(buildJanePerson());
Patient patient1 = createPatient(buildJanePatient());
Patient patient2 = createPatient(buildJanePatient());
assertEquals(0, myEmpiLinkDao.count());
myEmpiLinkDaoSvc.createOrUpdateLinkEntity(person, patient1, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
myEmpiLinkDaoSvc.createOrUpdateLinkEntity(person, patient2, EmpiMatchOutcome.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
myEmpiLinkSvc.syncEmpiLinksToPersonLinks(person, createContextForCreate());
assertTrue(person.hasLink());
assertEquals(patient1.getIdElement().toVersionless().getValue(), person.getLinkFirstRep().getTarget().getReference());
}
}

View File

@ -1,10 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.jpa.empi.provider.EmpiProviderUpdateLinkR4Test;
/**
* Tests for this service are in the test for the provider that wraps this service:
* @see EmpiProviderUpdateLinkR4Test
*/
public class EmpiLinkUpdaterSvcImplTest {
}

View File

@ -1,588 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.empi.model.CanonicalEID;
import ca.uhn.fhir.empi.util.EIDHelper;
import ca.uhn.fhir.empi.util.PersonHelper;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.r4.model.Enumerations;
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.Person;
import org.hl7.fhir.r4.model.Practitioner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static ca.uhn.fhir.empi.api.EmpiMatchResultEnum.MATCH;
import static ca.uhn.fhir.empi.api.EmpiMatchResultEnum.NO_MATCH;
import static ca.uhn.fhir.empi.api.EmpiMatchResultEnum.POSSIBLE_DUPLICATE;
import static ca.uhn.fhir.empi.api.EmpiMatchResultEnum.POSSIBLE_MATCH;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.blankOrNullString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.in;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.slf4j.LoggerFactory.getLogger;
public class EmpiMatchLinkSvcTest extends BaseEmpiR4Test {
private static final Logger ourLog = getLogger(EmpiMatchLinkSvcTest.class);
@Autowired
IEmpiLinkSvc myEmpiLinkSvc;
@Autowired
private EIDHelper myEidHelper;
@Autowired
private PersonHelper myPersonHelper;
@BeforeEach
public void before() {
super.loadEmpiSearchParameters();
}
@Test
public void testAddPatientLinksToNewPersonIfNoneFound() {
createPatientAndUpdateLinks(buildJanePatient());
assertLinkCount(1);
assertLinksMatchResult(MATCH);
assertLinksNewPerson(true);
assertLinksMatchedByEid(false);
}
@Test
public void testAddPatientLinksToNewPersonIfNoMatch() {
Patient patient1 = createPatientAndUpdateLinks(buildJanePatient());
Patient patient2 = createPatientAndUpdateLinks(buildPaulPatient());
assertLinkCount(2);
assertThat(patient1, is(not(samePersonAs(patient2))));
assertLinksMatchResult(MATCH, MATCH);
assertLinksNewPerson(true, true);
assertLinksMatchedByEid(false, false);
}
@Test
public void testAddPatientLinksToExistingPersonIfMatch() {
Patient patient1 = createPatientAndUpdateLinks(buildJanePatient());
assertLinkCount(1);
Patient patient2 = createPatientAndUpdateLinks(buildJanePatient());
assertLinkCount(2);
assertThat(patient1, is(samePersonAs(patient2)));
assertLinksMatchResult(MATCH, MATCH);
assertLinksNewPerson(true, false);
assertLinksMatchedByEid(false, false);
}
@Test
public void testWhenMatchOccursOnPersonThatHasBeenManuallyNOMATCHedThatItIsBlocked() {
Patient originalJane = createPatientAndUpdateLinks(buildJanePatient());
IBundleProvider search = myPersonDao.search(new SearchParameterMap());
IAnyResource janePerson = (IAnyResource) search.getResources(0, 1).get(0);
//Create a manual NO_MATCH between janePerson and unmatchedJane.
Patient unmatchedJane = createPatient(buildJanePatient());
myEmpiLinkSvc.updateLink(janePerson, unmatchedJane, EmpiMatchOutcome.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
//rerun EMPI rules against unmatchedJane.
myEmpiMatchLinkSvc.updateEmpiLinksForEmpiTarget(unmatchedJane, createContextForCreate());
assertThat(unmatchedJane, is(not(samePersonAs(janePerson))));
assertThat(unmatchedJane, is(not(linkedTo(originalJane))));
assertLinksMatchResult(MATCH, NO_MATCH, MATCH);
assertLinksNewPerson(true, false, true);
assertLinksMatchedByEid(false, false, false);
}
@Test
public void testWhenPOSSIBLE_MATCHOccursOnPersonThatHasBeenManuallyNOMATCHedThatItIsBlocked() {
Patient originalJane = createPatientAndUpdateLinks(buildJanePatient());
IBundleProvider search = myPersonDao.search(new SearchParameterMap());
IAnyResource janePerson = (IAnyResource) search.getResources(0, 1).get(0);
Patient unmatchedPatient = createPatient(buildJanePatient());
//This simulates an admin specifically saying that unmatchedPatient does NOT match janePerson.
myEmpiLinkSvc.updateLink(janePerson, unmatchedPatient, EmpiMatchOutcome.NO_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
//TODO change this so that it will only partially match.
//Now normally, when we run update links, it should link to janePerson. However, this manual NO_MATCH link
//should cause a whole new Person to be created.
myEmpiMatchLinkSvc.updateEmpiLinksForEmpiTarget(unmatchedPatient, createContextForCreate());
assertThat(unmatchedPatient, is(not(samePersonAs(janePerson))));
assertThat(unmatchedPatient, is(not(linkedTo(originalJane))));
assertLinksMatchResult(MATCH, NO_MATCH, MATCH);
assertLinksNewPerson(true, false, true);
assertLinksMatchedByEid(false, false, false);
}
@Test
public void testWhenPatientIsCreatedWithEIDThatItPropagatesToNewPerson() {
String sampleEID = "sample-eid";
Patient janePatient = addExternalEID(buildJanePatient(), sampleEID);
janePatient = createPatientAndUpdateLinks(janePatient);
Optional<EmpiLink> empiLink = myEmpiLinkDaoSvc.getMatchedLinkForTargetPid(janePatient.getIdElement().getIdPartAsLong());
assertThat(empiLink.isPresent(), is(true));
Person person = getPersonFromEmpiLink(empiLink.get());
List<CanonicalEID> externalEid = myEidHelper.getExternalEid(person);
assertThat(externalEid.get(0).getSystem(), is(equalTo(myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem())));
assertThat(externalEid.get(0).getValue(), is(equalTo(sampleEID)));
}
@Test
public void testWhenPatientIsCreatedWithoutAnEIDThePersonGetsAutomaticallyAssignedOne() {
Patient patient = createPatientAndUpdateLinks(buildJanePatient());
EmpiLink empiLink = myEmpiLinkDaoSvc.getMatchedLinkForTargetPid(patient.getIdElement().getIdPartAsLong()).get();
Person person = getPersonFromEmpiLink(empiLink);
Identifier identifierFirstRep = person.getIdentifierFirstRep();
assertThat(identifierFirstRep.getSystem(), is(equalTo(EmpiConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM)));
assertThat(identifierFirstRep.getValue(), not(blankOrNullString()));
}
@Test
public void testPatientAttributesAreCopiedOverWhenPersonIsCreatedFromPatient() {
Patient patient = createPatientAndUpdateLinks(buildPatientWithNameIdAndBirthday("Gary", "GARY_ID", new Date()));
Optional<EmpiLink> empiLink = myEmpiLinkDaoSvc.getMatchedLinkForTargetPid(patient.getIdElement().getIdPartAsLong());
Person read = getPersonFromEmpiLink(empiLink.get());
assertThat(read.getNameFirstRep().getFamily(), is(equalTo(patient.getNameFirstRep().getFamily())));
assertThat(read.getNameFirstRep().getGivenAsSingleString(), is(equalTo(patient.getNameFirstRep().getGivenAsSingleString())));
assertThat(read.getBirthDateElement().toHumanDisplay(), is(equalTo(patient.getBirthDateElement().toHumanDisplay())));
assertThat(read.getTelecomFirstRep().getValue(), is(equalTo(patient.getTelecomFirstRep().getValue())));
assertThat(read.getPhoto().getData(), is(equalTo(patient.getPhotoFirstRep().getData())));
assertThat(read.getGender(), is(equalTo(patient.getGender())));
}
@Test
public void testPatientMatchingAnotherPatientLinksToSamePerson() {
Patient janePatient = createPatientAndUpdateLinks(buildJanePatient());
Patient sameJanePatient = createPatientAndUpdateLinks(buildJanePatient());
assertThat(janePatient, is(samePersonAs(sameJanePatient)));
}
@Test
public void testIncomingPatientWithEIDThatMatchesPersonWithHapiEidAddsExternalEidToPerson() {
// Existing Person with system-assigned EID found linked from matched Patient. incoming Patient has EID. Replace Person system-assigned EID with Patient EID.
Patient patient = createPatientAndUpdateLinks(buildJanePatient());
Person janePerson = getPersonFromTarget(patient);
List<CanonicalEID> hapiEid = myEidHelper.getHapiEid(janePerson);
String foundHapiEid = hapiEid.get(0).getValue();
Patient janePatient = addExternalEID(buildJanePatient(), "12345");
createPatientAndUpdateLinks(janePatient);
//We want to make sure the patients were linked to the same person.
assertThat(patient, is(samePersonAs(janePatient)));
Person person = getPersonFromTarget(patient);
List<Identifier> identifier = person.getIdentifier();
//The collision should have kept the old identifier
Identifier firstIdentifier = identifier.get(0);
assertThat(firstIdentifier.getSystem(), is(equalTo(EmpiConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM)));
assertThat(firstIdentifier.getValue(), is(equalTo(foundHapiEid)));
//The collision should have added a new identifier with the external system.
Identifier secondIdentifier = identifier.get(1);
assertThat(secondIdentifier.getSystem(), is(equalTo(myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem())));
assertThat(secondIdentifier.getValue(), is(equalTo("12345")));
}
@Test
public void testIncomingPatientWithEidMatchesAnotherPatientWithSameEIDAreLinked() {
Patient patient1 = addExternalEID(buildJanePatient(), "uniqueid");
createPatientAndUpdateLinks(patient1);
Patient patient2 = addExternalEID(buildPaulPatient(), "uniqueid");
createPatientAndUpdateLinks(patient2);
assertThat(patient1, is(samePersonAs(patient2)));
}
@Test
public void testHavingMultipleEIDsOnIncomingPatientMatchesCorrectly() {
Patient patient1 = buildJanePatient();
addExternalEID(patient1, "id_1");
addExternalEID(patient1, "id_2");
addExternalEID(patient1, "id_3");
addExternalEID(patient1, "id_4");
createPatientAndUpdateLinks(patient1);
Patient patient2 = buildPaulPatient();
addExternalEID(patient2, "id_5");
addExternalEID(patient2, "id_1");
createPatientAndUpdateLinks(patient2);
assertThat(patient1, is(samePersonAs(patient2)));
}
@Test
public void testDuplicatePersonLinkIsCreatedWhenAnIncomingPatientArrivesWithEIDThatMatchesAnotherEIDPatient() {
Patient patient1 = addExternalEID(buildJanePatient(), "eid-1");
patient1 = createPatientAndUpdateLinks(patient1);
Patient patient2 = addExternalEID(buildJanePatient(), "eid-2");
patient2 = createPatientAndUpdateLinks(patient2);
List<EmpiLink> possibleDuplicates = myEmpiLinkDaoSvc.getPossibleDuplicates();
assertThat(possibleDuplicates, hasSize(1));
List<Long> duplicatePids = Stream.of(patient1, patient2)
.map(this::getPersonFromTarget)
.map(myIdHelperService::getPidOrNull)
.collect(Collectors.toList());
//The two Persons related to the patients should both show up in the only existing POSSIBLE_DUPLICATE EmpiLink.
EmpiLink empiLink = possibleDuplicates.get(0);
assertThat(empiLink.getPersonPid(), is(in(duplicatePids)));
assertThat(empiLink.getTargetPid(), is(in(duplicatePids)));
}
@Test
public void testPatientWithNoEmpiTagIsNotMatched() {
// Patient with "no-empi" tag is not matched
Patient janePatient = buildJanePatient();
janePatient.getMeta().addTag(EmpiConstants.SYSTEM_EMPI_MANAGED, EmpiConstants.CODE_NO_EMPI_MANAGED, "Don't EMPI on me!");
createPatientAndUpdateLinks(janePatient);
assertLinkCount(0);
}
@Test
public void testPractitionersDoNotMatchToPatients() {
Patient janePatient = createPatientAndUpdateLinks(buildJanePatient());
Practitioner janePractitioner = createPractitionerAndUpdateLinks(buildJanePractitioner());
assertLinkCount(2);
assertThat(janePatient, is(not(samePersonAs(janePractitioner))));
}
@Test
public void testPractitionersThatMatchShouldLink() {
Practitioner janePractitioner = createPractitionerAndUpdateLinks(buildJanePractitioner());
Practitioner anotherJanePractitioner = createPractitionerAndUpdateLinks(buildJanePractitioner());
assertLinkCount(2);
assertThat(anotherJanePractitioner, is(samePersonAs(janePractitioner)));
}
@Test
public void testWhenThereAreNoMATCHOrPOSSIBLE_MATCHOutcomesThatANewPersonIsCreated() {
/**
* CASE 1: No MATCHED and no PROBABLE_MATCHED outcomes -> a new Person resource
* is created and linked to that Pat/Prac.
*/
assertLinkCount(0);
Patient janePatient = createPatientAndUpdateLinks(buildJanePatient());
assertLinkCount(1);
assertThat(janePatient, is(matchedToAPerson()));
}
@Test
public void testWhenAllMATCHResultsAreToSamePersonThatTheyAreLinked() {
/**
* CASE 2: All of the MATCHED Pat/Prac resources are already linked to the same Person ->
* a new Link is created between the new Pat/Prac and that Person and is set to MATCHED.
*/
Patient janePatient = createPatientAndUpdateLinks(buildJanePatient());
Patient janePatient2 = createPatientAndUpdateLinks(buildJanePatient());
assertLinkCount(2);
assertThat(janePatient, is(samePersonAs(janePatient2)));
Patient incomingJanePatient = createPatientAndUpdateLinks(buildJanePatient());
assertThat(incomingJanePatient, is(samePersonAs(janePatient, janePatient2)));
assertThat(incomingJanePatient, is(linkedTo(janePatient, janePatient2)));
}
@Test
public void testMATCHResultWithMultipleCandidatesCreatesPOSSIBLE_DUPLICATELinksAndNoPersonIsCreated() {
/**
* CASE 3: The MATCHED Pat/Prac resources link to more than one Person -> Mark all links as POSSIBLE_MATCH.
* All other Person resources are marked as POSSIBLE_DUPLICATE of this first Person.
*/
Patient janePatient = createPatientAndUpdateLinks(buildJanePatient());
Patient janePatient2 = createPatient(buildJanePatient());
//In a normal situation, janePatient2 would just match to jane patient, but here we need to hack it so they are their
//own individual Persons for the purpose of this test.
IAnyResource person = myPersonHelper.createPersonFromEmpiTarget(janePatient2);
myEmpiLinkSvc.updateLink(person, janePatient2, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.AUTO, createContextForCreate());
assertThat(janePatient, is(not(samePersonAs(janePatient2))));
//In theory, this will match both Persons!
Patient incomingJanePatient = createPatientAndUpdateLinks(buildJanePatient());
//There should now be a single POSSIBLE_DUPLICATE link with
assertThat(janePatient, is(possibleDuplicateOf(janePatient2)));
//There should now be 2 POSSIBLE_MATCH links with this person.
assertThat(incomingJanePatient, is(possibleMatchWith(janePatient, janePatient2)));
//Ensure there is no successful MATCH links for incomingJanePatient
Optional<EmpiLink> matchedLinkForTargetPid = myEmpiLinkDaoSvc.getMatchedLinkForTargetPid(myIdHelperService.getPidOrNull(incomingJanePatient));
assertThat(matchedLinkForTargetPid.isPresent(), is(false));
logAllLinks();
assertLinksMatchResult(MATCH, MATCH, POSSIBLE_MATCH, POSSIBLE_MATCH, POSSIBLE_DUPLICATE);
assertLinksNewPerson(true, true, false, false, false);
assertLinksMatchedByEid(false, false, false, false, false);
}
@Test
public void testWhenAllMatchResultsArePOSSIBLE_MATCHThattheyAreLinkedAndNoPersonIsCreated() {
/**
* CASE 4: Only POSSIBLE_MATCH outcomes -> In this case, empi-link records are created with POSSIBLE_MATCH
* outcome and await manual assignment to either NO_MATCH or MATCHED. Person link is added.
*/
Patient patient = buildJanePatient();
patient.getNameFirstRep().setFamily("familyone");
patient = createPatientAndUpdateLinks(patient);
assertThat(patient, is(samePersonAs(patient)));
Patient patient2 = buildJanePatient();
patient2.getNameFirstRep().setFamily("pleasedonotmatchatall");
patient2 = createPatientAndUpdateLinks(patient2);
assertThat(patient2, is(possibleMatchWith(patient)));
Patient patient3 = buildJanePatient();
patient3.getNameFirstRep().setFamily("pleasedonotmatchatall");
patient3 = createPatientAndUpdateLinks(patient3);
assertThat(patient3, is(possibleMatchWith(patient2)));
assertThat(patient3, is(possibleMatchWith(patient)));
IBundleProvider bundle = myPersonDao.search(new SearchParameterMap());
assertEquals(1, bundle.size());
Person person = (Person) bundle.getResources(0, 1).get(0);
assertEquals(Person.IdentityAssuranceLevel.LEVEL2, person.getLink().get(0).getAssurance());
assertEquals(Person.IdentityAssuranceLevel.LEVEL1, person.getLink().get(1).getAssurance());
assertEquals(Person.IdentityAssuranceLevel.LEVEL1, person.getLink().get(2).getAssurance());
assertLinksMatchResult(MATCH, POSSIBLE_MATCH, POSSIBLE_MATCH);
assertLinksNewPerson(true, false, false);
assertLinksMatchedByEid(false, false, false);
}
@Test
public void testWhenAnIncomingResourceHasMatchesAndPossibleMatchesThatItLinksToMatch() {
Patient patient = buildJanePatient();
patient.getNameFirstRep().setFamily("familyone");
patient = createPatientAndUpdateLinks(patient);
assertThat(patient, is(samePersonAs(patient)));
Patient patient2 = buildJanePatient();
patient2.getNameFirstRep().setFamily("pleasedonotmatchatall");
patient2 = createPatientAndUpdateLinks(patient2);
Patient patient3 = buildJanePatient();
patient3.getNameFirstRep().setFamily("familyone");
patient3 = createPatientAndUpdateLinks(patient3);
assertThat(patient2, is(not(samePersonAs(patient))));
assertThat(patient2, is(possibleMatchWith(patient)));
assertThat(patient3, is(samePersonAs(patient)));
}
@Test
public void testAutoMatchesGenerateAssuranceLevel3() {
Patient patient = createPatientAndUpdateLinks(buildJanePatient());
Person janePerson = getPersonFromTarget(patient);
Person.PersonLinkComponent linkFirstRep = janePerson.getLinkFirstRep();
assertThat(linkFirstRep.getTarget().getReference(), is(equalTo(patient.getIdElement().toVersionless().toString())));
assertThat(linkFirstRep.getAssurance(), is(equalTo(Person.IdentityAssuranceLevel.LEVEL2)));
}
@Test
public void testManualMatchesGenerateAssuranceLevel4() {
Patient patient = createPatientAndUpdateLinks(buildJanePatient());
Person janePerson = getPersonFromTarget(patient);
myEmpiLinkSvc.updateLink(janePerson, patient, EmpiMatchOutcome.NEW_PERSON_MATCH, EmpiLinkSourceEnum.MANUAL, createContextForCreate());
janePerson = getPersonFromTarget(patient);
Person.PersonLinkComponent linkFirstRep = janePerson.getLinkFirstRep();
assertThat(linkFirstRep.getTarget().getReference(), is(equalTo(patient.getIdElement().toVersionless().toString())));
assertThat(linkFirstRep.getAssurance(), is(equalTo(Person.IdentityAssuranceLevel.LEVEL3)));
}
//Case #1
@Test
public void testPatientUpdateOverwritesPersonDataOnChanges() {
Patient janePatient = createPatientAndUpdateLinks(buildJanePatient());
Person janePerson = getPersonFromTarget(janePatient);
//Change Jane's name to paul.
Patient patient1 = buildPaulPatient();
patient1.setId(janePatient.getId());
Patient janePaulPatient = updatePatientAndUpdateLinks(patient1);
assertThat(janePerson, is(samePersonAs(janePaulPatient)));
//Ensure the related person was updated with new info.
Person personFromTarget = getPersonFromTarget(janePaulPatient);
HumanName nameFirstRep = personFromTarget.getNameFirstRep();
assertThat(nameFirstRep.getGivenAsSingleString(), is(equalToIgnoringCase("paul")));
}
@Test
public void testPatientCreateDoesNotOverwritePersonAttributesThatAreInvolvedInLinking() {
Patient paul = buildPaulPatient();
paul.setGender(Enumerations.AdministrativeGender.MALE);
paul = createPatientAndUpdateLinks(paul);
Person personFromTarget = getPersonFromTarget(paul);
assertThat(personFromTarget.getGender(), is(equalTo(Enumerations.AdministrativeGender.MALE)));
Patient paul2 = buildPaulPatient();
paul2.setGender(Enumerations.AdministrativeGender.FEMALE);
paul2 = createPatientAndUpdateLinks(paul2);
assertThat(paul2, is(samePersonAs(paul)));
//Newly matched patients aren't allowed to overwrite Person Attributes unless they are empty, so gender should still be set to male.
Person paul2Person = getPersonFromTarget(paul2);
assertThat(paul2Person.getGender(), is(equalTo(Enumerations.AdministrativeGender.MALE)));
}
@Test
//Test Case #1
public void testPatientUpdatesOverwritePersonData() {
Patient paul = buildPaulPatient();
String incorrectBirthdate = "1980-06-27";
paul.getBirthDateElement().setValueAsString(incorrectBirthdate);
paul = createPatientAndUpdateLinks(paul);
Person personFromTarget = getPersonFromTarget(paul);
assertThat(personFromTarget.getBirthDateElement().getValueAsString(), is(incorrectBirthdate));
String correctBirthdate = "1990-06-28";
paul.getBirthDateElement().setValueAsString(correctBirthdate);
paul = updatePatientAndUpdateLinks(paul);
personFromTarget = getPersonFromTarget(paul);
assertThat(personFromTarget.getBirthDateElement().getValueAsString(), is(equalTo(correctBirthdate)));
assertLinkCount(1);
}
@Test
// Test Case #3
public void testUpdatedEidThatWouldRelinkAlsoCausesPossibleDuplicate() {
String EID_1 = "123";
String EID_2 = "456";
Patient paul = createPatientAndUpdateLinks(addExternalEID(buildPaulPatient(), EID_1));
Person originalPaulPerson = getPersonFromTarget(paul);
Patient jane = createPatientAndUpdateLinks(addExternalEID(buildJanePatient(), EID_2));
Person originalJanePerson = getPersonFromTarget(jane);
clearExternalEIDs(paul);
addExternalEID(paul, EID_2);
updatePatientAndUpdateLinks(paul);
assertThat(originalJanePerson, is(possibleDuplicateOf(originalPaulPerson)));
assertThat(jane, is(samePersonAs(paul)));
}
@Test
//Test Case #2
public void testSinglyLinkedPersonThatGetsAnUpdatedEidSimplyUpdatesEID() {
String EID_1 = "123";
String EID_2 = "456";
Patient paul = createPatientAndUpdateLinks(addExternalEID(buildPaulPatient(), EID_1));
Person originalPaulPerson = getPersonFromTarget(paul);
String oldEid = myEidHelper.getExternalEid(originalPaulPerson).get(0).getValue();
assertThat(oldEid, is(equalTo(EID_1)));
clearExternalEIDs(paul);
addExternalEID(paul, EID_2);
paul = updatePatientAndUpdateLinks(paul);
assertNoDuplicates();
Person newlyFoundPaulPerson = getPersonFromTarget(paul);
assertThat(originalPaulPerson, is(samePersonAs(newlyFoundPaulPerson)));
String newEid = myEidHelper.getExternalEid(newlyFoundPaulPerson).get(0).getValue();
assertThat(newEid, is(equalTo(EID_2)));
}
private void assertNoDuplicates() {
List<EmpiLink> possibleDuplicates = myEmpiLinkDaoSvc.getPossibleDuplicates();
assertThat(possibleDuplicates, hasSize(0));
}
@Test
//Test Case #3
public void testWhenAnEidChangeWouldCauseARelinkingThatAPossibleDuplicateIsCreated() {
Patient patient1 = buildJanePatient();
addExternalEID(patient1, "eid-1");
patient1 = createPatientAndUpdateLinks(patient1);
Patient patient2 = buildPaulPatient();
addExternalEID(patient2, "eid-2");
patient2 = createPatientAndUpdateLinks(patient2);
Patient patient3 = buildPaulPatient();
addExternalEID(patient3, "eid-2");
patient3 = createPatientAndUpdateLinks(patient3);
//Now, Patient 2 and 3 are linked, and the person has 2 eids.
assertThat(patient2, is(samePersonAs(patient3)));
assertNoDuplicates();
// Person A -> {P1}
// Person B -> {P2, P3}
patient2.getIdentifier().clear();
addExternalEID(patient2, "eid-1");
patient2 = updatePatientAndUpdateLinks(patient2);
// Person A -> {P1, P2}
// Person B -> {P3}
// Possible duplicates A<->B
assertThat(patient2, is(samePersonAs(patient1)));
List<EmpiLink> possibleDuplicates = myEmpiLinkDaoSvc.getPossibleDuplicates();
assertThat(possibleDuplicates, hasSize(1));
assertThat(patient3, is(possibleDuplicateOf(patient1)));
}
}

View File

@ -1,421 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.jpa.empi.helper.EmpiLinkHelper;
import ca.uhn.fhir.jpa.empi.interceptor.IEmpiStorageInterceptor;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.server.TransactionLogMessages;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.r4.model.Address;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Person;
import org.hl7.fhir.r4.model.Reference;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class EmpiPersonMergerSvcTest extends BaseEmpiR4Test {
public static final String GIVEN_NAME = "Jenn";
public static final String FAMILY_NAME = "Chan";
public static final String POSTAL_CODE = "M6G 1B4";
private static final String BAD_GIVEN_NAME = "Bob";
private static final EmpiMatchOutcome POSSIBLE_MATCH = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.POSSIBLE_MATCH);
@Autowired
IEmpiPersonMergerSvc myEmpiPersonMergerSvc;
@Autowired
EmpiLinkHelper myEmpiLinkHelper;
@Autowired
IEmpiStorageInterceptor myEmpiStorageInterceptor;
@Autowired
IInterceptorService myInterceptorService;
private Person myFromPerson;
private Person myToPerson;
private Long myFromPersonPid;
private Long myToPersonPid;
private Patient myTargetPatient1;
private Patient myTargetPatient2;
private Patient myTargetPatient3;
@BeforeEach
public void before() {
super.loadEmpiSearchParameters();
myFromPerson = createPerson();
IdType fromPersonId = myFromPerson.getIdElement().toUnqualifiedVersionless();
myFromPersonPid = myIdHelperService.getPidOrThrowException(fromPersonId);
myToPerson = createPerson();
IdType toPersonId = myToPerson.getIdElement().toUnqualifiedVersionless();
myToPersonPid = myIdHelperService.getPidOrThrowException(toPersonId);
myTargetPatient1 = createPatient();
myTargetPatient2 = createPatient();
myTargetPatient3 = createPatient();
// Register the empi storage interceptor after the creates so the delete hook is fired when we merge
myInterceptorService.registerInterceptor(myEmpiStorageInterceptor);
}
@Override
@AfterEach
public void after() throws IOException {
myInterceptorService.unregisterInterceptor(myEmpiStorageInterceptor);
super.after();
}
@Test
public void emptyMerge() {
assertEquals(2, getAllPersons().size());
assertEquals(2, getAllActivePersons().size());
Person mergedPerson = mergePersons();
assertEquals(myToPerson.getIdElement(), mergedPerson.getIdElement());
assertThat(mergedPerson, is(samePersonAs(mergedPerson)));
assertEquals(2, getAllPersons().size());
assertEquals(1, getAllActivePersons().size());
}
private Person mergePersons() {
assertEquals(0, redirectLinkCount());
Person retval = (Person) myEmpiPersonMergerSvc.mergePersons(myFromPerson, myToPerson, createEmpiContext());
assertEquals(1, redirectLinkCount());
return retval;
}
private int redirectLinkCount() {
EmpiLink empiLink = new EmpiLink().setMatchResult(EmpiMatchResultEnum.REDIRECT);
Example<EmpiLink> example = Example.of(empiLink);
return myEmpiLinkDao.findAll(example).size();
}
private EmpiTransactionContext createEmpiContext() {
return new EmpiTransactionContext(TransactionLogMessages.createFromTransactionGuid(UUID.randomUUID().toString()), EmpiTransactionContext.OperationType.MERGE_PERSONS);
}
@Test
public void mergeRemovesPossibleDuplicatesLink() {
EmpiLink empiLink = myEmpiLinkDaoSvc.newEmpiLink().setPersonPid(myToPersonPid).setTargetPid(myFromPersonPid).setMatchResult(EmpiMatchResultEnum.POSSIBLE_DUPLICATE).setLinkSource(EmpiLinkSourceEnum.AUTO);
saveLink(empiLink);
{
List<EmpiLink> foundLinks = myEmpiLinkDao.findAll();
assertEquals(1, foundLinks.size());
assertEquals(EmpiMatchResultEnum.POSSIBLE_DUPLICATE, foundLinks.get(0).getMatchResult());
}
mergePersons();
{
List<EmpiLink> foundLinks = myEmpiLinkDao.findAll();
assertEquals(1, foundLinks.size());
assertEquals(EmpiMatchResultEnum.REDIRECT, foundLinks.get(0).getMatchResult());
}
}
@Test
public void fullFromEmptyTo() {
populatePerson(myFromPerson);
Person mergedPerson = mergePersons();
HumanName returnedName = mergedPerson.getNameFirstRep();
assertEquals(GIVEN_NAME, returnedName.getGivenAsSingleString());
assertEquals(FAMILY_NAME, returnedName.getFamily());
assertEquals(POSTAL_CODE, mergedPerson.getAddressFirstRep().getPostalCode());
}
@Test
public void emptyFromFullTo() {
myFromPerson.getName().add(new HumanName().addGiven(BAD_GIVEN_NAME));
populatePerson(myToPerson);
Person mergedPerson = mergePersons();
HumanName returnedName = mergedPerson.getNameFirstRep();
assertEquals(GIVEN_NAME, returnedName.getGivenAsSingleString());
assertEquals(FAMILY_NAME, returnedName.getFamily());
assertEquals(POSTAL_CODE, mergedPerson.getAddressFirstRep().getPostalCode());
}
@Test
public void fromLinkToNoLink() {
createEmpiLink(myFromPerson, myTargetPatient1);
Person mergedPerson = mergePersons();
List<EmpiLink> links = getNonRedirectLinksByPerson(mergedPerson);
assertEquals(1, links.size());
assertThat(mergedPerson, is(possibleLinkedTo(myTargetPatient1)));
assertEquals(1, myToPerson.getLink().size());
}
@Test
public void fromNoLinkToLink() {
createEmpiLink(myToPerson, myTargetPatient1);
Person mergedPerson = mergePersons();
List<EmpiLink> links = getNonRedirectLinksByPerson(mergedPerson);
assertEquals(1, links.size());
assertThat(mergedPerson, is(possibleLinkedTo(myTargetPatient1)));
assertEquals(1, myToPerson.getLink().size());
}
@Test
public void fromManualLinkOverridesAutoToLink() {
EmpiLink fromLink = createEmpiLink(myFromPerson, myTargetPatient1);
fromLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
fromLink.setMatchResult(EmpiMatchResultEnum.MATCH);
saveLink(fromLink);
createEmpiLink(myToPerson, myTargetPatient1);
mergePersons();
List<EmpiLink> links = getNonRedirectLinksByPerson(myToPerson);
assertEquals(1, links.size());
assertEquals(EmpiLinkSourceEnum.MANUAL, links.get(0).getLinkSource());
}
private List<EmpiLink> getNonRedirectLinksByPerson(Person thePerson) {
return myEmpiLinkDaoSvc.findEmpiLinksByPerson(thePerson).stream()
.filter(link -> !link.isRedirect())
.collect(Collectors.toList());
}
@Test
public void fromManualNoMatchLinkOverridesAutoToLink() {
EmpiLink fromLink = createEmpiLink(myFromPerson, myTargetPatient1);
fromLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
fromLink.setMatchResult(EmpiMatchResultEnum.NO_MATCH);
saveLink(fromLink);
createEmpiLink(myToPerson, myTargetPatient1);
mergePersons();
List<EmpiLink> links = getNonRedirectLinksByPerson(myToPerson);
assertEquals(1, links.size());
assertEquals(EmpiLinkSourceEnum.MANUAL, links.get(0).getLinkSource());
}
@Test
public void fromManualAutoMatchLinkNoOverridesManualToLink() {
createEmpiLink(myFromPerson, myTargetPatient1);
EmpiLink toLink = createEmpiLink(myToPerson, myTargetPatient1);
toLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
toLink.setMatchResult(EmpiMatchResultEnum.NO_MATCH);
saveLink(toLink);
mergePersons();
List<EmpiLink> links = getNonRedirectLinksByPerson(myToPerson);
assertEquals(1, links.size());
assertEquals(EmpiLinkSourceEnum.MANUAL, links.get(0).getLinkSource());
}
@Test
public void fromNoMatchMergeToManualMatchIsError() {
EmpiLink fromLink = createEmpiLink(myFromPerson, myTargetPatient1);
fromLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
fromLink.setMatchResult(EmpiMatchResultEnum.NO_MATCH);
saveLink(fromLink);
EmpiLink toLink = createEmpiLink(myToPerson, myTargetPatient1);
toLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
toLink.setMatchResult(EmpiMatchResultEnum.MATCH);
saveLink(toLink);
try {
mergePersons();
fail();
} catch (InvalidRequestException e) {
assertEquals("A MANUAL NO_MATCH link may not be merged into a MANUAL MATCH link for the same target", e.getMessage());
}
}
@Test
public void fromMatchMergeToManualNoMatchIsError() {
EmpiLink fromLink = createEmpiLink(myFromPerson, myTargetPatient1);
fromLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
fromLink.setMatchResult(EmpiMatchResultEnum.MATCH);
saveLink(fromLink);
EmpiLink toLink = createEmpiLink(myToPerson, myTargetPatient1);
toLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
toLink.setMatchResult(EmpiMatchResultEnum.NO_MATCH);
saveLink(toLink);
try {
mergePersons();
fail();
} catch (InvalidRequestException e) {
assertEquals("A MANUAL MATCH link may not be merged into a MANUAL NO_MATCH link for the same target", e.getMessage());
}
}
@Test
public void fromNoMatchMergeToManualMatchDifferentPatientIsOk() {
EmpiLink fromLink = createEmpiLink(myFromPerson, myTargetPatient1);
fromLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
fromLink.setMatchResult(EmpiMatchResultEnum.NO_MATCH);
saveLink(fromLink);
EmpiLink toLink = createEmpiLink(myToPerson, myTargetPatient2);
toLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
toLink.setMatchResult(EmpiMatchResultEnum.MATCH);
saveLink(toLink);
mergePersons();
assertEquals(1, myToPerson.getLink().size());
assertEquals(3, myEmpiLinkDao.count());
}
@Test
public void from123To1() {
createEmpiLink(myFromPerson, myTargetPatient1);
createEmpiLink(myFromPerson, myTargetPatient2);
createEmpiLink(myFromPerson, myTargetPatient3);
createEmpiLink(myToPerson, myTargetPatient1);
mergePersons();
myEmpiLinkHelper.logEmpiLinks();
assertThat(myToPerson, is(possibleLinkedTo(myTargetPatient1, myTargetPatient2, myTargetPatient3)));
assertEquals(3, myToPerson.getLink().size());
}
@Test
public void from1To123() {
createEmpiLink(myFromPerson, myTargetPatient1);
createEmpiLink(myToPerson, myTargetPatient1);
createEmpiLink(myToPerson, myTargetPatient2);
createEmpiLink(myToPerson, myTargetPatient3);
mergePersons();
myEmpiLinkHelper.logEmpiLinks();
assertThat(myToPerson, is(possibleLinkedTo(myTargetPatient1, myTargetPatient2, myTargetPatient3)));
assertEquals(3, myToPerson.getLink().size());
}
@Test
public void from123To123() {
createEmpiLink(myFromPerson, myTargetPatient1);
createEmpiLink(myFromPerson, myTargetPatient2);
createEmpiLink(myFromPerson, myTargetPatient3);
createEmpiLink(myToPerson, myTargetPatient1);
createEmpiLink(myToPerson, myTargetPatient2);
createEmpiLink(myToPerson, myTargetPatient3);
mergePersons();
myEmpiLinkHelper.logEmpiLinks();
assertThat(myToPerson, is(possibleLinkedTo(myTargetPatient1, myTargetPatient2, myTargetPatient3)));
assertEquals(3, myToPerson.getLink().size());
}
@Test
public void from12To23() {
createEmpiLink(myFromPerson, myTargetPatient1);
createEmpiLink(myFromPerson, myTargetPatient2);
createEmpiLink(myToPerson, myTargetPatient2);
createEmpiLink(myToPerson, myTargetPatient3);
mergePersons();
myEmpiLinkHelper.logEmpiLinks();
assertThat(myToPerson, is(possibleLinkedTo(myTargetPatient1, myTargetPatient2, myTargetPatient3)));
assertEquals(3, myToPerson.getLink().size());
}
@Test
public void testMergeNames() {
myFromPerson.addName().addGiven("Jim");
myFromPerson.getNameFirstRep().addGiven("George");
assertThat(myFromPerson.getName(), hasSize(1));
assertThat(myFromPerson.getName().get(0).getGiven(), hasSize(2));
myToPerson.addName().addGiven("Jeff");
myToPerson.getNameFirstRep().addGiven("George");
assertThat(myToPerson.getName(), hasSize(1));
assertThat(myToPerson.getName().get(0).getGiven(), hasSize(2));
Person mergedPerson = mergePersons();
assertThat(mergedPerson.getName(), hasSize(2));
assertThat(mergedPerson.getName().get(0).getGiven(), hasSize(2));
assertThat(mergedPerson.getName().get(1).getGiven(), hasSize(2));
}
@Test
public void testMergeNamesAllSame() {
myFromPerson.addName().addGiven("Jim");
myFromPerson.getNameFirstRep().addGiven("George");
assertThat(myFromPerson.getName(), hasSize(1));
assertThat(myFromPerson.getName().get(0).getGiven(), hasSize(2));
myToPerson.addName().addGiven("Jim");
myToPerson.getNameFirstRep().addGiven("George");
assertThat(myToPerson.getName(), hasSize(1));
assertThat(myToPerson.getName().get(0).getGiven(), hasSize(2));
mergePersons();
assertThat(myToPerson.getName(), hasSize(1));
assertThat(myToPerson.getName().get(0).getGiven(), hasSize(2));
}
@Test
public void testMergeIdentities() {
myFromPerson.addIdentifier().setValue("aaa");
myFromPerson.addIdentifier().setValue("bbb");
assertThat(myFromPerson.getIdentifier(), hasSize(2));
myToPerson.addIdentifier().setValue("aaa");
myToPerson.addIdentifier().setValue("ccc");
assertThat(myToPerson.getIdentifier(), hasSize(2));
mergePersons();
assertThat(myToPerson.getIdentifier(), hasSize(3));
}
private EmpiLink createEmpiLink(Person thePerson, Patient theTargetPatient) {
thePerson.addLink().setTarget(new Reference(theTargetPatient));
return myEmpiLinkDaoSvc.createOrUpdateLinkEntity(thePerson, theTargetPatient, POSSIBLE_MATCH, EmpiLinkSourceEnum.AUTO, createContextForCreate());
}
private void populatePerson(Person thePerson) {
thePerson.addName(new HumanName().addGiven(GIVEN_NAME).setFamily(FAMILY_NAME));
thePerson.setGender(Enumerations.AdministrativeGender.FEMALE);
thePerson.setBirthDateElement(new DateType("1981-01-01"));
Address address = new Address();
address.addLine("622 College St");
address.addLine("Suite 401");
address.setDistrict("Little Italy");
address.setCity("Toronto");
address.setCountry("Canada");
address.setPostalCode(POSTAL_CODE);
thePerson.setAddress(Collections.singletonList(address));
}
}

View File

@ -1,52 +0,0 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.r4.model.Person;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Optional;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class EmpiResourceDaoSvcTest extends BaseEmpiR4Test {
private static final String TEST_EID = "TEST_EID";
@Autowired
EmpiResourceDaoSvc myResourceDaoSvc;
@BeforeEach
public void before() {
super.loadEmpiSearchParameters();
}
@Test
public void testSearchPersonByEidExcludesInactive() {
Person goodPerson = addExternalEID(createPerson(), TEST_EID);
myPersonDao.update(goodPerson);
Person badPerson = addExternalEID(createPerson(), TEST_EID);
badPerson.setActive(false);
myPersonDao.update(badPerson);
Optional<IAnyResource> foundPerson = myResourceDaoSvc.searchPersonByEid(TEST_EID);
assertTrue(foundPerson.isPresent());
assertThat(foundPerson.get().getIdElement().toUnqualifiedVersionless().getValue(), is(goodPerson.getIdElement().toUnqualifiedVersionless().getValue()));
}
@Test
public void testSearchPersonByEidExcludesNonEmpiManaged() {
Person goodPerson = addExternalEID(createPerson(), TEST_EID);
myPersonDao.update(goodPerson);
Person badPerson = addExternalEID(createPerson(new Person(), false), TEST_EID);
myPersonDao.update(badPerson);
Optional<IAnyResource> foundPerson = myResourceDaoSvc.searchPersonByEid(TEST_EID);
assertTrue(foundPerson.isPresent());
assertThat(foundPerson.get().getIdElement().toUnqualifiedVersionless().getValue(), is(goodPerson.getIdElement().toUnqualifiedVersionless().getValue()));
}
}

View File

@ -1,66 +0,0 @@
{
"version": "1",
"candidateSearchParams": [
{
"resourceType": "Patient",
"searchParams": [
"birthdate"
]
},
{
"resourceType": "*",
"searchParams": [
"identifier"
]
},
{
"resourceType": "Patient",
"searchParams": [
"general-practitioner"
]
}
],
"candidateFilterSearchParams": [
{
"resourceType": "*",
"searchParam": "active",
"fixedValue": "true"
}
],
"matchFields": [
{
"name": "cosine-given-name",
"resourceType": "*",
"resourcePath": "name.given",
"similarity": {
"algorithm": "COSINE",
"matchThreshold": 0.8,
"exact": true
}
},
{
"name": "jaro-last-name",
"resourceType": "*",
"resourcePath": "name.family",
"similarity": {
"algorithm": "JARO_WINKLER",
"matchThreshold": 0.8,
"exact": true
}
},
{
"name": "medicare-id",
"resourceType": "*",
"resourcePath": "identifier",
"matcher": {
"algorithm": "IDENTIFIER",
"identifierSystem": "http://hl7.org/fhir/sid/us-medicare"
}
}
],
"matchResultMap": {
"cosine-given-name": "POSSIBLE_MATCH",
"cosine-given-name,jaro-last-name": "MATCH"
},
"eidSystem": "http://company.io/fhir/NamingSystem/custom-eid-system"
}

View File

@ -10,15 +10,15 @@
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<artifactId>hapi-fhir-jpaserver-empi</artifactId>
<artifactId>hapi-fhir-jpaserver-mdm</artifactId>
<packaging>jar</packaging>
<name>HAPI FHIR JPA Server - Enterprise Master Patient Index</name>
<name>HAPI FHIR JPA Server - Master Data Management</name>
<dependencies>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server-empi</artifactId>
<artifactId>hapi-fhir-server-mdm</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.empi.broker;
package ca.uhn.fhir.jpa.mdm.broker;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -21,14 +21,14 @@ package ca.uhn.fhir.jpa.empi.broker;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.empi.svc.EmpiMatchLinkSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiResourceFilteringSvc;
import ca.uhn.fhir.jpa.mdm.svc.MdmMatchLinkSvc;
import ca.uhn.fhir.jpa.mdm.svc.MdmResourceFilteringSvc;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
import ca.uhn.fhir.rest.server.TransactionLogMessages;
@ -43,17 +43,20 @@ import org.springframework.messaging.MessagingException;
import org.springframework.stereotype.Service;
@Service
public class EmpiMessageHandler implements MessageHandler {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
public class MdmMessageHandler implements MessageHandler {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
@Autowired
private EmpiMatchLinkSvc myEmpiMatchLinkSvc;
private MdmMatchLinkSvc myMdmMatchLinkSvc;
@Autowired
private IInterceptorBroadcaster myInterceptorBroadcaster;
@Autowired
private FhirContext myFhirContext;
@Autowired
private EmpiResourceFilteringSvc myEmpiResourceFilteringSvc;
private MdmResourceFilteringSvc myMdmResourceFilteringSvc;
@Autowired
private IMdmSettings myMdmSettings;
@Override
public void handleMessage(Message<?> theMessage) throws MessagingException {
@ -66,92 +69,93 @@ public class EmpiMessageHandler implements MessageHandler {
ResourceModifiedMessage msg = ((ResourceModifiedJsonMessage) theMessage).getPayload();
try {
if (myEmpiResourceFilteringSvc.shouldBeProcessed(getResourceFromPayload(msg))) {
matchEmpiAndUpdateLinks(msg);
if (myMdmResourceFilteringSvc.shouldBeProcessed(getResourceFromPayload(msg))) {
matchMdmAndUpdateLinks(msg);
}
} catch (Exception e) {
ourLog.error("Failed to handle EMPI Matching Resource:", e);
ourLog.error("Failed to handle MDM Matching Resource:", e);
throw e;
}
}
public void matchEmpiAndUpdateLinks(ResourceModifiedMessage theMsg) {
private void matchMdmAndUpdateLinks(ResourceModifiedMessage theMsg) {
String resourceType = theMsg.getId(myFhirContext).getResourceType();
validateResourceType(resourceType);
EmpiTransactionContext empiContext = createEmpiContext(theMsg);
MdmTransactionContext mdmContext = createMdmContext(theMsg, resourceType);
try {
switch (theMsg.getOperationType()) {
case CREATE:
handleCreatePatientOrPractitioner(theMsg, empiContext);
handleCreatePatientOrPractitioner(theMsg, mdmContext);
break;
case UPDATE:
case MANUALLY_TRIGGERED:
handleUpdatePatientOrPractitioner(theMsg, empiContext);
handleUpdatePatientOrPractitioner(theMsg, mdmContext);
break;
case DELETE:
default:
ourLog.trace("Not processing modified message for {}", theMsg.getOperationType());
}
}catch (Exception e) {
log(empiContext, "Failure during EMPI processing: " + e.getMessage(), e);
log(mdmContext, "Failure during MDM processing: " + e.getMessage(), e);
} finally {
// Interceptor call: EMPI_AFTER_PERSISTED_RESOURCE_CHECKED
// Interceptor call: MDM_AFTER_PERSISTED_RESOURCE_CHECKED
ResourceOperationMessage outgoingMsg = new ResourceOperationMessage(myFhirContext, theMsg.getPayload(myFhirContext), theMsg.getOperationType());
outgoingMsg.setTransactionId(theMsg.getTransactionId());
HookParams params = new HookParams()
.add(ResourceOperationMessage.class, outgoingMsg)
.add(TransactionLogMessages.class, empiContext.getTransactionLogMessages());
myInterceptorBroadcaster.callHooks(Pointcut.EMPI_AFTER_PERSISTED_RESOURCE_CHECKED, params);
.add(TransactionLogMessages.class, mdmContext.getTransactionLogMessages());
myInterceptorBroadcaster.callHooks(Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED, params);
}
}
private EmpiTransactionContext createEmpiContext(ResourceModifiedMessage theMsg) {
private MdmTransactionContext createMdmContext(ResourceModifiedMessage theMsg, String theResourceType) {
TransactionLogMessages transactionLogMessages = TransactionLogMessages.createFromTransactionGuid(theMsg.getTransactionId());
EmpiTransactionContext.OperationType empiOperation;
MdmTransactionContext.OperationType mdmOperation;
switch (theMsg.getOperationType()) {
case CREATE:
empiOperation = EmpiTransactionContext.OperationType.CREATE_RESOURCE;
mdmOperation = MdmTransactionContext.OperationType.CREATE_RESOURCE;
break;
case UPDATE:
empiOperation = EmpiTransactionContext.OperationType.UPDATE_RESOURCE;
mdmOperation = MdmTransactionContext.OperationType.UPDATE_RESOURCE;
break;
case MANUALLY_TRIGGERED:
empiOperation = EmpiTransactionContext.OperationType.SUBMIT_RESOURCE_TO_EMPI;
mdmOperation = MdmTransactionContext.OperationType.SUBMIT_RESOURCE_TO_MDM;
break;
case DELETE:
default:
ourLog.trace("Not creating an EmpiTransactionContext for {}", theMsg.getOperationType());
throw new InvalidRequestException("We can't handle non-update/create operations in EMPI");
ourLog.trace("Not creating an MdmTransactionContext for {}", theMsg.getOperationType());
throw new InvalidRequestException("We can't handle non-update/create operations in MDM");
}
return new EmpiTransactionContext(transactionLogMessages, empiOperation);
return new MdmTransactionContext(transactionLogMessages, mdmOperation, theResourceType);
}
private void validateResourceType(String theResourceType) {
if (!EmpiUtil.supportedTargetType(theResourceType)) {
throw new IllegalStateException("Unsupported resource type submitted to EMPI matching queue: " + theResourceType);
if (!myMdmSettings.isSupportedMdmType(theResourceType)) {
throw new IllegalStateException("Unsupported resource type submitted to MDM matching queue: " + theResourceType);
}
}
private void handleCreatePatientOrPractitioner(ResourceModifiedMessage theMsg, EmpiTransactionContext theEmpiTransactionContext) {
myEmpiMatchLinkSvc.updateEmpiLinksForEmpiTarget(getResourceFromPayload(theMsg), theEmpiTransactionContext);
private void handleCreatePatientOrPractitioner(ResourceModifiedMessage theMsg, MdmTransactionContext theMdmTransactionContext) {
myMdmMatchLinkSvc.updateMdmLinksForMdmSource(getResourceFromPayload(theMsg), theMdmTransactionContext);
}
private IAnyResource getResourceFromPayload(ResourceModifiedMessage theMsg) {
return (IAnyResource) theMsg.getNewPayload(myFhirContext);
}
private void handleUpdatePatientOrPractitioner(ResourceModifiedMessage theMsg, EmpiTransactionContext theEmpiTransactionContext) {
myEmpiMatchLinkSvc.updateEmpiLinksForEmpiTarget(getResourceFromPayload(theMsg), theEmpiTransactionContext);
private void handleUpdatePatientOrPractitioner(ResourceModifiedMessage theMsg, MdmTransactionContext theMdmTransactionContext) {
myMdmMatchLinkSvc.updateMdmLinksForMdmSource(getResourceFromPayload(theMsg), theMdmTransactionContext);
}
private void log(EmpiTransactionContext theEmpiContext, String theMessage) {
theEmpiContext.addTransactionLogMessage(theMessage);
private void log(MdmTransactionContext theMdmContext, String theMessage) {
theMdmContext.addTransactionLogMessage(theMessage);
ourLog.debug(theMessage);
}
private void log(EmpiTransactionContext theEmpiContext, String theMessage, Exception theException) {
theEmpiContext.addTransactionLogMessage(theMessage);
private void log(MdmTransactionContext theMdmContext, String theMessage, Exception theException) {
theMdmContext.addTransactionLogMessage(theMessage);
ourLog.error(theMessage, theException);
}
}

View File

@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.empi.broker;
package ca.uhn.fhir.jpa.mdm.broker;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.jpa.subscription.channel.api.ChannelConsumerSettings;
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelFactory;
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelReceiver;
@ -16,7 +16,7 @@ import javax.annotation.PreDestroy;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -35,29 +35,29 @@ import javax.annotation.PreDestroy;
*/
@Service
public class EmpiQueueConsumerLoader {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
public class MdmQueueConsumerLoader {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
@Autowired
private EmpiMessageHandler myEmpiMessageHandler;
private MdmMessageHandler myMdmMessageHandler;
@Autowired
private IChannelFactory myChannelFactory;
@Autowired
private IEmpiSettings myEmpiSettings;
private IMdmSettings myMdmSettings;
protected IChannelReceiver myEmpiChannel;
protected IChannelReceiver myMdmChannel;
@PostConstruct
public void startListeningToEmpiChannel() {
if (myEmpiChannel == null) {
public void startListeningToMdmChannel() {
if (myMdmChannel == null) {
ChannelConsumerSettings config = new ChannelConsumerSettings();
config.setConcurrentConsumers(myEmpiSettings.getConcurrentConsumers());
myEmpiChannel = myChannelFactory.getOrCreateReceiver(IEmpiSettings.EMPI_CHANNEL_NAME, ResourceModifiedJsonMessage.class, config);
if (myEmpiChannel == null) {
ourLog.error("Unable to create receiver for {}", IEmpiSettings.EMPI_CHANNEL_NAME);
config.setConcurrentConsumers(myMdmSettings.getConcurrentConsumers());
myMdmChannel = myChannelFactory.getOrCreateReceiver(IMdmSettings.EMPI_CHANNEL_NAME, ResourceModifiedJsonMessage.class, config);
if (myMdmChannel == null) {
ourLog.error("Unable to create receiver for {}", IMdmSettings.EMPI_CHANNEL_NAME);
} else {
myEmpiChannel.subscribe(myEmpiMessageHandler);
ourLog.info("EMPI Matching Consumer subscribed to Matching Channel {} with name {}", myEmpiChannel.getClass().getName(), myEmpiChannel.getName());
myMdmChannel.subscribe(myMdmMessageHandler);
ourLog.info("MDM Matching Consumer subscribed to Matching Channel {} with name {}", myMdmChannel.getClass().getName(), myMdmChannel.getName());
}
}
}
@ -65,14 +65,14 @@ public class EmpiQueueConsumerLoader {
@SuppressWarnings("unused")
@PreDestroy
public void stop() {
if (myEmpiChannel != null) {
myEmpiChannel.unsubscribe(myEmpiMessageHandler);
ourLog.info("EMPI Matching Consumer unsubscribed from Matching Channel {} with name {}", myEmpiChannel.getClass().getName(), myEmpiChannel.getName());
if (myMdmChannel != null) {
myMdmChannel.unsubscribe(myMdmMessageHandler);
ourLog.info("MDM Matching Consumer unsubscribed from Matching Channel {} with name {}", myMdmChannel.getClass().getName(), myMdmChannel.getName());
}
}
@VisibleForTesting
public IChannelReceiver getEmpiChannelForUnitTest() {
return myEmpiChannel;
public IChannelReceiver getMdmChannelForUnitTest() {
return myMdmChannel;
}
}

View File

@ -0,0 +1,235 @@
package ca.uhn.fhir.jpa.mdm.config;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.api.IMdmControllerSvc;
import ca.uhn.fhir.mdm.api.IMdmExpungeSvc;
import ca.uhn.fhir.mdm.api.IMdmLinkQuerySvc;
import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
import ca.uhn.fhir.mdm.api.IMdmLinkUpdaterSvc;
import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc;
import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.provider.MdmControllerHelper;
import ca.uhn.fhir.mdm.provider.MdmProviderLoader;
import ca.uhn.fhir.mdm.rules.config.MdmRuleValidator;
import ca.uhn.fhir.mdm.rules.svc.MdmResourceMatcherSvc;
import ca.uhn.fhir.mdm.util.EIDHelper;
import ca.uhn.fhir.mdm.util.MessageHelper;
import ca.uhn.fhir.mdm.util.GoldenResourceHelper;
import ca.uhn.fhir.jpa.dao.mdm.MdmLinkDeleteSvc;
import ca.uhn.fhir.jpa.mdm.broker.MdmMessageHandler;
import ca.uhn.fhir.jpa.mdm.broker.MdmQueueConsumerLoader;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkFactory;
import ca.uhn.fhir.jpa.mdm.interceptor.MdmStorageInterceptor;
import ca.uhn.fhir.jpa.mdm.interceptor.IMdmStorageInterceptor;
import ca.uhn.fhir.jpa.mdm.svc.MdmClearSvcImpl;
import ca.uhn.fhir.jpa.mdm.svc.MdmControllerSvcImpl;
import ca.uhn.fhir.jpa.mdm.svc.MdmEidUpdateService;
import ca.uhn.fhir.jpa.mdm.svc.MdmLinkQuerySvcImpl;
import ca.uhn.fhir.jpa.mdm.svc.MdmLinkSvcImpl;
import ca.uhn.fhir.jpa.mdm.svc.MdmLinkUpdaterSvcImpl;
import ca.uhn.fhir.jpa.mdm.svc.MdmMatchFinderSvcImpl;
import ca.uhn.fhir.jpa.mdm.svc.MdmMatchLinkSvc;
import ca.uhn.fhir.jpa.mdm.svc.MdmGoldenResourceDeletingSvc;
import ca.uhn.fhir.jpa.mdm.svc.GoldenResourceMergerSvcImpl;
import ca.uhn.fhir.jpa.mdm.svc.MdmResourceDaoSvc;
import ca.uhn.fhir.jpa.mdm.svc.MdmResourceFilteringSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmCandidateSearchCriteriaBuilderSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmCandidateSearchSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.FindCandidateByEidSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.FindCandidateByLinkSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.FindCandidateByExampleSvc;
import ca.uhn.fhir.rest.server.util.ISearchParamRetriever;
import ca.uhn.fhir.validation.IResourceLoader;
import org.slf4j.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MdmConsumerConfig {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
@Bean
IMdmStorageInterceptor mdmStorageInterceptor() {
return new MdmStorageInterceptor();
}
@Bean
MdmQueueConsumerLoader mdmQueueConsumerLoader() {
return new MdmQueueConsumerLoader();
}
@Bean
MdmMessageHandler mdmMessageHandler() {
return new MdmMessageHandler();
}
@Bean
MdmMatchLinkSvc mdmMatchLinkSvc() {
return new MdmMatchLinkSvc();
}
@Bean
MdmEidUpdateService eidUpdateService() {
return new MdmEidUpdateService();
}
@Bean
MdmResourceDaoSvc mdmResourceDaoSvc() {
return new MdmResourceDaoSvc();
}
@Bean
IMdmLinkSvc mdmLinkSvc() {
return new MdmLinkSvcImpl();
}
@Bean
GoldenResourceHelper goldenResourceHelper(FhirContext theFhirContext) {
return new GoldenResourceHelper(theFhirContext);
}
@Bean
MessageHelper messageHelper(IMdmSettings theMdmSettings, FhirContext theFhirContext) {
return new MessageHelper(theMdmSettings, theFhirContext);
}
@Bean
MdmSubscriptionLoader mdmSubscriptionLoader() {
return new MdmSubscriptionLoader();
}
@Bean
MdmGoldenResourceFindingSvc mdmGoldenResourceFindingSvc() {
return new MdmGoldenResourceFindingSvc();
}
@Bean
FindCandidateByEidSvc findCandidateByEidSvc() {
return new FindCandidateByEidSvc();
}
@Bean
FindCandidateByLinkSvc findCandidateByLinkSvc() {
return new FindCandidateByLinkSvc();
}
@Bean
FindCandidateByExampleSvc findCandidateByScoreSvc() {
return new FindCandidateByExampleSvc();
}
@Bean
MdmProviderLoader mdmProviderLoader() {
return new MdmProviderLoader();
}
@Bean
MdmRuleValidator mdmRuleValidator(FhirContext theFhirContext, ISearchParamRetriever theSearchParamRetriever) {
return new MdmRuleValidator(theFhirContext, theSearchParamRetriever);
}
@Bean
IMdmMatchFinderSvc mdmMatchFinderSvc() {
return new MdmMatchFinderSvcImpl();
}
@Bean
IGoldenResourceMergerSvc mdmGoldenResourceMergerSvc() {
return new GoldenResourceMergerSvcImpl();
}
@Bean
IMdmLinkQuerySvc mdmLinkQuerySvc() {
return new MdmLinkQuerySvcImpl();
}
@Bean
IMdmExpungeSvc mdmResetSvc(MdmLinkDaoSvc theMdmLinkDaoSvc, MdmGoldenResourceDeletingSvc theDeletingSvc, IMdmSettings theIMdmSettings) {
return new MdmClearSvcImpl(theMdmLinkDaoSvc, theDeletingSvc, theIMdmSettings);
}
@Bean
MdmCandidateSearchSvc mdmCandidateSearchSvc() {
return new MdmCandidateSearchSvc();
}
@Bean
MdmCandidateSearchCriteriaBuilderSvc mdmCriteriaBuilderSvc() {
return new MdmCandidateSearchCriteriaBuilderSvc();
}
@Bean
MdmResourceMatcherSvc mdmResourceComparatorSvc(FhirContext theFhirContext, IMdmSettings theMdmSettings) {
return new MdmResourceMatcherSvc(theFhirContext, theMdmSettings);
}
@Bean
EIDHelper eidHelper(FhirContext theFhirContext, IMdmSettings theMdmSettings) {
return new EIDHelper(theFhirContext, theMdmSettings);
}
@Bean
MdmLinkDaoSvc mdmLinkDaoSvc() {
return new MdmLinkDaoSvc();
}
@Bean
MdmLinkFactory mdmLinkFactory(IMdmSettings theMdmSettings) {
return new MdmLinkFactory(theMdmSettings);
}
@Bean
IMdmLinkUpdaterSvc mdmLinkUpdaterSvc() {
return new MdmLinkUpdaterSvcImpl();
}
@Bean
MdmLoader mdmLoader() {
return new MdmLoader();
}
@Bean
MdmLinkDeleteSvc mdmLinkDeleteSvc() {
return new MdmLinkDeleteSvc();
}
@Bean
MdmResourceFilteringSvc mdmResourceFilteringSvc() {
return new MdmResourceFilteringSvc();
}
@Bean
MdmControllerHelper mdmProviderHelper(FhirContext theFhirContext, IResourceLoader theResourceLoader, IMdmSettings theMdmSettings, MessageHelper messageHelper) {
return new MdmControllerHelper(theFhirContext, theResourceLoader, theMdmSettings, messageHelper);
}
@Bean
IMdmControllerSvc mdmControllerSvc() {
return new MdmControllerSvcImpl();
}
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.empi.config;
package ca.uhn.fhir.jpa.mdm.config;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -20,8 +20,8 @@ package ca.uhn.fhir.jpa.empi.config;
* #L%
*/
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.provider.EmpiProviderLoader;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.provider.MdmProviderLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -31,34 +31,29 @@ import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
@Service
public class EmpiLoader {
private static final Logger ourLog = LoggerFactory.getLogger(EmpiLoader.class);
public class MdmLoader {
private static final Logger ourLog = LoggerFactory.getLogger(MdmLoader.class);
@Autowired
IEmpiSettings myEmpiProperties;
IMdmSettings myMdmSettings;
@Autowired
EmpiProviderLoader myEmpiProviderLoader;
MdmProviderLoader myMdmProviderLoader;
@Autowired
EmpiSubscriptionLoader myEmpiSubscriptionLoader;
@Autowired
EmpiSearchParameterLoader myEmpiSearchParameterLoader;
MdmSubscriptionLoader myMdmSubscriptionLoader;
@EventListener(classes = {ContextRefreshedEvent.class})
// This @Order is here to ensure that MatchingQueueSubscriberLoader has initialized before we initialize this.
// Otherwise the EMPI subscriptions won't get loaded into the SubscriptionRegistry
// Otherwise the MDM subscriptions won't get loaded into the SubscriptionRegistry
@Order
public void updateSubscriptions() {
if (!myEmpiProperties.isEnabled()) {
if (!myMdmSettings.isEnabled()) {
return;
}
myEmpiProviderLoader.loadProvider();
ourLog.info("EMPI provider registered");
myMdmProviderLoader.loadProvider();
ourLog.info("MDM provider registered");
myEmpiSubscriptionLoader.daoUpdateEmpiSubscriptions();
ourLog.info("EMPI subscriptions updated");
myEmpiSearchParameterLoader.daoUpdateEmpiSearchParameters();
ourLog.info("EMPI search parameters updated");
myMdmSubscriptionLoader.daoUpdateMdmSubscriptions();
ourLog.info("MDM subscriptions updated");
}
}

View File

@ -0,0 +1,78 @@
package ca.uhn.fhir.jpa.mdm.config;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.api.IMdmChannelSubmitterSvc;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.api.IMdmSubmitSvc;
import ca.uhn.fhir.mdm.rules.config.MdmRuleValidator;
import ca.uhn.fhir.jpa.dao.mdm.MdmLinkDeleteSvc;
import ca.uhn.fhir.jpa.mdm.interceptor.MdmSubmitterInterceptorLoader;
import ca.uhn.fhir.jpa.mdm.svc.MdmChannelSubmitterSvcImpl;
import ca.uhn.fhir.jpa.mdm.svc.MdmGoldenResourceDeletingSvc;
import ca.uhn.fhir.jpa.mdm.svc.MdmSearchParamSvc;
import ca.uhn.fhir.jpa.mdm.svc.MdmSubmitSvcImpl;
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelFactory;
import ca.uhn.fhir.mdm.util.MessageHelper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@Configuration
public class MdmSubmitterConfig {
@Bean
MdmSubmitterInterceptorLoader mdmSubmitterInterceptorLoader() {
return new MdmSubmitterInterceptorLoader();
}
@Bean
MdmSearchParamSvc mdmSearchParamSvc() {
return new MdmSearchParamSvc();
}
@Bean
MdmRuleValidator mdmRuleValidator(FhirContext theFhirContext) {
return new MdmRuleValidator(theFhirContext, mdmSearchParamSvc());
}
@Bean
MdmLinkDeleteSvc mdmLinkDeleteSvc() {
return new MdmLinkDeleteSvc();
}
@Bean
MdmGoldenResourceDeletingSvc mdmGoldenResourceDeletingSvc() {
return new MdmGoldenResourceDeletingSvc();
}
@Bean
@Lazy
IMdmChannelSubmitterSvc mdmChannelSubmitterSvc(FhirContext theFhirContext, IChannelFactory theChannelFactory) {
return new MdmChannelSubmitterSvcImpl(theFhirContext, theChannelFactory);
}
@Bean
IMdmSubmitSvc mdmBatchService(IMdmSettings theMdmSetting) {
return new MdmSubmitSvcImpl();
}
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.empi.config;
package ca.uhn.fhir.jpa.mdm.config;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -22,9 +22,9 @@ package ca.uhn.fhir.jpa.empi.config;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.mdm.api.MdmConstants;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
@ -37,12 +37,16 @@ import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class EmpiSubscriptionLoader {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
import java.util.List;
import java.util.stream.Collectors;
@Service
public class MdmSubscriptionLoader {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
public static final String MDM_SUBSCIPRION_ID_PREFIX = "mdm-";
public static final String EMPI_PATIENT_SUBSCRIPTION_ID = "empi-patient";
public static final String EMPI_PRACTITIONER_SUBSCRIPTION_ID = "empi-practitioner";
@Autowired
public FhirContext myFhirContext;
@Autowired
@ -51,27 +55,35 @@ public class EmpiSubscriptionLoader {
public IdHelperService myIdHelperService;
@Autowired
IChannelNamer myChannelNamer;
@Autowired
private IMdmSettings myMdmSettings;
private IFhirResourceDao<IBaseResource> mySubscriptionDao;
synchronized public void daoUpdateEmpiSubscriptions() {
IBaseResource patientSub;
IBaseResource practitionerSub;
synchronized public void daoUpdateMdmSubscriptions() {
List<IBaseResource> subscriptions;
List<String> mdmResourceTypes = myMdmSettings.getMdmRules().getMdmTypes();
switch (myFhirContext.getVersion().getVersion()) {
case DSTU3:
patientSub = buildEmpiSubscriptionDstu3(EMPI_PATIENT_SUBSCRIPTION_ID, "Patient?");
practitionerSub = buildEmpiSubscriptionDstu3(EMPI_PRACTITIONER_SUBSCRIPTION_ID, "Practitioner?");
subscriptions = mdmResourceTypes
.stream()
.map(resourceType -> buildMdmSubscriptionDstu3(MDM_SUBSCIPRION_ID_PREFIX + resourceType, resourceType+"?"))
.collect(Collectors.toList());
break;
case R4:
patientSub = buildEmpiSubscriptionR4(EMPI_PATIENT_SUBSCRIPTION_ID, "Patient?");
practitionerSub = buildEmpiSubscriptionR4(EMPI_PRACTITIONER_SUBSCRIPTION_ID, "Practitioner?");
subscriptions = mdmResourceTypes
.stream()
.map(resourceType -> buildMdmSubscriptionR4(MDM_SUBSCIPRION_ID_PREFIX + resourceType, resourceType+"?"))
.collect(Collectors.toList());
break;
default:
throw new ConfigurationException("EMPI not supported for FHIR version " + myFhirContext.getVersion().getVersion());
throw new ConfigurationException("MDM not supported for FHIR version " + myFhirContext.getVersion().getVersion());
}
mySubscriptionDao = myDaoRegistry.getResourceDao("Subscription");
updateIfNotPresent(patientSub);
updateIfNotPresent(practitionerSub);
for (IBaseResource subscription : subscriptions) {
updateIfNotPresent(subscription);
}
}
private synchronized void updateIfNotPresent(IBaseResource theSubscription) {
@ -83,30 +95,30 @@ public class EmpiSubscriptionLoader {
}
}
private org.hl7.fhir.dstu3.model.Subscription buildEmpiSubscriptionDstu3(String theId, String theCriteria) {
private org.hl7.fhir.dstu3.model.Subscription buildMdmSubscriptionDstu3(String theId, String theCriteria) {
org.hl7.fhir.dstu3.model.Subscription retval = new org.hl7.fhir.dstu3.model.Subscription();
retval.setId(theId);
retval.setReason("EMPI");
retval.setReason("MDM");
retval.setStatus(org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus.REQUESTED);
retval.setCriteria(theCriteria);
retval.getMeta().addTag().setSystem(EmpiConstants.SYSTEM_EMPI_MANAGED).setCode(EmpiConstants.CODE_HAPI_EMPI_MANAGED);
retval.getMeta().addTag().setSystem(MdmConstants.SYSTEM_MDM_MANAGED).setCode(MdmConstants.CODE_HAPI_MDM_MANAGED);
org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelComponent channel = retval.getChannel();
channel.setType(org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType.MESSAGE);
channel.setEndpoint("channel:" + myChannelNamer.getChannelName(IEmpiSettings.EMPI_CHANNEL_NAME, new ChannelProducerSettings()));
channel.setEndpoint("channel:" + myChannelNamer.getChannelName(IMdmSettings.EMPI_CHANNEL_NAME, new ChannelProducerSettings()));
channel.setPayload("application/json");
return retval;
}
private Subscription buildEmpiSubscriptionR4(String theId, String theCriteria) {
private Subscription buildMdmSubscriptionR4(String theId, String theCriteria) {
Subscription retval = new Subscription();
retval.setId(theId);
retval.setReason("EMPI");
retval.setReason("MDM");
retval.setStatus(Subscription.SubscriptionStatus.REQUESTED);
retval.setCriteria(theCriteria);
retval.getMeta().addTag().setSystem(EmpiConstants.SYSTEM_EMPI_MANAGED).setCode(EmpiConstants.CODE_HAPI_EMPI_MANAGED);
retval.getMeta().addTag().setSystem(MdmConstants.SYSTEM_MDM_MANAGED).setCode(MdmConstants.CODE_HAPI_MDM_MANAGED);
Subscription.SubscriptionChannelComponent channel = retval.getChannel();
channel.setType(Subscription.SubscriptionChannelType.MESSAGE);
channel.setEndpoint("channel:" + myChannelNamer.getChannelName(IEmpiSettings.EMPI_CHANNEL_NAME, new ChannelProducerSettings()));
channel.setEndpoint("channel:" + myChannelNamer.getChannelName(IMdmSettings.EMPI_CHANNEL_NAME, new ChannelProducerSettings()));
channel.setPayload("application/json");
return retval;
}

View File

@ -0,0 +1,338 @@
package ca.uhn.fhir.jpa.mdm.dao;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.entity.MdmLink;
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.log.Logs;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
public class MdmLinkDaoSvc {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
@Autowired
private IMdmLinkDao myMdmLinkDao;
@Autowired
private MdmLinkFactory myMdmLinkFactory;
@Autowired
private IdHelperService myIdHelperService;
@Autowired
private FhirContext myFhirContext;
@Transactional
public MdmLink createOrUpdateLinkEntity(IBaseResource theGoldenResource, IBaseResource theSourceResource, MdmMatchOutcome theMatchOutcome, MdmLinkSourceEnum theLinkSource, @Nullable MdmTransactionContext theMdmTransactionContext) {
Long goldenResourcePid = myIdHelperService.getPidOrNull(theGoldenResource);
Long sourceResourcePid = myIdHelperService.getPidOrNull(theSourceResource);
MdmLink mdmLink = getOrCreateMdmLinkByGoldenResourcePidAndSourceResourcePid(goldenResourcePid, sourceResourcePid);
mdmLink.setLinkSource(theLinkSource);
mdmLink.setMatchResult(theMatchOutcome.getMatchResultEnum());
// Preserve these flags for link updates
mdmLink.setEidMatch(theMatchOutcome.isEidMatch() | mdmLink.isEidMatch());
mdmLink.setHadToCreateNewGoldenResource(theMatchOutcome.isCreatedNewResource() | mdmLink.getHadToCreateNewGoldenResource());
mdmLink.setMdmSourceType(myFhirContext.getResourceType(theSourceResource));
if (mdmLink.getScore() != null) {
mdmLink.setScore(Math.max(theMatchOutcome.score, mdmLink.getScore()));
} else {
mdmLink.setScore(theMatchOutcome.score);
}
String message = String.format("Creating MdmLink from %s to %s -> %s", theGoldenResource.getIdElement().toUnqualifiedVersionless(), theSourceResource.getIdElement().toUnqualifiedVersionless(), theMatchOutcome);
theMdmTransactionContext.addTransactionLogMessage(message);
ourLog.debug(message);
save(mdmLink);
return mdmLink;
}
@Nonnull
public MdmLink getOrCreateMdmLinkByGoldenResourcePidAndSourceResourcePid(Long theGoldenResourcePid, Long theSourceResourcePid) {
Optional<MdmLink> oExisting = getLinkByGoldenResourcePidAndSourceResourcePid(theGoldenResourcePid, theSourceResourcePid);
if (oExisting.isPresent()) {
return oExisting.get();
} else {
MdmLink newLink = myMdmLinkFactory.newMdmLink();
newLink.setGoldenResourcePid(theGoldenResourcePid);
newLink.setSourcePid(theSourceResourcePid);
return newLink;
}
}
public Optional<MdmLink> getLinkByGoldenResourcePidAndSourceResourcePid(Long theGoldenResourcePid, Long theSourceResourcePid) {
if (theSourceResourcePid == null || theGoldenResourcePid == null) {
return Optional.empty();
}
MdmLink link = myMdmLinkFactory.newMdmLink();
link.setSourcePid(theSourceResourcePid);
link.setGoldenResourcePid(theGoldenResourcePid);
Example<MdmLink> example = Example.of(link);
return myMdmLinkDao.findOne(example);
}
/**
* Given a source resource Pid, and a match result, return all links that match these criteria.
*
* @param theSourcePid the source of the relationship.
* @param theMatchResult the Match Result of the relationship
* @return a list of {@link MdmLink} entities matching these criteria.
*/
public List<MdmLink> getMdmLinksBySourcePidAndMatchResult(Long theSourcePid, MdmMatchResultEnum theMatchResult) {
MdmLink exampleLink = myMdmLinkFactory.newMdmLink();
exampleLink.setSourcePid(theSourcePid);
exampleLink.setMatchResult(theMatchResult);
Example<MdmLink> example = Example.of(exampleLink);
return myMdmLinkDao.findAll(example);
}
/**
* Given a source Pid, return its Matched {@link MdmLink}. There can only ever be at most one of these, but its possible
* the source has no matches, and may return an empty optional.
*
* @param theSourcePid The Pid of the source you wish to find the matching link for.
* @return the {@link MdmLink} that contains the Match information for the source.
*/
public Optional<MdmLink> getMatchedLinkForSourcePid(Long theSourcePid) {
MdmLink exampleLink = myMdmLinkFactory.newMdmLink();
exampleLink.setSourcePid(theSourcePid);
exampleLink.setMatchResult(MdmMatchResultEnum.MATCH);
Example<MdmLink> example = Example.of(exampleLink);
return myMdmLinkDao.findOne(example);
}
/**
* Given an IBaseResource, return its Matched {@link MdmLink}. There can only ever be at most one of these, but its possible
* the source has no matches, and may return an empty optional.
*
* @param theSourceResource The IBaseResource representing the source you wish to find the matching link for.
* @return the {@link MdmLink} that contains the Match information for the source.
*/
public Optional<MdmLink> getMatchedLinkForSource(IBaseResource theSourceResource) {
Long pid = myIdHelperService.getPidOrNull(theSourceResource);
if (pid == null) {
return Optional.empty();
}
MdmLink exampleLink = myMdmLinkFactory.newMdmLink();
exampleLink.setSourcePid(pid);
exampleLink.setMatchResult(MdmMatchResultEnum.MATCH);
Example<MdmLink> example = Example.of(exampleLink);
return myMdmLinkDao.findOne(example);
}
/**
* Given a golden resource a source and a match result, return the matching {@link MdmLink}, if it exists.
*
* @param theGoldenResourcePid The Pid of the Golden Resource in the relationship
* @param theSourcePid The Pid of the source in the relationship
* @param theMatchResult The MatchResult you are looking for.
* @return an Optional {@link MdmLink} containing the matched link if it exists.
*/
public Optional<MdmLink> getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(Long theGoldenResourcePid,
Long theSourcePid, MdmMatchResultEnum theMatchResult) {
MdmLink exampleLink = myMdmLinkFactory.newMdmLink();
exampleLink.setGoldenResourcePid(theGoldenResourcePid);
exampleLink.setSourcePid(theSourcePid);
exampleLink.setMatchResult(theMatchResult);
Example<MdmLink> example = Example.of(exampleLink);
return myMdmLinkDao.findOne(example);
}
/**
* Get all {@link MdmLink} which have {@link MdmMatchResultEnum#POSSIBLE_DUPLICATE} as their match result.
*
* @return A list of {@link MdmLink} that hold potential duplicate golden resources.
*/
public List<MdmLink> getPossibleDuplicates() {
MdmLink exampleLink = myMdmLinkFactory.newMdmLink();
exampleLink.setMatchResult(MdmMatchResultEnum.POSSIBLE_DUPLICATE);
Example<MdmLink> example = Example.of(exampleLink);
return myMdmLinkDao.findAll(example);
}
public Optional<MdmLink> findMdmLinkBySource(IBaseResource theSourceResource) {
@Nullable Long pid = myIdHelperService.getPidOrNull(theSourceResource);
if (pid == null) {
return Optional.empty();
}
MdmLink exampleLink = myMdmLinkFactory.newMdmLink().setSourcePid(pid);
Example<MdmLink> example = Example.of(exampleLink);
return myMdmLinkDao.findOne(example);
}
/**
* Delete a given {@link MdmLink}. Note that this does not clear out the Golden resource.
* It is a simple entity delete.
*
* @param theMdmLink the {@link MdmLink} to delete.
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void deleteLink(MdmLink theMdmLink) {
myMdmLinkDao.delete(theMdmLink);
}
/**
* Given a Golden Resource, return all links in which they are the source Golden Resource of the {@link MdmLink}
*
* @param theGoldenResource The {@link IBaseResource} Golden Resource who's links you would like to retrieve.
* @return A list of all {@link MdmLink} entities in which theGoldenResource is the source Golden Resource
*/
public List<MdmLink> findMdmLinksByGoldenResource(IBaseResource theGoldenResource) {
Long pid = myIdHelperService.getPidOrNull(theGoldenResource);
if (pid == null) {
return Collections.emptyList();
}
MdmLink exampleLink = myMdmLinkFactory.newMdmLink().setGoldenResourcePid(pid);
Example<MdmLink> example = Example.of(exampleLink);
return myMdmLinkDao.findAll(example);
}
/**
* Delete all {@link MdmLink} entities, and return all resource PIDs from the source of the relationship.
*
* @return A list of Long representing the related Golden Resource Pids.
*/
@Transactional
public List<Long> deleteAllMdmLinksAndReturnGoldenResourcePids() {
List<MdmLink> all = myMdmLinkDao.findAll();
return deleteMdmLinksAndReturnGoldenResourcePids(all);
}
private List<Long> deleteMdmLinksAndReturnGoldenResourcePids(List<MdmLink> theLinks) {
Set<Long> goldenResources = theLinks.stream().map(MdmLink::getGoldenResourcePid).collect(Collectors.toSet());
//TODO GGG this is probably invalid... we are essentially looking for GOLDEN -> GOLDEN links, which are either POSSIBLE_DUPLICATE
//and REDIRECT
goldenResources.addAll(theLinks.stream()
.filter(link -> link.getMatchResult().equals(MdmMatchResultEnum.REDIRECT)
|| link.getMatchResult().equals(MdmMatchResultEnum.POSSIBLE_DUPLICATE))
.map(MdmLink::getSourcePid).collect(Collectors.toSet()));
ourLog.info("Deleting {} MDM link records...", theLinks.size());
myMdmLinkDao.deleteAll(theLinks);
ourLog.info("{} MDM link records deleted", theLinks.size());
return new ArrayList<>(goldenResources);
}
/**
* Given a valid {@link String}, delete all {@link MdmLink} entities for that type, and get the Pids
* for the Golden Resources which were the sources of the links.
*
* @param theSourceType the type of relationship you would like to delete.
* @return A list of longs representing the Pids of the Golden Resources resources used as the sources of the relationships that were deleted.
*/
public List<Long> deleteAllMdmLinksOfTypeAndReturnGoldenResourcePids(String theSourceType) {
MdmLink link = new MdmLink();
link.setMdmSourceType(theSourceType);
Example<MdmLink> exampleLink = Example.of(link);
List<MdmLink> allOfType = myMdmLinkDao.findAll(exampleLink);
return deleteMdmLinksAndReturnGoldenResourcePids(allOfType);
}
/**
* Persist an MDM link to the database.
*
* @param theMdmLink the link to save.
* @return the persisted {@link MdmLink} entity.
*/
public MdmLink save(MdmLink theMdmLink) {
if (theMdmLink.getCreated() == null) {
theMdmLink.setCreated(new Date());
}
theMdmLink.setUpdated(new Date());
return myMdmLinkDao.save(theMdmLink);
}
/**
* Given an example {@link MdmLink}, return all links from the database which match the example.
*
* @param theExampleLink The MDM link containing the data we would like to search for.
* @return a list of {@link MdmLink} entities which match the example.
*/
public List<MdmLink> findMdmLinkByExample(Example<MdmLink> theExampleLink) {
return myMdmLinkDao.findAll(theExampleLink);
}
/**
* Given a source {@link IBaseResource}, return all {@link MdmLink} entities in which this source is the source
* of the relationship. This will show you all links for a given Patient/Practitioner.
*
* @param theSourceResource the source resource to find links for.
* @return all links for the source.
*/
public List<MdmLink> findMdmLinksBySourceResource(IBaseResource theSourceResource) {
Long pid = myIdHelperService.getPidOrNull(theSourceResource);
if (pid == null) {
return Collections.emptyList();
}
MdmLink exampleLink = myMdmLinkFactory.newMdmLink().setSourcePid(pid);
Example<MdmLink> example = Example.of(exampleLink);
return myMdmLinkDao.findAll(example);
}
/**
* Finds all {@link MdmLink} entities in which theGoldenResource's PID is the source
* of the relationship.
*
* @param theGoldenResource the source resource to find links for.
* @return all links for the source.
*/
public List<MdmLink> findMdmMatchLinksByGoldenResource(IBaseResource theGoldenResource) {
Long pid = myIdHelperService.getPidOrNull(theGoldenResource);
if (pid == null) {
return Collections.emptyList();
}
MdmLink exampleLink = myMdmLinkFactory.newMdmLink().setGoldenResourcePid(pid);
exampleLink.setMatchResult(MdmMatchResultEnum.MATCH);
Example<MdmLink> example = Example.of(exampleLink);
return myMdmLinkDao.findAll(example);
}
/**
* Factory delegation method, whenever you need a new MdmLink, use this factory method.
* //TODO Should we make the constructor private for MdmLink? or work out some way to ensure they can only be instantiated via factory.
*
* @return A new {@link MdmLink}.
*/
public MdmLink newMdmLink() {
return myMdmLinkFactory.newMdmLink();
}
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.empi.dao;
package ca.uhn.fhir.jpa.mdm.dao;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -20,24 +20,24 @@ package ca.uhn.fhir.jpa.empi.dao;
* #L%
*/
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.jpa.entity.MdmLink;
import org.springframework.beans.factory.annotation.Autowired;
public class EmpiLinkFactory {
private final IEmpiSettings myEmpiSettings;
public class MdmLinkFactory {
private final IMdmSettings myMdmSettings;
@Autowired
public EmpiLinkFactory(IEmpiSettings theEmpiSettings) {
myEmpiSettings = theEmpiSettings;
public MdmLinkFactory(IMdmSettings theMdmSettings) {
myMdmSettings = theMdmSettings;
}
/**
* Create a new EmpiLink, populating it with the version of the ruleset used to create it.
* Create a new {@link MdmLink}, populating it with the version of the ruleset used to create it.
*
* @return the new {@link EmpiLink}
* @return the new {@link MdmLink}
*/
public EmpiLink newEmpiLink() {
return new EmpiLink(myEmpiSettings.getRuleVersion());
public MdmLink newMdmLink() {
return new MdmLink(myMdmSettings.getRuleVersion());
}
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.empi.interceptor;
package ca.uhn.fhir.jpa.mdm.interceptor;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -20,5 +20,5 @@ package ca.uhn.fhir.jpa.empi.interceptor;
* #L%
*/
public interface IEmpiStorageInterceptor {
public interface IMdmStorageInterceptor {
}

View File

@ -0,0 +1,191 @@
package ca.uhn.fhir.jpa.mdm.interceptor;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.api.MdmConstants;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.model.CanonicalEID;
import ca.uhn.fhir.mdm.util.EIDHelper;
import ca.uhn.fhir.mdm.util.MdmResourceUtil;
import ca.uhn.fhir.mdm.util.GoldenResourceHelper;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.dao.mdm.MdmLinkDeleteSvc;
import ca.uhn.fhir.jpa.dao.expunge.ExpungeEverythingService;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
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 java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class MdmStorageInterceptor implements IMdmStorageInterceptor {
private static final Logger ourLog = LoggerFactory.getLogger(MdmStorageInterceptor.class);
@Autowired
private ExpungeEverythingService myExpungeEverythingService;
@Autowired
private MdmLinkDeleteSvc myMdmLinkDeleteSvc;
@Autowired
private FhirContext myFhirContext;
@Autowired
private EIDHelper myEIDHelper;
@Autowired
private IMdmSettings myMdmSettings;
@Autowired
private GoldenResourceHelper myGoldenResourceHelper;
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
public void blockManualResourceManipulationOnCreate(IBaseResource theBaseResource, RequestDetails theRequestDetails, ServletRequestDetails theServletRequestDetails) {
//If running in single EID mode, forbid multiple eids.
if (myMdmSettings.isPreventMultipleEids()) {
forbidIfHasMultipleEids(theBaseResource);
}
// TODO GGG MDM find a better way to identify internal calls?
if (isInternalRequest(theRequestDetails)) {
return;
}
forbidIfMdmManagedTagIsPresent(theBaseResource);
}
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
public void blockManualGoldenResourceManipulationOnUpdate(IBaseResource theOldResource, IBaseResource theUpdatedResource, RequestDetails theRequestDetails, ServletRequestDetails theServletRequestDetails) {
//If running in single EID mode, forbid multiple eids.
if (myMdmSettings.isPreventMultipleEids()) {
forbidIfHasMultipleEids(theUpdatedResource);
}
if (MdmResourceUtil.isGoldenRecordRedirected(theUpdatedResource)) {
ourLog.debug("Deleting MDM links to deactivated Golden resource {}", theUpdatedResource.getIdElement().toUnqualifiedVersionless());
int deleted = myMdmLinkDeleteSvc.deleteNonRedirectWithAnyReferenceTo(theUpdatedResource);
if (deleted > 0) {
ourLog.debug("Deleted {} MDM links", deleted);
}
}
if (isInternalRequest(theRequestDetails)) {
return;
}
forbidIfMdmManagedTagIsPresent(theOldResource);
forbidModifyingMdmTag(theUpdatedResource, theOldResource);
if (myMdmSettings.isPreventEidUpdates()) {
forbidIfModifyingExternalEidOnTarget(theUpdatedResource, theOldResource);
}
}
@Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED)
public void deleteMdmLinks(RequestDetails theRequest, IBaseResource theResource) {
if (!myMdmSettings.isSupportedMdmType(myFhirContext.getResourceType(theResource))) {
return;
}
myMdmLinkDeleteSvc.deleteWithAnyReferenceTo(theResource);
}
private void forbidIfModifyingExternalEidOnTarget(IBaseResource theNewResource, IBaseResource theOldResource) {
List<CanonicalEID> newExternalEids = myEIDHelper.getExternalEid(theNewResource);
List<CanonicalEID> oldExternalEids = myEIDHelper.getExternalEid(theOldResource);
if (oldExternalEids.isEmpty()) {
return;
}
if (!myEIDHelper.eidMatchExists(newExternalEids, oldExternalEids)) {
throwBlockEidChange();
}
}
private void throwBlockEidChange() {
throw new ForbiddenOperationException("While running with EID updates disabled, EIDs may not be updated on source resources");
}
/*
* Will throw a forbidden error if a request attempts to add/remove the MDM tag on a Resource.
*/
private void forbidModifyingMdmTag(IBaseResource theNewResource, IBaseResource theOldResource) {
if (MdmResourceUtil.isMdmManaged(theNewResource) != MdmResourceUtil.isMdmManaged(theOldResource)) {
throwBlockMdmManagedTagChange();
}
}
private void forbidIfHasMultipleEids(IBaseResource theResource) {
String resourceType = extractResourceType(theResource);
if (myMdmSettings.isSupportedMdmType(resourceType)) {
if (myEIDHelper.getExternalEid(theResource).size() > 1) {
throwBlockMultipleEids();
}
}
}
/*
* We assume that if we have RequestDetails, then this was an HTTP request and not an internal one.
*/
private boolean isInternalRequest(RequestDetails theRequestDetails) {
return theRequestDetails == null;
}
private void forbidIfMdmManagedTagIsPresent(IBaseResource theResource) {
if (MdmResourceUtil.isMdmManaged(theResource)) {
throwModificationBlockedByMdm();
}
if (MdmResourceUtil.hasGoldenRecordSystemTag(theResource)) {
throwModificationBlockedByMdm();
}
}
private void throwBlockMdmManagedTagChange() {
throw new ForbiddenOperationException("The " + MdmConstants.CODE_HAPI_MDM_MANAGED + " tag on a resource may not be changed once created.");
}
private void throwModificationBlockedByMdm() {
throw new ForbiddenOperationException("Cannot create or modify Resources that are managed by MDM. This resource contains a tag with one of these systems: " + MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS + " or " + MdmConstants.SYSTEM_MDM_MANAGED);
}
private void throwBlockMultipleEids() {
throw new ForbiddenOperationException("While running with multiple EIDs disabled, source resources may have at most one EID.");
}
private String extractResourceType(IBaseResource theResource) {
return myFhirContext.getResourceType(theResource);
}
@Hook(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING)
public void expungeAllMdmLinks(AtomicInteger theCounter) {
ourLog.debug("Expunging all MdmLink records");
theCounter.addAndGet(myExpungeEverythingService.expungeEverythingByType(MdmLink.class));
}
@Hook(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE)
public void expungeAllMatchedMdmLinks(AtomicInteger theCounter, IBaseResource theResource) {
ourLog.debug("Expunging MdmLink records with reference to {}", theResource.getIdElement());
theCounter.addAndGet(myMdmLinkDeleteSvc.deleteWithAnyReferenceTo(theResource));
}
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.empi.interceptor;
package ca.uhn.fhir.jpa.mdm.interceptor;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -20,8 +20,8 @@ package ca.uhn.fhir.jpa.empi.interceptor;
* #L%
*/
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionSubmitInterceptorLoader;
@ -31,15 +31,15 @@ import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct;
public class EmpiSubmitterInterceptorLoader {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
public class MdmSubmitterInterceptorLoader {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
@Autowired
private IEmpiSettings myEmpiProperties;
private IMdmSettings myMdmSettings;
@Autowired
DaoConfig myDaoConfig;
@Autowired
private IEmpiStorageInterceptor myIEmpiStorageInterceptor;
private IMdmStorageInterceptor myIMdmStorageInterceptor;
@Autowired
private IInterceptorService myInterceptorService;
@Autowired
@ -47,13 +47,13 @@ public class EmpiSubmitterInterceptorLoader {
@PostConstruct
public void start() {
if (!myEmpiProperties.isEnabled()) {
if (!myMdmSettings.isEnabled()) {
return;
}
myDaoConfig.addSupportedSubscriptionType(Subscription.SubscriptionChannelType.MESSAGE);
myInterceptorService.registerInterceptor(myIEmpiStorageInterceptor);
ourLog.info("EMPI interceptor registered");
myInterceptorService.registerInterceptor(myIMdmStorageInterceptor);
ourLog.info("MDM interceptor registered");
// We need to call SubscriptionSubmitInterceptorLoader.start() again in case there were no subscription types the first time it was called.
mySubscriptionSubmitInterceptorLoader.start();
}

View File

@ -0,0 +1,164 @@
package ca.uhn.fhir.jpa.mdm.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.mdm.util.GoldenResourceHelper;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.mdm.util.MdmResourceUtil;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class GoldenResourceMergerSvcImpl implements IGoldenResourceMergerSvc {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
@Autowired
GoldenResourceHelper myGoldenResourceHelper;
@Autowired
MdmLinkDaoSvc myMdmLinkDaoSvc;
@Autowired
IMdmLinkSvc myMdmLinkSvc;
@Autowired
IdHelperService myIdHelperService;
@Autowired
MdmResourceDaoSvc myMdmResourceDaoSvc;
@Override
@Transactional
public IAnyResource mergeGoldenResources(IAnyResource theFromGoldenResource, IAnyResource theToGoldenResource, MdmTransactionContext theMdmTransactionContext) {
Long fromGoldenResourcePid = myIdHelperService.getPidOrThrowException(theFromGoldenResource);
Long toGoldenResourcePid = myIdHelperService.getPidOrThrowException(theToGoldenResource);
String resourceType = theMdmTransactionContext.getResourceType();
//Merge attributes, to be determined when survivorship is solved.
myGoldenResourceHelper.mergeFields(theFromGoldenResource, theToGoldenResource);
//Merge the links from the FROM to the TO resource. Clean up dangling links.
mergeGoldenResourceLinks(theFromGoldenResource, theToGoldenResource, toGoldenResourcePid, theMdmTransactionContext);
//Create the new REDIRECT link
addMergeLink(toGoldenResourcePid, fromGoldenResourcePid, resourceType);
//Strip the golden resource tag from the now-deprecated resource.
myMdmResourceDaoSvc.removeGoldenResourceTag(theFromGoldenResource, resourceType);
//Add the REDIRECT tag to that same deprecated resource.
MdmResourceUtil.setGoldenResourceRedirected(theFromGoldenResource);
//Save the deprecated resource.
myMdmResourceDaoSvc.upsertGoldenResource(theFromGoldenResource, resourceType);
log(theMdmTransactionContext, "Merged " + theFromGoldenResource.getIdElement().toVersionless() + " into " + theToGoldenResource.getIdElement().toVersionless());
return theToGoldenResource;
}
private void addMergeLink(Long theGoldenResourcePidAkaActive, Long theTargetResourcePidAkaDeactivated, String theResourceType) {
MdmLink mdmLink = myMdmLinkDaoSvc
.getOrCreateMdmLinkByGoldenResourcePidAndSourceResourcePid(theGoldenResourcePidAkaActive, theTargetResourcePidAkaDeactivated);
mdmLink
.setMdmSourceType(theResourceType)
.setMatchResult(MdmMatchResultEnum.REDIRECT)
.setLinkSource(MdmLinkSourceEnum.MANUAL);
myMdmLinkDaoSvc.save(mdmLink);
}
/**
* Helper method which performs merger of links between resources, and cleans up dangling links afterwards.
*
* For each incomingLink, either ignore it, move it, or replace the original one
* 1. If the link already exists on the TO resource, and it is an automatic link, ignore the link, and subsequently delete it.
* 2. If the link does not exist on the TO resource, redirect the link from the FROM resource to the TO resource
* 3. If an incoming link is MANUAL, and theres a matching link on the FROM resource which is AUTOMATIC, the manual link supercedes the automatic one.
* 4. Manual link collisions cause invalid request exception.
*
* @param theFromResource
* @param theToResource
* @param theToResourcePid
* @param theMdmTransactionContext
*/
private void mergeGoldenResourceLinks(IAnyResource theFromResource, IAnyResource theToResource, Long theToResourcePid, MdmTransactionContext theMdmTransactionContext) {
List<MdmLink> fromLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theFromResource); // fromLinks - links going to theFromResource
List<MdmLink> toLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theToResource); // toLinks - links going to theToResource
List<MdmLink> toDelete = new ArrayList<>();
for (MdmLink fromLink : fromLinks) {
Optional<MdmLink> optionalToLink = findFirstLinkWithMatchingSource(toLinks, fromLink);
if (optionalToLink.isPresent()) {
// The original links already contain this target, so move it over to the toResource
MdmLink toLink = optionalToLink.get();
if (fromLink.isManual()) {
switch (toLink.getLinkSource()) {
case AUTO:
//3
log(theMdmTransactionContext, String.format("MANUAL overrides AUT0. Deleting link %s", toLink.toString()));
myMdmLinkDaoSvc.deleteLink(toLink);
break;
case MANUAL:
if (fromLink.getMatchResult() != toLink.getMatchResult()) {
throw new InvalidRequestException("A MANUAL " + fromLink.getMatchResult() + " link may not be merged into a MANUAL " + toLink.getMatchResult() + " link for the same target");
}
}
} else {
//1
toDelete.add(fromLink);
continue;
}
}
//2 The original TO links didn't contain this target, so move it over to the toGoldenResource
fromLink.setGoldenResourcePid(theToResourcePid);
ourLog.trace("Saving link {}", fromLink);
myMdmLinkDaoSvc.save(fromLink);
}
//1 Delete dangling links
toDelete.forEach(link -> myMdmLinkDaoSvc.deleteLink(link));
}
private Optional<MdmLink> findFirstLinkWithMatchingSource(List<MdmLink> theMdmLinks, MdmLink theLinkWithSourceToMatch) {
return theMdmLinks.stream()
.filter(mdmLink -> mdmLink.getSourcePid().equals(theLinkWithSourceToMatch.getSourcePid()))
.findFirst();
}
private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) {
theMdmTransactionContext.addTransactionLogMessage(theMessage);
ourLog.debug(theMessage);
}
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.empi.svc;
package ca.uhn.fhir.jpa.mdm.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -21,59 +21,59 @@ package ca.uhn.fhir.jpa.empi.svc;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.IEmpiChannelSubmitterSvc;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.jpa.subscription.channel.api.ChannelProducerSettings;
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelFactory;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
import ca.uhn.fhir.mdm.api.IMdmChannelSubmitterSvc;
import ca.uhn.fhir.mdm.log.Logs;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.MessageChannel;
import static ca.uhn.fhir.empi.api.IEmpiSettings.EMPI_CHANNEL_NAME;
import static ca.uhn.fhir.mdm.api.IMdmSettings.EMPI_CHANNEL_NAME;
/**
* This class is responsible for manual submissions of {@link IAnyResource} resources onto the Empi Channel.
* This class is responsible for manual submissions of {@link IAnyResource} resources onto the MDM Channel.
*/
public class EmpiChannelSubmitterSvcImpl implements IEmpiChannelSubmitterSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
public class MdmChannelSubmitterSvcImpl implements IMdmChannelSubmitterSvc {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
private MessageChannel myEmpiChannelProducer;
private MessageChannel myMdmChannelProducer;
private FhirContext myFhirContext;
private IChannelFactory myChannelFactory;
@Override
public void submitResourceToEmpiChannel(IBaseResource theResource) {
public void submitResourceToMdmChannel(IBaseResource theResource) {
ResourceModifiedJsonMessage resourceModifiedJsonMessage = new ResourceModifiedJsonMessage();
ResourceModifiedMessage resourceModifiedMessage = new ResourceModifiedMessage(myFhirContext, theResource, ResourceModifiedMessage.OperationTypeEnum.MANUALLY_TRIGGERED);
resourceModifiedMessage.setOperationType(ResourceModifiedMessage.OperationTypeEnum.MANUALLY_TRIGGERED);
resourceModifiedJsonMessage.setPayload(resourceModifiedMessage);
boolean success = getEmpiChannelProducer().send(resourceModifiedJsonMessage);
boolean success = getMdmChannelProducer().send(resourceModifiedJsonMessage);
if (!success) {
ourLog.error("Failed to submit {} to EMPI Channel.", resourceModifiedMessage.getPayloadId());
ourLog.error("Failed to submit {} to MDM Channel.", resourceModifiedMessage.getPayloadId());
}
}
@Autowired
public EmpiChannelSubmitterSvcImpl(FhirContext theFhirContext, IChannelFactory theIChannelFactory) {
public MdmChannelSubmitterSvcImpl(FhirContext theFhirContext, IChannelFactory theIChannelFactory) {
myFhirContext = theFhirContext;
myChannelFactory = theIChannelFactory;
}
private void init() {
ChannelProducerSettings channelSettings = new ChannelProducerSettings();
myEmpiChannelProducer= myChannelFactory.getOrCreateProducer(EMPI_CHANNEL_NAME, ResourceModifiedJsonMessage.class, channelSettings);
myMdmChannelProducer = myChannelFactory.getOrCreateProducer(EMPI_CHANNEL_NAME, ResourceModifiedJsonMessage.class, channelSettings);
}
private MessageChannel getEmpiChannelProducer() {
if (myEmpiChannelProducer == null) {
private MessageChannel getMdmChannelProducer() {
if (myMdmChannelProducer == null) {
init();
}
return myEmpiChannelProducer;
return myMdmChannelProducer;
}
}

View File

@ -0,0 +1,84 @@
package ca.uhn.fhir.jpa.mdm.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.mdm.api.IMdmExpungeSvc;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
/**
* This class is responsible for clearing out existing MDM links, as well as deleting all Golden Resources related to those MDM Links.
*/
public class MdmClearSvcImpl implements IMdmExpungeSvc {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
final MdmLinkDaoSvc myMdmLinkDaoSvc;
final MdmGoldenResourceDeletingSvc myMdmGoldenResourceDeletingSvcImpl;
final IMdmSettings myMdmSettings;
@Autowired
public MdmClearSvcImpl(MdmLinkDaoSvc theMdmLinkDaoSvc, MdmGoldenResourceDeletingSvc theMdmGoldenResourceDeletingSvcImpl, IMdmSettings theIMdmSettings) {
myMdmLinkDaoSvc = theMdmLinkDaoSvc;
myMdmGoldenResourceDeletingSvcImpl = theMdmGoldenResourceDeletingSvcImpl;
myMdmSettings = theIMdmSettings;
}
@Override
public long expungeAllMdmLinksOfSourceType(String theSourceResourceType, ServletRequestDetails theRequestDetails) {
throwExceptionIfInvalidSourceResourceType(theSourceResourceType);
ourLog.info("Clearing all MDM Links for resource type {}...", theSourceResourceType);
List<Long> goldenResourcePids = myMdmLinkDaoSvc.deleteAllMdmLinksOfTypeAndReturnGoldenResourcePids(theSourceResourceType);
DeleteMethodOutcome deleteOutcome = myMdmGoldenResourceDeletingSvcImpl.expungeGoldenResourcePids(goldenResourcePids, theSourceResourceType, theRequestDetails);
ourLog.info("MDM clear operation complete. Removed {} MDM links and {} Golden Resources.", goldenResourcePids.size(), deleteOutcome.getExpungedResourcesCount());
return goldenResourcePids.size();
}
private void throwExceptionIfInvalidSourceResourceType(String theResourceType) {
if (!myMdmSettings.isSupportedMdmType(theResourceType)) {
throw new InvalidRequestException(ProviderConstants.MDM_CLEAR + " does not support resource type: " + theResourceType);
}
}
@Override
public long expungeAllMdmLinks(ServletRequestDetails theRequestDetails) {
ourLog.info("Clearing all MDM Links...");
long retVal = 0;
for(String mdmType : myMdmSettings.getMdmRules().getMdmTypes()) {
List<Long> goldenResourcePids = myMdmLinkDaoSvc.deleteAllMdmLinksAndReturnGoldenResourcePids();
DeleteMethodOutcome deleteOutcome = myMdmGoldenResourceDeletingSvcImpl.expungeGoldenResourcePids(goldenResourcePids, null, theRequestDetails);
ourLog.info("MDM clear operation on type {} complete. Removed {} MDM links and expunged {} Golden resources.", mdmType, goldenResourcePids.size(), deleteOutcome.getExpungedResourcesCount());
retVal += goldenResourcePids.size();
}
ourLog.info("MDM clear completed expunged with a total of {} golden resources cleared.", retVal);
return retVal;
}
}

View File

@ -0,0 +1,100 @@
package ca.uhn.fhir.jpa.mdm.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.mdm.api.MdmLinkJson;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.api.IMdmControllerSvc;
import ca.uhn.fhir.mdm.api.IMdmLinkQuerySvc;
import ca.uhn.fhir.mdm.api.IMdmLinkUpdaterSvc;
import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.mdm.provider.MdmControllerHelper;
import ca.uhn.fhir.mdm.provider.MdmControllerUtil;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.stream.Stream;
/**
* This class acts as a layer between MdmProviders and MDM services to support a REST API that's not a FHIR Operation API.
*/
@Service
public class MdmControllerSvcImpl implements IMdmControllerSvc {
@Autowired
MdmControllerHelper myMdmControllerHelper;
@Autowired
IGoldenResourceMergerSvc myGoldenResourceMergerSvc;
@Autowired
IMdmLinkQuerySvc myMdmLinkQuerySvc;
@Autowired
IMdmLinkUpdaterSvc myIMdmLinkUpdaterSvc;
@Override
public IAnyResource mergeGoldenResources(String theFromGoldenResourceId, String theToGoldenResourceId, MdmTransactionContext theMdmTransactionContext) {
IAnyResource fromGoldenResource = myMdmControllerHelper.getLatestGoldenResourceFromIdOrThrowException(ProviderConstants.MDM_MERGE_GR_FROM_GOLDEN_RESOURCE_ID, theFromGoldenResourceId);
IAnyResource toGoldenResource = myMdmControllerHelper.getLatestGoldenResourceFromIdOrThrowException(ProviderConstants.MDM_MERGE_GR_TO_GOLDEN_RESOURCE_ID, theToGoldenResourceId);
myMdmControllerHelper.validateMergeResources(fromGoldenResource, toGoldenResource);
myMdmControllerHelper.validateSameVersion(fromGoldenResource, theFromGoldenResourceId);
myMdmControllerHelper.validateSameVersion(toGoldenResource, theToGoldenResourceId);
return myGoldenResourceMergerSvc.mergeGoldenResources(fromGoldenResource, toGoldenResource, theMdmTransactionContext);
}
@Override
public Stream<MdmLinkJson> queryLinks(@Nullable String theGoldenResourceId, @Nullable String theSourceResourceId, @Nullable String theMatchResult, @Nullable String theLinkSource, MdmTransactionContext theMdmTransactionContext) {
IIdType goldenResourceId = MdmControllerUtil.extractGoldenResourceIdDtOrNull(ProviderConstants.MDM_QUERY_LINKS_GOLDEN_RESOURCE_ID, theGoldenResourceId);
IIdType sourceId = MdmControllerUtil.extractSourceIdDtOrNull(ProviderConstants.MDM_QUERY_LINKS_RESOURCE_ID, theSourceResourceId);
MdmMatchResultEnum matchResult = MdmControllerUtil.extractMatchResultOrNull(theMatchResult);
MdmLinkSourceEnum linkSource = MdmControllerUtil.extractLinkSourceOrNull(theLinkSource);
return myMdmLinkQuerySvc.queryLinks(goldenResourceId, sourceId, matchResult, linkSource, theMdmTransactionContext);
}
@Override
public Stream<MdmLinkJson> getDuplicateGoldenResources(MdmTransactionContext theMdmTransactionContext) {
return myMdmLinkQuerySvc.getDuplicateGoldenResources(theMdmTransactionContext);
}
@Override
public IAnyResource updateLink(String theGoldenResourceId, String theSourceResourceId, String theMatchResult, MdmTransactionContext theMdmTransactionContext) {
MdmMatchResultEnum matchResult = MdmControllerUtil.extractMatchResultOrNull(theMatchResult);
IAnyResource goldenResource = myMdmControllerHelper.getLatestGoldenResourceFromIdOrThrowException(ProviderConstants.MDM_UPDATE_LINK_GOLDEN_RESOURCE_ID, theGoldenResourceId);
IAnyResource source = myMdmControllerHelper.getLatestSourceFromIdOrThrowException(ProviderConstants.MDM_UPDATE_LINK_RESOURCE_ID, theSourceResourceId);
myMdmControllerHelper.validateSameVersion(goldenResource, theGoldenResourceId);
myMdmControllerHelper.validateSameVersion(source, theSourceResourceId);
return myIMdmLinkUpdaterSvc.updateLink(goldenResource, source, matchResult, theMdmTransactionContext);
}
@Override
public void notDuplicateGoldenResource(String theGoldenResourceId, String theTargetGoldenResourceId, MdmTransactionContext theMdmTransactionContext) {
IAnyResource goldenResource = myMdmControllerHelper.getLatestGoldenResourceFromIdOrThrowException(ProviderConstants.MDM_UPDATE_LINK_GOLDEN_RESOURCE_ID, theGoldenResourceId);
IAnyResource target = myMdmControllerHelper.getLatestGoldenResourceFromIdOrThrowException(ProviderConstants.MDM_UPDATE_LINK_RESOURCE_ID, theTargetGoldenResourceId);
myIMdmLinkUpdaterSvc.notDuplicateGoldenResource(goldenResource, target, theMdmTransactionContext);
}
}

View File

@ -0,0 +1,182 @@
package ca.uhn.fhir.jpa.mdm.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchOutcome;
import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.model.CanonicalEID;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.mdm.util.EIDHelper;
import ca.uhn.fhir.mdm.util.GoldenResourceHelper;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MatchedGoldenResourceCandidate;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class MdmEidUpdateService {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
@Autowired
private MdmResourceDaoSvc myMdmResourceDaoSvc;
@Autowired
private IMdmLinkSvc myMdmLinkSvc;
@Autowired
private MdmGoldenResourceFindingSvc myMdmGoldenResourceFindingSvc;
@Autowired
private GoldenResourceHelper myGoldenResourceHelper;
@Autowired
private EIDHelper myEIDHelper;
@Autowired
private MdmLinkDaoSvc myMdmLinkDaoSvc;
@Autowired
private IMdmSettings myMdmSettings;
void handleMdmUpdate(IAnyResource theResource, MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext) {
MdmUpdateContext updateContext = new MdmUpdateContext(theMatchedGoldenResourceCandidate, theResource);
if (updateContext.isRemainsMatchedToSameGoldenResource()) {
// Copy over any new external EIDs which don't already exist.
// TODO NG - Eventually this call will use terser to clone data in, once the surviorship rules for copying data will be confirmed
// myPersonHelper.updatePersonFromUpdatedEmpiTarget(updateContext.getMatchedPerson(), theResource, theEmpiTransactionContext);
if (!updateContext.isIncomingResourceHasAnEid() || updateContext.isHasEidsInCommon()) {
//update to patient that uses internal EIDs only.
myMdmLinkSvc.updateLink(updateContext.getMatchedGoldenResource(), theResource, theMatchedGoldenResourceCandidate.getMatchResult(), MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
} else if (!updateContext.isHasEidsInCommon()) {
handleNoEidsInCommon(theResource, theMatchedGoldenResourceCandidate, theMdmTransactionContext, updateContext);
}
} else {
//This is a new linking scenario. we have to break the existing link and link to the new Golden Resource. For now, we create duplicate.
//updated patient has an EID that matches to a new candidate. Link them, and set the Golden Resources possible duplicates
linkToNewGoldenResourceAndFlagAsDuplicate(theResource, updateContext.getExistingGoldenResource(), updateContext.getMatchedGoldenResource(), theMdmTransactionContext);
}
}
private void handleNoEidsInCommon(IAnyResource theResource, MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext, MdmUpdateContext theUpdateContext) {
// the user is simply updating their EID. We propagate this change to the GoldenResource.
//overwrite. No EIDS in common, but still same GoldenResource.
if (myMdmSettings.isPreventMultipleEids()) {
if (myMdmLinkDaoSvc.findMdmMatchLinksByGoldenResource(theUpdateContext.getMatchedGoldenResource()).size() <= 1) { // If there is only 0/1 link on the GoldenResource, we can safely overwrite the EID.
handleExternalEidOverwrite(theUpdateContext.getMatchedGoldenResource(), theResource, theMdmTransactionContext);
} else { // If the GoldenResource has multiple targets tied to it, we can't just overwrite the EID, so we split the GoldenResource.
createNewGoldenResourceAndFlagAsDuplicate(theResource, theMdmTransactionContext, theUpdateContext.getExistingGoldenResource());
}
} else {
myGoldenResourceHelper.handleExternalEidAddition(theUpdateContext.getMatchedGoldenResource(), theResource, theMdmTransactionContext);
}
myMdmLinkSvc.updateLink(theUpdateContext.getMatchedGoldenResource(), theResource, theMatchedGoldenResourceCandidate.getMatchResult(), MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
}
private void handleExternalEidOverwrite(IAnyResource theGoldenResource, IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) {
List<CanonicalEID> eidFromResource = myEIDHelper.getExternalEid(theResource);
if (!eidFromResource.isEmpty()) {
myGoldenResourceHelper.overwriteExternalEids(theGoldenResource, eidFromResource);
}
}
private boolean candidateIsSameAsMdmLinkGoldenResource(MdmLink theExistingMatchLink, MatchedGoldenResourceCandidate theGoldenResourceCandidate) {
return theExistingMatchLink.getGoldenResourcePid().equals(theGoldenResourceCandidate.getCandidateGoldenResourcePid().getIdAsLong());
}
private void createNewGoldenResourceAndFlagAsDuplicate(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext, IAnyResource theOldGoldenResource) {
log(theMdmTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs.");
IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theResource);
myMdmLinkSvc.updateLink(newGoldenResource, theResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
myMdmLinkSvc.updateLink(newGoldenResource, theOldGoldenResource, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
}
private void linkToNewGoldenResourceAndFlagAsDuplicate(IAnyResource theResource, IAnyResource theOldGoldenResource, IAnyResource theNewGoldenResource, MdmTransactionContext theMdmTransactionContext) {
log(theMdmTransactionContext, "Changing a match link!");
myMdmLinkSvc.deleteLink(theOldGoldenResource, theResource, theMdmTransactionContext);
myMdmLinkSvc.updateLink(theNewGoldenResource, theResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
log(theMdmTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs.");
myMdmLinkSvc.updateLink(theNewGoldenResource, theOldGoldenResource, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, theMdmTransactionContext);
}
private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) {
theMdmTransactionContext.addTransactionLogMessage(theMessage);
ourLog.debug(theMessage);
}
/**
* Data class to hold context surrounding an update operation for an MDM target.
*/
class MdmUpdateContext {
private final boolean myHasEidsInCommon;
private final boolean myIncomingResourceHasAnEid;
private IAnyResource myExistingGoldenResource;
private boolean myRemainsMatchedToSameGoldenResource;
private final IAnyResource myMatchedGoldenResource;
public IAnyResource getMatchedGoldenResource() {
return myMatchedGoldenResource;
}
MdmUpdateContext(MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, IAnyResource theResource) {
final String resourceType = theResource.getIdElement().getResourceType();
myMatchedGoldenResource = myMdmGoldenResourceFindingSvc.getGoldenResourceFromMatchedGoldenResourceCandidate(theMatchedGoldenResourceCandidate, resourceType);
myHasEidsInCommon = myEIDHelper.hasEidOverlap(myMatchedGoldenResource, theResource);
myIncomingResourceHasAnEid = !myEIDHelper.getExternalEid(theResource).isEmpty();
Optional<MdmLink> theExistingMatchLink = myMdmLinkDaoSvc.getMatchedLinkForSource(theResource);
myExistingGoldenResource = null;
if (theExistingMatchLink.isPresent()) {
MdmLink mdmLink = theExistingMatchLink.get();
Long existingGoldenResourcePid = mdmLink.getGoldenResourcePid();
myExistingGoldenResource = myMdmResourceDaoSvc.readGoldenResourceByPid(new ResourcePersistentId(existingGoldenResourcePid), resourceType);
myRemainsMatchedToSameGoldenResource = candidateIsSameAsMdmLinkGoldenResource(mdmLink, theMatchedGoldenResourceCandidate);
} else {
myRemainsMatchedToSameGoldenResource = false;
}
}
public boolean isHasEidsInCommon() {
return myHasEidsInCommon;
}
public boolean isIncomingResourceHasAnEid() {
return myIncomingResourceHasAnEid;
}
public IAnyResource getExistingGoldenResource() {
return myExistingGoldenResource;
}
public boolean isRemainsMatchedToSameGoldenResource() {
return myRemainsMatchedToSameGoldenResource;
}
}
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.empi.svc;
package ca.uhn.fhir.jpa.mdm.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.empi.svc;
* #L%
*/
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.dao.expunge.DeleteExpungeService;
@ -35,8 +35,9 @@ import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class EmpiPersonDeletingSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
public class MdmGoldenResourceDeletingSvc {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
/**
* This is here for the case of possible infinite loops. Technically batch conflict deletion should handle this, but this is an escape hatch.
@ -50,7 +51,7 @@ public class EmpiPersonDeletingSvc {
@Autowired
DeleteExpungeService myDeleteExpungeService;
public DeleteMethodOutcome expungePersonPids(List<Long> thePersonPids, ServletRequestDetails theRequestDetails) {
return myDeleteExpungeService.expungeByResourcePids(ProviderConstants.EMPI_CLEAR, "Person", new SliceImpl<>(thePersonPids), theRequestDetails);
public DeleteMethodOutcome expungeGoldenResourcePids(List<Long> theGoldenResourcePids, String theResourceType, ServletRequestDetails theRequestDetails) {
return myDeleteExpungeService.expungeByResourcePids(ProviderConstants.MDM_CLEAR, theResourceType, new SliceImpl<>(theGoldenResourcePids), theRequestDetails);
}
}

View File

@ -0,0 +1,96 @@
package ca.uhn.fhir.jpa.mdm.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.mdm.api.MdmLinkJson;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.api.IMdmLinkQuerySvc;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.MdmLink;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import java.util.stream.Stream;
public class MdmLinkQuerySvcImpl implements IMdmLinkQuerySvc {
private static final Logger ourLog = LoggerFactory.getLogger(MdmLinkQuerySvcImpl.class);
@Autowired
IdHelperService myIdHelperService;
@Autowired
MdmLinkDaoSvc myMdmLinkDaoSvc;
@Override
public Stream<MdmLinkJson> queryLinks(IIdType theGoldenResourceId, IIdType theSourceResourceId, MdmMatchResultEnum theMatchResult, MdmLinkSourceEnum theLinkSource, MdmTransactionContext theMdmContext) {
Example<MdmLink> exampleLink = exampleLinkFromParameters(theGoldenResourceId, theSourceResourceId, theMatchResult, theLinkSource);
return myMdmLinkDaoSvc.findMdmLinkByExample(exampleLink).stream()
.filter(mdmLink -> mdmLink.getMatchResult() != MdmMatchResultEnum.POSSIBLE_DUPLICATE)
.map(this::toJson);
}
@Override
public Stream<MdmLinkJson> getDuplicateGoldenResources(MdmTransactionContext theMdmContext) {
Example<MdmLink> exampleLink = exampleLinkFromParameters(null, null, MdmMatchResultEnum.POSSIBLE_DUPLICATE, null);
return myMdmLinkDaoSvc.findMdmLinkByExample(exampleLink).stream().map(this::toJson);
}
private MdmLinkJson toJson(MdmLink theLink) {
MdmLinkJson retval = new MdmLinkJson();
String sourceId = myIdHelperService.resourceIdFromPidOrThrowException(theLink.getSourcePid()).toVersionless().getValue();
retval.setSourceId(sourceId);
String goldenResourceId = myIdHelperService.resourceIdFromPidOrThrowException(theLink.getGoldenResourcePid()).toVersionless().getValue();
retval.setGoldenResourceId(goldenResourceId);
retval.setCreated(theLink.getCreated());
retval.setEidMatch(theLink.getEidMatch());
retval.setLinkSource(theLink.getLinkSource());
retval.setMatchResult(theLink.getMatchResult());
retval.setLinkCreatedNewResource(theLink.getHadToCreateNewGoldenResource());
retval.setScore(theLink.getScore());
retval.setUpdated(theLink.getUpdated());
retval.setVector(theLink.getVector());
retval.setVersion(theLink.getVersion());
return retval;
}
private Example<MdmLink> exampleLinkFromParameters(IIdType theGoldenResourceId, IIdType theSourceId, MdmMatchResultEnum theMatchResult, MdmLinkSourceEnum theLinkSource) {
MdmLink mdmLink = myMdmLinkDaoSvc.newMdmLink();
if (theGoldenResourceId != null) {
mdmLink.setGoldenResourcePid(myIdHelperService.getPidOrThrowException(theGoldenResourceId));
}
if (theSourceId != null) {
mdmLink.setSourcePid(myIdHelperService.getPidOrThrowException(theSourceId));
}
if (theMatchResult != null) {
mdmLink.setMatchResult(theMatchResult);
}
if (theLinkSource != null) {
mdmLink.setLinkSource(theLinkSource);
}
return Example.of(mdmLink);
}
}

View File

@ -0,0 +1,140 @@
package ca.uhn.fhir.jpa.mdm.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
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.log.Logs;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
import java.util.Optional;
/**
* This class is in charge of managing MdmLinks between Golden Resources and source resources
*/
@Service
public class MdmLinkSvcImpl implements IMdmLinkSvc {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
@Autowired
private MdmResourceDaoSvc myMdmResourceDaoSvc;
@Autowired
private MdmLinkDaoSvc myMdmLinkDaoSvc;
@Autowired
private IdHelperService myIdHelperService;
@Override
@Transactional
public void updateLink(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmMatchOutcome theMatchOutcome, MdmLinkSourceEnum theLinkSource, MdmTransactionContext theMdmTransactionContext) {
if (theMatchOutcome.isPossibleDuplicate() && goldenResourceLinkedAsNoMatch(theGoldenResource, theSourceResource)) {
log(theMdmTransactionContext, theGoldenResource.getIdElement().toUnqualifiedVersionless() +
" is linked as NO_MATCH with " +
theSourceResource.getIdElement().toUnqualifiedVersionless() +
" not linking as POSSIBLE_DUPLICATE.");
return;
}
MdmMatchResultEnum matchResultEnum = theMatchOutcome.getMatchResultEnum();
validateRequestIsLegal(theGoldenResource, theSourceResource, matchResultEnum, theLinkSource);
myMdmResourceDaoSvc.upsertGoldenResource(theGoldenResource, theMdmTransactionContext.getResourceType());
createOrUpdateLinkEntity(theGoldenResource, theSourceResource, theMatchOutcome, theLinkSource, theMdmTransactionContext);
}
private boolean goldenResourceLinkedAsNoMatch(IAnyResource theGoldenResource, IAnyResource theSourceResource) {
Long goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource);
Long sourceId = myIdHelperService.getPidOrThrowException(theSourceResource);
// TODO perf collapse into one query
return myMdmLinkDaoSvc.getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(goldenResourceId, sourceId, MdmMatchResultEnum.NO_MATCH).isPresent() ||
myMdmLinkDaoSvc.getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(sourceId, goldenResourceId, MdmMatchResultEnum.NO_MATCH).isPresent();
}
@Override
public void deleteLink(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmTransactionContext theMdmTransactionContext) {
Optional<MdmLink> optionalMdmLink = getMdmLinkForGoldenResourceSourceResourcePair(theGoldenResource, theSourceResource);
if (optionalMdmLink.isPresent()) {
MdmLink mdmLink = optionalMdmLink.get();
log(theMdmTransactionContext, "Deleting MdmLink [" + theGoldenResource.getIdElement().toVersionless() + " -> " + theSourceResource.getIdElement().toVersionless() + "] with result: " + mdmLink.getMatchResult());
myMdmLinkDaoSvc.deleteLink(mdmLink);
}
}
/**
* Helper function which runs various business rules about what types of requests are allowed.
*/
private void validateRequestIsLegal(IAnyResource theGoldenResource, IAnyResource theResource, MdmMatchResultEnum theMatchResult, MdmLinkSourceEnum theLinkSource) {
Optional<MdmLink> oExistingLink = getMdmLinkForGoldenResourceSourceResourcePair(theGoldenResource, theResource);
if (oExistingLink.isPresent() && systemIsAttemptingToModifyManualLink(theLinkSource, oExistingLink.get())) {
throw new InternalErrorException("MDM system is not allowed to modify links on manually created links");
}
if (systemIsAttemptingToAddNoMatch(theLinkSource, theMatchResult)) {
throw new InternalErrorException("MDM system is not allowed to automatically NO_MATCH a resource");
}
}
/**
* Helper function which detects when the MDM system is attempting to add a NO_MATCH link, which is not allowed.
*/
private boolean systemIsAttemptingToAddNoMatch(MdmLinkSourceEnum theLinkSource, MdmMatchResultEnum theMatchResult) {
return theLinkSource == MdmLinkSourceEnum.AUTO && theMatchResult == MdmMatchResultEnum.NO_MATCH;
}
/**
* Helper function to let us catch when System MDM rules are attempting to override a manually defined link.
*/
private boolean systemIsAttemptingToModifyManualLink(MdmLinkSourceEnum theIncomingSource, MdmLink theExistingSource) {
return theIncomingSource == MdmLinkSourceEnum.AUTO && theExistingSource.isManual();
}
private Optional<MdmLink> getMdmLinkForGoldenResourceSourceResourcePair(IAnyResource theGoldenResource, IAnyResource theCandidate) {
if (theGoldenResource.getIdElement().getIdPart() == null || theCandidate.getIdElement().getIdPart() == null) {
return Optional.empty();
} else {
return myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(
myIdHelperService.getPidOrNull(theGoldenResource),
myIdHelperService.getPidOrNull(theCandidate)
);
}
}
private void createOrUpdateLinkEntity(IBaseResource theGoldenResource, IBaseResource theSourceResource, MdmMatchOutcome theMatchOutcome, MdmLinkSourceEnum theLinkSource, MdmTransactionContext theMdmTransactionContext) {
myMdmLinkDaoSvc.createOrUpdateLinkEntity(theGoldenResource, theSourceResource, theMatchOutcome, theLinkSource, theMdmTransactionContext);
}
private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) {
theMdmTransactionContext.addTransactionLogMessage(theMessage);
ourLog.debug(theMessage);
}
}

View File

@ -0,0 +1,165 @@
package ca.uhn.fhir.jpa.mdm.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.api.IMdmLinkSvc;
import ca.uhn.fhir.mdm.api.IMdmLinkUpdaterSvc;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.mdm.util.MdmResourceUtil;
import ca.uhn.fhir.mdm.util.MessageHelper;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.util.Objects;
import java.util.Optional;
public class MdmLinkUpdaterSvcImpl implements IMdmLinkUpdaterSvc {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
@Autowired
FhirContext myFhirContext;
@Autowired
IdHelperService myIdHelperService;
@Autowired
MdmLinkDaoSvc myMdmLinkDaoSvc;
@Autowired
IMdmLinkSvc myMdmLinkSvc;
@Autowired
MdmResourceDaoSvc myMdmResourceDaoSvc;
@Autowired
MdmMatchLinkSvc myMdmMatchLinkSvc;
@Autowired
IMdmSettings myMdmSettings;
@Autowired
MessageHelper myMessageHelper;
@Transactional
@Override
public IAnyResource updateLink(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmMatchResultEnum theMatchResult, MdmTransactionContext theMdmContext) {
String sourceType = myFhirContext.getResourceType(theSourceResource);
validateUpdateLinkRequest(theGoldenResource, theSourceResource, theMatchResult, sourceType);
Long goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource);
Long targetId = myIdHelperService.getPidOrThrowException(theSourceResource);
Optional<MdmLink> optionalMdmLink = myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourceId, targetId);
if (!optionalMdmLink.isPresent()) {
throw new InvalidRequestException(myMessageHelper.getMessageForNoLink(theGoldenResource, theSourceResource));
}
MdmLink mdmLink = optionalMdmLink.get();
if (mdmLink.getMatchResult() == theMatchResult) {
ourLog.warn("MDM Link for " + theGoldenResource.getIdElement().toVersionless() + ", " + theSourceResource.getIdElement().toVersionless() + " already has value " + theMatchResult + ". Nothing to do.");
return theGoldenResource;
}
ourLog.info("Manually updating MDM Link for " + theGoldenResource.getIdElement().toVersionless() + ", " + theSourceResource.getIdElement().toVersionless() + " from " + mdmLink.getMatchResult() + " to " + theMatchResult + ".");
mdmLink.setMatchResult(theMatchResult);
mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL);
myMdmLinkDaoSvc.save(mdmLink);
myMdmResourceDaoSvc.upsertGoldenResource(theGoldenResource, theMdmContext.getResourceType());
if (theMatchResult == MdmMatchResultEnum.NO_MATCH) {
// Need to find a new Golden Resource to link this target to
myMdmMatchLinkSvc.updateMdmLinksForMdmSource(theSourceResource, theMdmContext);
}
return theGoldenResource;
}
private void validateUpdateLinkRequest(IAnyResource theGoldenRecord, IAnyResource theSourceResource, MdmMatchResultEnum theMatchResult, String theSourceType) {
String goldenRecordType = myFhirContext.getResourceType(theGoldenRecord);
if (theMatchResult != MdmMatchResultEnum.NO_MATCH &&
theMatchResult != MdmMatchResultEnum.MATCH) {
throw new InvalidRequestException(myMessageHelper.getMessageForUnsupportedMatchResult());
}
if (!myMdmSettings.isSupportedMdmType(goldenRecordType)) {
throw new InvalidRequestException(myMessageHelper.getMessageForUnsupportedFirstArgumentTypeInUpdate(goldenRecordType));
}
if (!myMdmSettings.isSupportedMdmType(theSourceType)) {
throw new InvalidRequestException(myMessageHelper.getMessageForUnsupportedSecondArgumentTypeInUpdate(theSourceType));
}
if (!Objects.equals(goldenRecordType, theSourceType)) {
throw new InvalidRequestException(myMessageHelper.getMessageForArgumentTypeMismatchInUpdate(goldenRecordType, theSourceType));
}
if (!MdmResourceUtil.isMdmManaged(theGoldenRecord)) {
throw new InvalidRequestException(myMessageHelper.getMessageForUnmanagedResource());
}
if (!MdmResourceUtil.isMdmAllowed(theSourceResource)) {
throw new InvalidRequestException(myMessageHelper.getMessageForUnsupportedSourceResource());
}
}
@Transactional
@Override
public void notDuplicateGoldenResource(IAnyResource theGoldenResource, IAnyResource theTargetGoldenResource, MdmTransactionContext theMdmContext) {
validateNotDuplicateGoldenResourceRequest(theGoldenResource, theTargetGoldenResource);
Long goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource);
Long targetId = myIdHelperService.getPidOrThrowException(theTargetGoldenResource);
Optional<MdmLink> oMdmLink = myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourceId, targetId);
if (!oMdmLink.isPresent()) {
throw new InvalidRequestException("No link exists between " + theGoldenResource.getIdElement().toVersionless() + " and " + theTargetGoldenResource.getIdElement().toVersionless());
}
MdmLink mdmLink = oMdmLink.get();
if (!mdmLink.isPossibleDuplicate()) {
throw new InvalidRequestException(theGoldenResource.getIdElement().toVersionless() + " and " + theTargetGoldenResource.getIdElement().toVersionless() + " are not linked as POSSIBLE_DUPLICATE.");
}
mdmLink.setMatchResult(MdmMatchResultEnum.NO_MATCH);
mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL);
myMdmLinkDaoSvc.save(mdmLink);
}
/**
* Ensure that the two resources are of the same type and both are managed by HAPI-MDM
*/
private void validateNotDuplicateGoldenResourceRequest(IAnyResource theGoldenResource, IAnyResource theTarget) {
String goldenResourceType = myFhirContext.getResourceType(theGoldenResource);
String targetType = myFhirContext.getResourceType(theTarget);
if (!goldenResourceType.equalsIgnoreCase(targetType)) {
throw new InvalidRequestException("First argument to " + ProviderConstants.MDM_UPDATE_LINK + " must be the same resource type as the second argument. Was " + goldenResourceType + "/" + targetType);
}
if (!MdmResourceUtil.isMdmManaged(theGoldenResource) || !MdmResourceUtil.isMdmManaged(theTarget)) {
throw new InvalidRequestException("Only MDM Managed Golden Resources may be updated via this operation. The resource provided is not tagged as managed by HAPI-MDM");
}
}
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.empi.svc;
package ca.uhn.fhir.jpa.mdm.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* HAPI FHIR JPA Server - Master Data Management
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -20,11 +20,13 @@ package ca.uhn.fhir.jpa.empi.svc;
* #L%
*/
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.MatchedTarget;
import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiCandidateSearchSvc;
import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc;
import ca.uhn.fhir.mdm.api.MatchedTarget;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.rules.svc.MdmResourceMatcherSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmCandidateSearchSvc;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -34,21 +36,26 @@ import java.util.List;
import java.util.stream.Collectors;
@Service
public class EmpiMatchFinderSvcImpl implements IEmpiMatchFinderSvc {
public class MdmMatchFinderSvcImpl implements IMdmMatchFinderSvc {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
@Autowired
private EmpiCandidateSearchSvc myEmpiCandidateSearchSvc;
private MdmCandidateSearchSvc myMdmCandidateSearchSvc;
@Autowired
private EmpiResourceMatcherSvc myEmpiResourceMatcherSvc;
private MdmResourceMatcherSvc myMdmResourceMatcherSvc;
@Override
@Nonnull
public List<MatchedTarget> getMatchedTargets(String theResourceType, IAnyResource theResource) {
Collection<IAnyResource> targetCandidates = myEmpiCandidateSearchSvc.findCandidates(theResourceType, theResource);
Collection<IAnyResource> targetCandidates = myMdmCandidateSearchSvc.findCandidates(theResourceType, theResource);
return targetCandidates.stream()
.map(candidate -> new MatchedTarget(candidate, myEmpiResourceMatcherSvc.getMatchResult(theResource, candidate)))
List<MatchedTarget> matches = targetCandidates.stream()
.map(candidate -> new MatchedTarget(candidate, myMdmResourceMatcherSvc.getMatchResult(theResource, candidate)))
.collect(Collectors.toList());
ourLog.info("Found {} matched targets for {}", matches.size(), theResourceType);
return matches;
}
}

Some files were not shown because too many files have changed in this diff Show More