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>
@ -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>
|
||||
|
@ -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;
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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."
|
||||
|
@ -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>"
|
@ -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
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 80 KiB |
@ -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}}
|
||||
```
|
||||
|
||||
|
@ -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.
|
@ -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.
|
@ -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>
|
||||
|
@ -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)
|
@ -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.
|
@ -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>
|
||||
|
@ -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
|
||||
```
|
@ -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.
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
@ -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"));
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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(); }
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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]);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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<>();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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")));
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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 {
|
||||
}
|
@ -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)));
|
||||
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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 {
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|