Merge pull request #2223 from jamesagnew/revert-2177-gg_20201105-remove-person-references

Revert "Remove all references to Person from EMPI."
This commit is contained in:
Tadgh 2020-12-09 15:15:26 -05:00 committed by GitHub
commit a63565bd15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
266 changed files with 10097 additions and 10561 deletions

View File

@ -1703,21 +1703,21 @@ public enum Pointcut {
),
/**
* <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.
* <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.
* <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 MDM module during MDM processing.</li>
* <li>ca.uhn.fhir.rest.server.TransactionLogMessages - This parameter is for informational messages provided by the EMPI module during EMPI procesing. .</li>
* </ul>
* </p>
* <p>
* Hooks should return <code>void</code>.
* </p>
*/
MDM_AFTER_PERSISTED_RESOURCE_CHECKED(void.class, "ca.uhn.fhir.rest.server.messaging.ResourceOperationMessage", "ca.uhn.fhir.rest.server.TransactionLogMessages"),
EMPI_AFTER_PERSISTED_RESOURCE_CHECKED(void.class, "ca.uhn.fhir.rest.server.messaging.ResourceOperationMessage", "ca.uhn.fhir.rest.server.TransactionLogMessages"),
/**
* <b>Performance Tracing Hook:</b>

View File

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

View File

@ -1,9 +1,9 @@
---
type: add
issue: 2021
title: "Added [EMPI](https://hapifhir.io/hapi-fhir/docs/server_jpa_mdm/mdm.html) functionality, including phonetic
title: "Added [EMPI](https://hapifhir.io/hapi-fhir/docs/server_jpa_empi/empi.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_mdm/mdm_operations.html) are provided to
[EMPI Operations](https://hapifhir.io/hapi-fhir/docs/server_jpa_empi/empi_operations.html) are provided to
maintain EMPI links (e.g. resolving possible matches and possible duplicates). Also batch operations
are provided to identify links in existing patients and practitioners, and to 'wipe clean' all EMPI data and re-run
the batch as the empi matching rules are refined."

View File

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

View File

@ -51,12 +51,12 @@ page.server_jpa.diff=Diff Operation
page.server_jpa.lastn=LastN Operation
page.server_jpa.terminology=Terminology
section.server_jpa_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_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_partitioning.title=JPA Server: Partitioning and Multitenancy
page.server_jpa_partitioning.partitioning=Partitioning and Multitenancy

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 49 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
# MDM Operations
# EMPI Operations
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/MdmProviderR4.html).
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).
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.
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.
## Query links
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:
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:
<table class="table table-striped table-condensed">
<thead>
@ -19,19 +19,19 @@ Use the `$mdm-query-links` operation to view MDM links. The results returned are
</thead>
<tbody>
<tr>
<td>goldenResourceId</td>
<td>personId</td>
<td>String</td>
<td>0..1</td>
<td>
The id of the Golden Resource (e.g. Golden Patient Resource).
The id of the Person resource.
</td>
</tr>
<tr>
<td>resourceId</td>
<td>targetId</td>
<td>String</td>
<td>0..1</td>
<td>
The id of the source resource (e.g. Patient resource).
The id of the Patient or Practitioner resource.
</td>
</tr>
<tr>
@ -55,10 +55,10 @@ Use the `$mdm-query-links` operation to view MDM links. The results returned are
### Example
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:
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:
```url
http://example.com/$mdm-query-links
http://example.com/$empi-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": "goldenResourceId",
"valueString": "Patient/123"
"name": "personId",
"valueString": "Person/123"
}, {
"name": "sourceResourceId",
"name": "targetId",
"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": "hadToCreateNewResource",
"name": "newPerson",
"valueBoolean": false
}, {
"name": "score",
@ -106,20 +106,61 @@ This operation returns a `Parameters` resource that looks like the following:
}
```
## Query Duplicate Golden Resources
## Querying links via the Person resource
Use the `$mdm-duplicate-golden-resources` operation to request a list of duplicate Golden Resources.
This operation takes no parameters.
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
### Example
Use an HTTP GET to the following URL to invoke this operation:
```url
http://example.com/$mdm-duplicate-golden-resources
http://example.com/$empi-duplicate-persons
```
This operation returns `Parameters` similar to `$mdm-query-links`:
This operation returns `Parameters` similar to `$empi-query-links`:
```json
@ -128,11 +169,11 @@ This operation returns `Parameters` similar to `$mdm-query-links`:
"parameter": [ {
"name": "link",
"part": [ {
"name": "goldenResourceId",
"valueString": "Patient/123"
"name": "personId",
"valueString": "Person/123"
}, {
"name": "sourceResourceId",
"valueString": "Patient/456"
"name": "targetId",
"valueString": "Person/456"
}, {
"name": "matchResult",
"valueString": "POSSIBLE_DUPLICATE"
@ -144,10 +185,9 @@ This operation returns `Parameters` similar to `$mdm-query-links`:
}
```
## Unduplicate Golden Resources
## Unduplicate Persons
Use the `$mdm-not-duplicate` operation to mark duplicate Golden Resources as not duplicates.
This operation takes the following parameters:
Use the `$empi-not-duplicate` operation to mark duplicate persons as not duplicates. This operation takes the following parameters:
<table class="table table-striped table-condensed">
<thead>
@ -160,19 +200,19 @@ This operation takes the following parameters:
</thead>
<tbody>
<tr>
<td>goldenResourceId</td>
<td>personId</td>
<td>String</td>
<td>1..1</td>
<td>
The id of the Golden Resource.
The id of the Person resource.
</td>
</tr>
<tr>
<td>resourceId</td>
<td>targetId</td>
<td>String</td>
<td>1..1</td>
<td>
The id of the source resource that has a possible duplicate link to.
The id of the Person that personId has a possible duplicate link to.
</td>
</tr>
</tbody>
@ -183,7 +223,7 @@ This operation takes the following parameters:
Use an HTTP POST to the following URL to invoke this operation:
```url
http://example.com/$mdm-not-duplicate
http://example.com/$empi-not-duplicate
```
The following request body could be used:
@ -192,11 +232,11 @@ The following request body could be used:
{
"resourceType": "Parameters",
"parameter": [ {
"name": "goldenResourceId",
"valueString": "Patient/123"
"name": "personId",
"valueString": "Person/123"
}, {
"name": "resourceId",
"valueString": "Patient/456"
"name": "targetId",
"valueString": "Person/456"
} ]
}
```
@ -215,7 +255,7 @@ When the operation is successful, it returns the following `Parameters`:
## Update Link
Use the `$mdm-update-link` operation to change the `matchResult` update of an mdm link. This operation takes the following parameters:
Use the `$empi-update-link` operation to change the `matchResult` update of an empi link. This operation takes the following parameters:
<table class="table table-striped table-condensed">
<thead>
@ -228,19 +268,19 @@ Use the `$mdm-update-link` operation to change the `matchResult` update of an md
</thead>
<tbody>
<tr>
<td>goldenResourceId</td>
<td>personId</td>
<td>String</td>
<td>1..1</td>
<td>
The id of the Golden Resource.
The id of the Person resource.
</td>
</tr>
<tr>
<td>resourceId</td>
<td>targetId</td>
<td>String</td>
<td>1..1</td>
<td>
The id of the source resource.
The id of the Patient or Practitioner resource.
</td>
</tr>
<tr>
@ -254,26 +294,26 @@ Use the `$mdm-update-link` operation to change the `matchResult` update of an md
</tbody>
</table>
MDM links updated in this way will automatically have their `linkSource` set to `MANUAL`.
Empi 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/$mdm-update-link
http://example.com/$empi-update-link
```
Any supported MDM type can be used. The following request body shows how to update link on the Patient resource type:
The following request body could be used:
```json
{
"resourceType": "Parameters",
"parameter": [ {
"name": "goldenResourceId",
"valueString": "Patient/123"
"name": "personId",
"valueString": "Person/123"
}, {
"name": "resourceId",
"name": "targetId",
"valueString": "Patient/456"
}, {
"name": "matchResult",
@ -282,13 +322,13 @@ Any supported MDM type can be used. The following request body shows how to upda
}
```
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.
The operation returns the updated `Person` resource. Note that this is the only way to modify EMPI-managed `Person` resources.
## Merge Golden Resources
## Merge Persons
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.
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
After the merge is complete, `fromGoldenResourceId` will be deactivated by assigning a metadata tag `REDIRECTED`.
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.
This operation takes the following parameters:
@ -303,19 +343,19 @@ This operation takes the following parameters:
</thead>
<tbody>
<tr>
<td>fromGoldenResourceId</td>
<td>fromPersonId</td>
<td>String</td>
<td>1..1</td>
<td>
The id of the Golden Resource to merge data from.
The id of the Person resource to merge data from.
</td>
</tr>
<tr>
<td>toGoldenResourceId</td>
<td>toPersonId</td>
<td>String</td>
<td>1..1</td>
<td>
The id of the Golden Resource to merge data into.
The id of the Person to merge data into.
</td>
</tr>
</tbody>
@ -326,7 +366,7 @@ This operation takes the following parameters:
Use an HTTP POST to the following URL to invoke this operation:
```url
http://example.com/$mdm-merge-golden-resources
http://example.com/$empi-merge-persons
```
The following request body could be used:
@ -335,24 +375,22 @@ The following request body could be used:
{
"resourceType": "Parameters",
"parameter": [ {
"name": "fromGoldenResourceId",
"valueString": "Patient/123"
"name": "fromPersonId",
"valueString": "Person/123"
}, {
"name": "toGoldenResourceId",
"name": "toPersonId",
"valueString": "Patient/128"
} ]
}
```
This operation returns the merged Golden Resource (`toGoldenResourceId`).
This operation returns the merged Person resource.
# Querying The MDM
# Querying The EMPI
## Querying the Patient Resource
When EMPI is enabled, the [$match operation](http://hl7.org/fhir/patient-operation-match.html) will be enabled on the JPA Server.
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.
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.
For example, the following request may be submitted:
@ -376,7 +414,7 @@ Content-Type: application/fhir+json; charset=UTF-8
}
```
Sample response for the Patient match is included below:
This might result in a response such as the following:
```json
{
@ -404,51 +442,20 @@ Sample response for the Patient match is included below:
}
],
"birthDate": "2000-01-01"
},
"search": {
"extension": [{
"url": "http://hl7.org/fhir/StructureDefinition/match-grade",
"valueCode": "certain"
}],
"mode": "match",
"score": 0.9
}
}
]
}
```
## Querying the Other Supported MDM Resources via `/$mdm-match`
## Clearing EMPI Links
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.
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.
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.
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.
This operation takes a single optional Parameter.
@ -463,11 +470,11 @@ This operation takes a single optional Parameter.
</thead>
<tbody>
<tr>
<td>sourceType</td>
<td>resourceType</td>
<td>String</td>
<td>0..1</td>
<td>
The Source Resource type you would like to clear. If omitted, will operate over all links.
The target Resource type you would like to clear. Currently limited to Patient/Practitioner. If omitted, will operate over all links.
</td>
</tr>
</tbody>
@ -478,7 +485,7 @@ This operation takes a single optional Parameter.
Use an HTTP POST to the following URL to invoke this operation:
```url
http://example.com/$mdm-clear
http://example.com/$empi-clear
```
The following request body could be used:
@ -487,13 +494,13 @@ The following request body could be used:
{
"resourceType": "Parameters",
"parameter": [ {
"name": "sourceType",
"name": "resourceType",
"valueString": "Patient"
} ]
}
```
This operation returns the number of MDM links that were cleared. The following is a sample response:
This operation returns the number of EMPI links that were cleared. The following is a sample response:
```json
{
@ -505,11 +512,13 @@ This operation returns the number of MDM links that were cleared. The following
}
```
## Batch-creating MDM Links
## Batch-creating EMPI Links
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.
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.
After the operation is complete, all resources that matched the criteria will now have at least one MDM link attached to them.
After the operation is complete, all resources that matched the criteria will now have at least one empi link attached to them.
This operation takes a single optional criteria parameter unless it is called on a specific instance.
@ -540,9 +549,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/$mdm-submit
http://example.com/Patient/$mdm-submit
http://example.com/Practitioner/$mdm-submit
http://example.com/$empi-submit
http://example.com/Patient/$empi-submit
http://example.com/Practitioner/$empi-submit
```
The following request body could be used:
@ -556,7 +565,7 @@ The following request body could be used:
} ]
}
```
This operation returns the number of resources that were submitted for MDM processing. The following is a sample response:
This operation returns the number of resources that were submitted for EMPI processing. The following is a sample response:
```json
{
@ -568,9 +577,10 @@ This operation returns the number of resources that were submitted for MDM proce
}
```
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/$mdm-submit
http://example.com/Practitioner/456/$mdm-submit
http://example.com/Patient/123/$empi-submit
http://example.com/Practitioner/456/$empi-submit
```

View File

@ -1,15 +1,14 @@
# Rules
HAPI MDM rules are defined in a single json document.
HAPI EMPI rules are defined in a single json document.
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.
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.
Here is an example of a full HAPI MDM rules json document:
Here is an example of a full HAPI EMPI rules json document:
```json
{
"version": "1",
"mdmTypes" : ["Organization", "Patient", "Practitioner"],
"candidateSearchParams": [
{
"resourceType": "Patient",
@ -81,14 +80,6 @@ Here is an example of a full HAPI MDM rules json document:
"algorithm": "JARO_WINKLER",
"matchThreshold": 0.80
}
},
{
"name": "org-name",
"resourceType": "Organization",
"resourcePath": "name",
"matcher": {
"algorithm": "STRING"
}
}
],
"matchResultMap": {
@ -97,8 +88,7 @@ Here is an example of a full HAPI MDM 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",
"org-name": "MATCH"
"firstname-jaro,phone,birthday": "POSSIBLE_MATCH"
}
}
```
@ -106,28 +96,24 @@ Here is an example of a full HAPI MDM 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).
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.
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",
@ -154,7 +140,6 @@ For example, if the incoming patient looked like this:
"James"
]
}
]
}
```
@ -401,17 +386,17 @@ The following algorithms are currently supported:
### matchResultMap
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`).
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`).
```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 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.
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.

View File

@ -1,34 +0,0 @@
# MDM Getting Started
## 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.

View File

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

View File

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

View File

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

View File

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

View File

@ -148,7 +148,7 @@ import java.util.Date;
@ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Test.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.subscription.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.searchparam.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.mdm.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.empi.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.cache.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.starter.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.batch.*")

View File

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

View File

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

View File

@ -54,9 +54,8 @@ 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", "GOLDEN_RESOURCE_PID"));
retval.add(new ResourceForeignKey("MPI_LINK", "TARGET_PID"));
retval.add(new ResourceForeignKey("MPI_LINK", "PERSON_PID"));
retval.add(new ResourceForeignKey("MPI_LINK", "TARGET_PID"));
retval.add(new ResourceForeignKey("NPM_PACKAGE_VER", "BINARY_RES_ID"));
retval.add(new ResourceForeignKey("NPM_PACKAGE_VER_RES", "BINARY_RES_ID"));
retval.add(new ResourceForeignKey("TRM_CODESYSTEM", "RES_ID"));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.mdm.svc;
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -21,10 +21,9 @@ package ca.uhn.fhir.jpa.mdm.svc;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.rules.json.MdmResourceSearchParamJson;
import ca.uhn.fhir.mdm.util.MdmResourceUtil;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
@ -33,48 +32,43 @@ import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MdmResourceFilteringSvc {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
public class EmpiResourceFilteringSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private IMdmSettings myMdmSettings;
private IEmpiSettings myEmpiSettings;
@Autowired
MdmSearchParamSvc myMdmSearchParamSvc;
EmpiSearchParamSvc myEmpiSearchParamSvc;
@Autowired
FhirContext myFhirContext;
/**
* Given a resource from the MDM Channel, determine whether or not MDM processing should occur on it.
* Given a resource from the EMPI Channel, determine whether or not EMPI processing should occur on it.
*
* MDM processing should occur if for any {@link MdmResourceSearchParamJson ) Search Param, the resource contains a value.
* EMPI processing should occur if for any {@link EmpiResourceSearchParamJson) Search Param, the resource contains a value.
*
* If the resource has no attributes that appear in the candidate search params, processing should be skipped, as there is not
* sufficient information to perform meaningful MDM processing. (For example, how can MDM processing occur on a patient that has _no_ attributes?)
* sufficient information to perform meaningful EMPI processing. (For example, how can EMPI processing occur on a patient that has _no_ attributes?)
*
* @param theResource the resource that you wish to check against MDM rules.
* @param theResource the resource that you wish to check against EMPI rules.
*
* @return whether or not MDM processing should proceed
* @return whether or not EMPI processing should proceed
*/
public boolean shouldBeProcessed(IAnyResource theResource) {
if (MdmResourceUtil.isMdmManaged(theResource)) {
ourLog.debug("MDM Message handler is dropping [{}] as it is MDM-managed.", theResource);
return false;
}
String resourceType = myFhirContext.getResourceType(theResource);
List<MdmResourceSearchParamJson> candidateSearchParams = myMdmSettings.getMdmRules().getCandidateSearchParams();
List<EmpiResourceSearchParamJson> candidateSearchParams = myEmpiSettings.getEmpiRules().getCandidateSearchParams();
if (candidateSearchParams.isEmpty()) {
return true;
}
boolean containsValueForSomeSearchParam = candidateSearchParams.stream()
.filter(csp -> myMdmSearchParamSvc.searchParamTypeIsValidForResourceType(csp.getResourceType(), resourceType))
.filter(csp -> myEmpiSearchParamSvc.searchParamTypeIsValidForResourceType(csp.getResourceType(), resourceType))
.flatMap(csp -> csp.getSearchParams().stream())
.map(searchParam -> myMdmSearchParamSvc.getValueFromResourceForSearchParam(theResource, searchParam))
.map(searchParam -> myEmpiSearchParamSvc.getValueFromResourceForSearchParam(theResource, searchParam))
.anyMatch(valueList -> !valueList.isEmpty());
ourLog.trace("Is {} suitable for MDM processing? : {}", theResource.getId(), containsValueForSomeSearchParam);
ourLog.trace("Is {} suitable for EMPI processing? : {}", theResource.getId(), containsValueForSomeSearchParam);
return containsValueForSomeSearchParam;
}
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.mdm.svc;
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -41,7 +41,7 @@ import javax.annotation.Nullable;
import java.util.List;
@Service
public class MdmSearchParamSvc implements ISearchParamRetriever {
public class EmpiSearchParamSvc implements ISearchParamRetriever {
@Autowired
FhirContext myFhirContext;
@Autowired
@ -72,27 +72,27 @@ public class MdmSearchParamSvc implements ISearchParamRetriever {
}
/**
* Given a source type, and a criteria string of the shape name=x&birthDate=y, generate a {@link SearchParameterMap}
* Given a target type, and a criteria string of the shape name=x&birthDate=y, generate a {@link SearchParameterMap}
* that represents this query.
*
* @param theSourceType the resource type to execute the search on
* @param theTargetType the resource type to execute the search on
* @param theCriteria the string search criteria.
*
* @return the generated SearchParameterMap, or an empty one if there is no criteria.
*/
public SearchParameterMap getSearchParameterMapFromCriteria(String theSourceType, @Nullable String theCriteria) {
public SearchParameterMap getSearchParameterMapFromCriteria(String theTargetType, @Nullable String theCriteria) {
SearchParameterMap spMap;
if (StringUtils.isBlank(theCriteria)) {
spMap = new SearchParameterMap();
} else {
spMap = mapFromCriteria(theSourceType, theCriteria);
spMap = mapFromCriteria(theTargetType, theCriteria);
}
return spMap;
}
public ISearchBuilder generateSearchBuilderForType(String theSourceType) {
IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theSourceType);
return mySearchBuilderFactory.newSearchBuilder(resourceDao, theSourceType, resourceDao.getResourceType());
public ISearchBuilder generateSearchBuilderForType(String theTargetType) {
IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theTargetType);
return mySearchBuilderFactory.newSearchBuilder(resourceDao, theTargetType, resourceDao.getResourceType());
}
/**

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.mdm.svc;
package ca.uhn.fhir.jpa.empi.svc;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -20,10 +20,10 @@ package ca.uhn.fhir.jpa.mdm.svc;
* #L%
*/
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.log.Logs;
import ca.uhn.fhir.empi.api.IEmpiChannelSubmitterSvc;
import ca.uhn.fhir.empi.api.IEmpiSubmitSvc;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
@ -49,114 +49,103 @@ import java.util.Collections;
import java.util.List;
import java.util.UUID;
public class MdmSubmitSvcImpl implements IMdmSubmitSvc {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
public class EmpiSubmitSvcImpl implements IEmpiSubmitSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private MdmSearchParamSvc myMdmSearchParamSvc;
private EmpiSearchParamSvc myEmpiSearchParamSvc;
@Autowired
private IMdmChannelSubmitterSvc myMdmChannelSubmitterSvc;
@Autowired
private IMdmSettings myMdmSettings;
private IEmpiChannelSubmitterSvc myEmpiChannelSubmitterSvc;
private static final int BUFFER_SIZE = 100;
@Override
@Transactional
public long submitAllSourceTypesToMdm(@Nullable String theCriteria) {
long submittedCount = myMdmSettings.getMdmRules().getMdmTypes().stream()
.mapToLong(type -> submitSourceResourceTypeToMdm(type, theCriteria))
.sum();
public long submitAllTargetTypesToEmpi(@Nullable String theCriteria) {
long submittedCount = 0;
submittedCount += submitPatientTypeToEmpi(theCriteria);
submittedCount += submitPractitionerTypeToEmpi(theCriteria);
return submittedCount;
}
@Override
@Transactional
public long submitSourceResourceTypeToMdm(String theSourceResourceType, @Nullable String theCriteria) {
public long submitTargetTypeToEmpi(String theTargetType, @Nullable String theCriteria) {
if (theCriteria == null) {
ourLog.info("Submitting all resources of type {} to MDM", theSourceResourceType);
ourLog.info("Submitting all resources of type {} to EMPI", theTargetType);
} else {
ourLog.info("Submitting resources of type {} with criteria {} to MDM", theSourceResourceType, theCriteria);
ourLog.info("Submitting resources of type {} with criteria {} to EMPI", theTargetType, theCriteria);
}
validateSourceType(theSourceResourceType);
SearchParameterMap spMap = myMdmSearchParamSvc.getSearchParameterMapFromCriteria(theSourceResourceType, theCriteria);
resolveTargetTypeOrThrowException(theTargetType);
SearchParameterMap spMap = myEmpiSearchParamSvc.getSearchParameterMapFromCriteria(theTargetType, theCriteria);
spMap.setLoadSynchronousUpTo(BUFFER_SIZE);
ISearchBuilder searchBuilder = myMdmSearchParamSvc.generateSearchBuilderForType(theSourceResourceType);
return submitAllMatchingResourcesToMdmChannel(spMap, searchBuilder);
ISearchBuilder searchBuilder = myEmpiSearchParamSvc.generateSearchBuilderForType(theTargetType);
return submitAllMatchingResourcesToEmpiChannel(spMap, searchBuilder);
}
private long submitAllMatchingResourcesToMdmChannel(SearchParameterMap theSpMap, ISearchBuilder theSearchBuilder) {
private long submitAllMatchingResourcesToEmpiChannel(SearchParameterMap theSpMap, ISearchBuilder theSearchBuilder) {
SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(null, UUID.randomUUID().toString());
long total = 0;
try (IResultIterator query = theSearchBuilder.createQuery(theSpMap, searchRuntimeDetails, null, RequestPartitionId.defaultPartition())) {
Collection<ResourcePersistentId> pidBatch;
do {
pidBatch = query.getNextResultBatch(BUFFER_SIZE);
total += loadPidsAndSubmitToMdmChannel(theSearchBuilder, pidBatch);
total += loadPidsAndSubmitToEmpiChannel(theSearchBuilder, pidBatch);
} while (query.hasNext());
} catch (IOException theE) {
throw new InternalErrorException("Failure while attempting to query resources for " + ProviderConstants.OPERATION_MDM_SUBMIT, theE);
throw new InternalErrorException("Failure while attempting to query resources for " + ProviderConstants.OPERATION_EMPI_SUBMIT, theE);
}
ourLog.info("MDM Submit complete. Submitted a total of {} resources.", total);
ourLog.info("EMPI Submit complete. Submitted a total of {} resources.", total);
return total;
}
/**
* Given a collection of ResourcePersistentId objects, and a search builder, load the IBaseResources and submit them to
* the MDM channel for processing.
* the EMPI channel for processing.
*
* @param theSearchBuilder the related DAO search builder.
* @param thePidsToSubmit The collection of PIDs whos resources you want to submit for MDM processing.
* @param thePidsToSubmit The collection of PIDs whos resources you want to submit for EMPI processing.
*
* @return The total count of submitted resources.
*/
private long loadPidsAndSubmitToMdmChannel(ISearchBuilder theSearchBuilder, Collection<ResourcePersistentId> thePidsToSubmit) {
private long loadPidsAndSubmitToEmpiChannel(ISearchBuilder theSearchBuilder, Collection<ResourcePersistentId> thePidsToSubmit) {
List<IBaseResource> resourcesToSubmit = new ArrayList<>();
theSearchBuilder.loadResourcesByPid(thePidsToSubmit, Collections.emptyList(), resourcesToSubmit, false, null);
ourLog.info("Submitting {} resources to MDM", resourcesToSubmit.size());
ourLog.info("Submitting {} resources to EMPI", resourcesToSubmit.size());
resourcesToSubmit
.forEach(resource -> myMdmChannelSubmitterSvc.submitResourceToMdmChannel(resource));
.forEach(resource -> myEmpiChannelSubmitterSvc.submitResourceToEmpiChannel(resource));
return resourcesToSubmit.size();
}
@Override
@Transactional
public long submitPractitionerTypeToMdm(@Nullable String theCriteria) {
return submitSourceResourceTypeToMdm("Practitioner", theCriteria);
public long submitPractitionerTypeToEmpi(@Nullable String theCriteria) {
return submitTargetTypeToEmpi("Practitioner", theCriteria);
}
@Override
@Transactional
public long submitPatientTypeToMdm(@Nullable String theCriteria) {
return submitSourceResourceTypeToMdm("Patient", theCriteria);
public long submitPatientTypeToEmpi(@Nullable String theCriteria) {
return submitTargetTypeToEmpi("Patient", theCriteria);
}
@Override
@Transactional
public long submitSourceResourceToMdm(IIdType theId) {
validateSourceType(theId.getResourceType());
public long submitTargetToEmpi(IIdType theId) {
resolveTargetTypeOrThrowException(theId.getResourceType());
IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theId.getResourceType());
IBaseResource read = resourceDao.read(theId);
myMdmChannelSubmitterSvc.submitResourceToMdmChannel(read);
myEmpiChannelSubmitterSvc.submitResourceToEmpiChannel(read);
return 1;
}
@Override
public void setMdmSettings(IMdmSettings theMdmSettings) {
myMdmSettings = theMdmSettings;
}
private void validateSourceType(String theResourceType) {
if(!myMdmSettings.getMdmRules().getMdmTypes().contains(theResourceType)) {
throw new InvalidRequestException(ProviderConstants.OPERATION_MDM_SUBMIT + " does not support resource type: " + theResourceType);
private void resolveTargetTypeOrThrowException(String theResourceType) {
if (!EmpiUtil.supportedTargetType(theResourceType)) {
throw new InvalidRequestException(ProviderConstants.OPERATION_EMPI_SUBMIT + " does not support resource type: " + theResourceType);
}
}
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.mdm.svc.candidate;
package ca.uhn.fhir.jpa.empi.svc.candidate;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -21,7 +21,7 @@ package ca.uhn.fhir.jpa.mdm.svc.candidate;
*/
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.springframework.beans.factory.annotation.Autowired;
@ -31,15 +31,15 @@ public abstract class BaseCandidateFinder {
@Autowired
IdHelperService myIdHelperService;
@Autowired
MdmLinkDaoSvc myMdmLinkDaoSvc;
EmpiLinkDaoSvc myEmpiLinkDaoSvc;
CandidateList findCandidates(IAnyResource theTarget) {
CandidateList candidateList = new CandidateList(getStrategy());
candidateList.addAll(findMatchGoldenResourceCandidates(theTarget));
candidateList.addAll(findMatchPersonCandidates(theTarget));
return candidateList;
}
protected abstract List<MatchedGoldenResourceCandidate> findMatchGoldenResourceCandidates(IAnyResource theTarget);
protected abstract List<MatchedPersonCandidate> findMatchPersonCandidates(IAnyResource theTarget);
protected abstract CandidateStrategyEnum getStrategy();
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.mdm.svc.candidate;
package ca.uhn.fhir.jpa.empi.svc.candidate;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -27,7 +27,7 @@ import java.util.stream.Stream;
public class CandidateList {
private final CandidateStrategyEnum myStrategy;
private final List<MatchedGoldenResourceCandidate> myList = new ArrayList<>();
private final List<MatchedPersonCandidate> myList = new ArrayList<>();
public CandidateList(CandidateStrategyEnum theStrategy) {
myStrategy = theStrategy;
@ -41,9 +41,9 @@ public class CandidateList {
return myList.isEmpty();
}
public void addAll(List<MatchedGoldenResourceCandidate> theList) { myList.addAll(theList); }
public void addAll(List<MatchedPersonCandidate> theList) { myList.addAll(theList); }
public MatchedGoldenResourceCandidate getOnlyMatch() {
public MatchedPersonCandidate getOnlyMatch() {
assert myList.size() == 1;
return myList.get(0);
}
@ -52,15 +52,15 @@ public class CandidateList {
return myList.size()== 1;
}
public Stream<MatchedGoldenResourceCandidate> stream() {
public Stream<MatchedPersonCandidate> stream() {
return myList.stream();
}
public List<MatchedGoldenResourceCandidate> getCandidates() {
public List<MatchedPersonCandidate> getCandidates() {
return Collections.unmodifiableList(myList);
}
public MatchedGoldenResourceCandidate getFirstMatch() {
public MatchedPersonCandidate getFirstMatch() {
return myList.get(0);
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.mdm.svc.candidate;
package ca.uhn.fhir.jpa.empi.svc.candidate;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -21,11 +21,11 @@ package ca.uhn.fhir.jpa.mdm.svc.candidate;
*/
public enum CandidateStrategyEnum {
/** Find Golden Resource candidates based on matching EID */
/** Find Person candidates based on matching EID */
EID,
/** Find Golden Resource candidates based on a link already existing for the source resource */
/** Find Person candidates based on a link already existing for the target resource */
LINK,
/** Find Golden Resource candidates based on other sources that match the incoming source using the MDM Matching rules */
/** Find Person candidates based on other targets that match the incoming target using the EMPI Matching rules */
SCORE;
public boolean isEidMatch() {

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.mdm.svc.candidate;
package ca.uhn.fhir.jpa.empi.svc.candidate;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -20,8 +20,8 @@ package ca.uhn.fhir.jpa.mdm.svc.candidate;
* #L%
*/
import ca.uhn.fhir.mdm.rules.json.MdmResourceSearchParamJson;
import ca.uhn.fhir.jpa.mdm.svc.MdmSearchParamSvc;
import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson;
import ca.uhn.fhir.jpa.empi.svc.EmpiSearchParamSvc;
import ca.uhn.fhir.util.UrlUtil;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.springframework.beans.factory.annotation.Autowired;
@ -35,10 +35,9 @@ import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class MdmCandidateSearchCriteriaBuilderSvc {
public class EmpiCandidateSearchCriteriaBuilderSvc {
@Autowired
private MdmSearchParamSvc myMdmSearchParamSvc;
private EmpiSearchParamSvc myEmpiSearchParamSvc;
/*
* Given a list of criteria upon which to block, a resource search parameter, and a list of values for that given search parameter,
@ -47,14 +46,14 @@ public class MdmCandidateSearchCriteriaBuilderSvc {
* Patient?active=true&name.given=Gary,Grant&name.family=Graham
*/
@Nonnull
public Optional<String> buildResourceQueryString(String theResourceType, IAnyResource theResource, List<String> theFilterCriteria, @Nullable MdmResourceSearchParamJson resourceSearchParam) {
public Optional<String> buildResourceQueryString(String theResourceType, IAnyResource theResource, List<String> theFilterCriteria, @Nullable EmpiResourceSearchParamJson resourceSearchParam) {
List<String> criteria = new ArrayList<>();
// If there are candidate search params, then make use of them, otherwise, search with only the filters.
if (resourceSearchParam != null) {
resourceSearchParam.iterator().forEachRemaining(searchParam -> {
//to compare it to all known GOLDEN_RESOURCE objects, using the overlapping search parameters that they have.
List<String> valuesFromResourceForSearchParam = myMdmSearchParamSvc.getValueFromResourceForSearchParam(theResource, searchParam);
//to compare it to all known PERSON objects, using the overlapping search parameters that they have.
List<String> valuesFromResourceForSearchParam = myEmpiSearchParamSvc.getValueFromResourceForSearchParam(theResource, searchParam);
if (!valuesFromResourceForSearchParam.isEmpty()) {
criteria.add(buildResourceMatchQuery(searchParam, valuesFromResourceForSearchParam));
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.mdm.svc.candidate;
package ca.uhn.fhir.jpa.empi.svc.candidate;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -20,14 +20,14 @@ package ca.uhn.fhir.jpa.mdm.svc.candidate;
* #L%
*/
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.rules.json.MdmFilterSearchParamJson;
import ca.uhn.fhir.mdm.rules.json.MdmResourceSearchParamJson;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.rules.json.EmpiFilterSearchParamJson;
import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.mdm.svc.MdmSearchParamSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiSearchParamSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IAnyResource;
@ -44,48 +44,47 @@ import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static ca.uhn.fhir.mdm.api.MdmConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE;
import static ca.uhn.fhir.empi.api.EmpiConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE;
@Service
public class MdmCandidateSearchSvc {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
public class EmpiCandidateSearchSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private IMdmSettings myMdmSettings;
private IEmpiSettings myEmpiConfig;
@Autowired
private MdmSearchParamSvc myMdmSearchParamSvc;
private EmpiSearchParamSvc myEmpiSearchParamSvc;
@Autowired
private DaoRegistry myDaoRegistry;
@Autowired
private IdHelperService myIdHelperService;
@Autowired
private MdmCandidateSearchCriteriaBuilderSvc myMdmCandidateSearchCriteriaBuilderSvc;
private EmpiCandidateSearchCriteriaBuilderSvc myEmpiCandidateSearchCriteriaBuilderSvc;
public MdmCandidateSearchSvc() {
public EmpiCandidateSearchSvc() {
}
/**
* Given a source resource, search for all resources that are considered an MDM match based on defined MDM rules.
* Given a target resource, search for all resources that are considered an EMPI match based on defined EMPI rules.
*
*
* @param theResourceType
* @param theResource the {@link IBaseResource} we are attempting to match.
* @param theResource the target {@link IBaseResource} we are attempting to match.
*
* @return the list of candidate {@link IBaseResource} which could be matches to theResource
*/
public Collection<IAnyResource> findCandidates(String theResourceType, IAnyResource theResource) {
Map<Long, IAnyResource> matchedPidsToResources = new HashMap<>();
List<MdmFilterSearchParamJson> filterSearchParams = myMdmSettings.getMdmRules().getCandidateFilterSearchParams();
List<EmpiFilterSearchParamJson> filterSearchParams = myEmpiConfig.getEmpiRules().getCandidateFilterSearchParams();
List<String> filterCriteria = buildFilterQuery(filterSearchParams, theResourceType);
List<MdmResourceSearchParamJson> candidateSearchParams = myMdmSettings.getMdmRules().getCandidateSearchParams();
List<EmpiResourceSearchParamJson> candidateSearchParams = myEmpiConfig.getEmpiRules().getCandidateSearchParams();
//If there are zero MdmResourceSearchParamJson, we end up only making a single search, otherwise we
//must perform one search per MdmResourceSearchParamJson.
//If there are zero EmpiResourceSearchParamJson, we end up only making a single search, otherwise we
//must perform one search per EmpiResourceSearchParamJson.
if (candidateSearchParams.isEmpty()) {
searchForIdsAndAddToMap(theResourceType, theResource, matchedPidsToResources, filterCriteria, null);
} else {
for (MdmResourceSearchParamJson resourceSearchParam : candidateSearchParams) {
for (EmpiResourceSearchParamJson resourceSearchParam : candidateSearchParams) {
if (!isSearchParamForResource(theResourceType, resourceSearchParam)) {
continue;
@ -100,12 +99,10 @@ public class MdmCandidateSearchSvc {
if (theResource.getIdElement().getIdPart() != null) {
matchedPidsToResources.remove(myIdHelperService.getPidOrNull(theResource));
}
ourLog.info("Found {} resources for {}", matchedPidsToResources.size(), theResourceType);
return matchedPidsToResources.values();
}
private boolean isSearchParamForResource(String theResourceType, MdmResourceSearchParamJson resourceSearchParam) {
private boolean isSearchParamForResource(String theResourceType, EmpiResourceSearchParamJson resourceSearchParam) {
String resourceType = resourceSearchParam.getResourceType();
return resourceType.equals(theResourceType) || resourceType.equalsIgnoreCase(ALL_RESOURCE_SEARCH_PARAM_TYPE);
}
@ -118,9 +115,9 @@ public class MdmCandidateSearchSvc {
* 4. Store all results in `theMatchedPidsToResources`
*/
@SuppressWarnings("rawtypes")
private void searchForIdsAndAddToMap(String theResourceType, IAnyResource theResource, Map<Long, IAnyResource> theMatchedPidsToResources, List<String> theFilterCriteria, MdmResourceSearchParamJson resourceSearchParam) {
private void searchForIdsAndAddToMap(String theResourceType, IAnyResource theResource, Map<Long, IAnyResource> theMatchedPidsToResources, List<String> theFilterCriteria, EmpiResourceSearchParamJson resourceSearchParam) {
//1.
Optional<String> oResourceCriteria = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString(theResourceType, theResource, theFilterCriteria, resourceSearchParam);
Optional<String> oResourceCriteria = myEmpiCandidateSearchCriteriaBuilderSvc.buildResourceQueryString(theResourceType, theResource, theFilterCriteria, resourceSearchParam);
if (!oResourceCriteria.isPresent()) {
return;
}
@ -128,11 +125,11 @@ public class MdmCandidateSearchSvc {
ourLog.debug("Searching for {} candidates with {}", theResourceType, resourceCriteria);
//2.
SearchParameterMap searchParameterMap = myMdmSearchParamSvc.mapFromCriteria(theResourceType, resourceCriteria);
SearchParameterMap searchParameterMap = myEmpiSearchParamSvc.mapFromCriteria(theResourceType, resourceCriteria);
searchParameterMap.setLoadSynchronous(true);
//TODO MDM this will blow up under large scale i think.
//TODO EMPI this will blow up under large scale i think.
//3.
IFhirResourceDao<?> resourceDao = myDaoRegistry.getResourceDao(theResourceType);
IBundleProvider search = resourceDao.search(searchParameterMap);
@ -150,18 +147,18 @@ public class MdmCandidateSearchSvc {
}
}
private List<String> buildFilterQuery(List<MdmFilterSearchParamJson> theFilterSearchParams, String theResourceType) {
private List<String> buildFilterQuery(List<EmpiFilterSearchParamJson> theFilterSearchParams, String theResourceType) {
return Collections.unmodifiableList(theFilterSearchParams.stream()
.filter(spFilterJson -> paramIsOnCorrectType(theResourceType, spFilterJson))
.map(this::convertToQueryString)
.collect(Collectors.toList()));
}
private boolean paramIsOnCorrectType(String theResourceType, MdmFilterSearchParamJson spFilterJson) {
private boolean paramIsOnCorrectType(String theResourceType, EmpiFilterSearchParamJson spFilterJson) {
return spFilterJson.getResourceType().equals(theResourceType) || spFilterJson.getResourceType().equalsIgnoreCase(ALL_RESOURCE_SEARCH_PARAM_TYPE);
}
private String convertToQueryString(MdmFilterSearchParamJson theSpFilterJson) {
private String convertToQueryString(EmpiFilterSearchParamJson theSpFilterJson) {
String qualifier = theSpFilterJson.getTokenParamModifierAsString();
return theSpFilterJson.getSearchParam() + qualifier + "=" + theSpFilterJson.getFixedValue();
}

View File

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

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.mdm.svc.candidate;
package ca.uhn.fhir.jpa.empi.svc.candidate;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -20,11 +20,11 @@ package ca.uhn.fhir.jpa.mdm.svc.candidate;
* #L%
*/
import ca.uhn.fhir.mdm.api.MdmMatchOutcome;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.model.CanonicalEID;
import ca.uhn.fhir.mdm.util.EIDHelper;
import ca.uhn.fhir.jpa.mdm.svc.MdmResourceDaoSvc;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.model.CanonicalEID;
import ca.uhn.fhir.empi.util.EIDHelper;
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.slf4j.Logger;
@ -37,26 +37,25 @@ import java.util.Optional;
@Service
public class FindCandidateByEidSvc extends BaseCandidateFinder {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private EIDHelper myEIDHelper;
@Autowired
private MdmResourceDaoSvc myMdmResourceDaoSvc;
private EmpiResourceDaoSvc myEmpiResourceDaoSvc;
protected List<MatchedGoldenResourceCandidate> findMatchGoldenResourceCandidates(IAnyResource theBaseResource) {
List<MatchedGoldenResourceCandidate> retval = new ArrayList<>();
protected List<MatchedPersonCandidate> findMatchPersonCandidates(IAnyResource theBaseResource) {
List<MatchedPersonCandidate> retval = new ArrayList<>();
List<CanonicalEID> eidFromResource = myEIDHelper.getExternalEid(theBaseResource);
if (!eidFromResource.isEmpty()) {
for (CanonicalEID eid : eidFromResource) {
Optional<IAnyResource> oFoundGoldenResource = myMdmResourceDaoSvc.searchGoldenResourceByEID(eid.getValue(), theBaseResource.getIdElement().getResourceType());
if (oFoundGoldenResource.isPresent()) {
IAnyResource foundGoldenResource = oFoundGoldenResource.get();
Long pidOrNull = myIdHelperService.getPidOrNull(foundGoldenResource);
MatchedGoldenResourceCandidate mpc = new MatchedGoldenResourceCandidate(new ResourcePersistentId(pidOrNull), MdmMatchOutcome.EID_MATCH);
ourLog.debug("Matched {} by EID {}", foundGoldenResource.getIdElement(), eid);
Optional<IAnyResource> oFoundPerson = myEmpiResourceDaoSvc.searchPersonByEid(eid.getValue());
if (oFoundPerson.isPresent()) {
IAnyResource foundPerson = oFoundPerson.get();
Long pidOrNull = myIdHelperService.getPidOrNull(foundPerson);
MatchedPersonCandidate mpc = new MatchedPersonCandidate(new ResourcePersistentId(pidOrNull), EmpiMatchOutcome.EID_MATCH);
ourLog.debug("Matched {} by EID {}", foundPerson.getIdElement(), eid);
retval.add(mpc);
}
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.mdm.svc.candidate;
package ca.uhn.fhir.jpa.empi.svc.candidate;
/*-
* #%L
* HAPI FHIR JPA Server - Master Data Management
* HAPI FHIR JPA Server - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
@ -20,8 +20,8 @@ package ca.uhn.fhir.jpa.mdm.svc.candidate;
* #L%
*/
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.empi.log.Logs;
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;
@ -33,25 +33,25 @@ import java.util.Optional;
@Service
public class FindCandidateByLinkSvc extends BaseCandidateFinder {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
/**
* Attempt to find a currently matching Golden Resource, based on the presence of an {@link MdmLink} entity.
* Attempt to find a currently matching Person, based on the presence of an {@link EmpiLink} entity.
*
* @param theTarget the {@link IAnyResource} that we want to find candidate Golden Resources for.
* @return an Optional list of {@link MatchedGoldenResourceCandidate} indicating matches.
* @param theTarget the {@link IAnyResource} that we want to find candidate Persons for.
* @return an Optional list of {@link MatchedPersonCandidate} indicating matches.
*/
@Override
protected List<MatchedGoldenResourceCandidate> findMatchGoldenResourceCandidates(IAnyResource theTarget) {
List<MatchedGoldenResourceCandidate> retval = new ArrayList<>();
protected List<MatchedPersonCandidate> findMatchPersonCandidates(IAnyResource theTarget) {
List<MatchedPersonCandidate> retval = new ArrayList<>();
Long targetPid = myIdHelperService.getPidOrNull(theTarget);
if (targetPid != null) {
Optional<MdmLink> oLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(targetPid);
Optional<EmpiLink> oLink = myEmpiLinkDaoSvc.getMatchedLinkForTargetPid(targetPid);
if (oLink.isPresent()) {
ResourcePersistentId goldenResourcePid = new ResourcePersistentId(oLink.get().getGoldenResourcePid());
ResourcePersistentId personPid = new ResourcePersistentId(oLink.get().getPersonPid());
ourLog.debug("Resource previously linked. Using existing link.");
retval.add(new MatchedGoldenResourceCandidate(goldenResourcePid, oLink.get()));
retval.add(new MatchedPersonCandidate(personPid, oLink.get()));
}
}
return retval;

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
package ca.uhn.fhir.jpa.mdm.config;
package ca.uhn.fhir.jpa.empi.config;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.rules.config.MdmRuleValidator;
import ca.uhn.fhir.mdm.rules.config.MdmSettings;
import ca.uhn.fhir.jpa.mdm.helper.MdmLinkHelper;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator;
import ca.uhn.fhir.empi.rules.config.EmpiSettings;
import ca.uhn.fhir.jpa.empi.helper.EmpiLinkHelper;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Value;
@ -15,20 +15,20 @@ import org.springframework.core.io.Resource;
import java.io.IOException;
@Configuration
public abstract class BaseTestMdmConfig {
public abstract class BaseTestEmpiConfig {
@Value("${mdm.prevent_eid_updates:true}")
@Value("${empi.prevent_eid_updates:true}")
boolean myPreventEidUpdates;
@Value("${mdm.prevent_multiple_eids:true}")
@Value("${empi.prevent_multiple_eids:true}")
boolean myPreventMultipleEids;
@Bean
IMdmSettings mdmSettings(MdmRuleValidator theMdmRuleValidator) throws IOException {
IEmpiSettings empiSettings(EmpiRuleValidator theEmpiRuleValidator) throws IOException {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
Resource resource = resourceLoader.getResource("mdm/mdm-rules.json");
Resource resource = resourceLoader.getResource("empi/empi-rules.json");
String json = IOUtils.toString(resource.getInputStream(), Charsets.UTF_8);
return new MdmSettings(theMdmRuleValidator)
return new EmpiSettings(theEmpiRuleValidator)
.setEnabled(false)
.setScriptText(json)
.setPreventEidUpdates(myPreventEidUpdates)
@ -36,7 +36,7 @@ public abstract class BaseTestMdmConfig {
}
@Bean
MdmLinkHelper mdmLinkHelper() {
return new MdmLinkHelper();
EmpiLinkHelper empiLinkHelper() {
return new EmpiLinkHelper();
}
}

View File

@ -1,9 +1,9 @@
package ca.uhn.fhir.jpa.mdm.config;
package ca.uhn.fhir.jpa.empi.config;
import ca.uhn.fhir.jpa.subscription.channel.config.SubscriptionChannelConfig;
import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig;
import org.springframework.context.annotation.Import;
@Import({SubscriptionSubmitterConfig.class, SubscriptionChannelConfig.class})
public class TestMdmConfigR4 extends BaseTestMdmConfig {
public class TestEmpiConfigR4 extends BaseTestEmpiConfig {
}

View File

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

View File

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

View File

@ -1,11 +1,11 @@
package ca.uhn.fhir.jpa.mdm.helper;
package ca.uhn.fhir.jpa.empi.helper;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.mdm.broker.MdmQueueConsumerLoader;
import ca.uhn.fhir.jpa.mdm.config.MdmSubscriptionLoader;
import ca.uhn.fhir.jpa.empi.broker.EmpiQueueConsumerLoader;
import ca.uhn.fhir.jpa.empi.config.EmpiSubscriptionLoader;
import ca.uhn.fhir.jpa.subscription.channel.impl.LinkedBlockingChannel;
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionLoader;
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry;
@ -27,21 +27,21 @@ import static org.mockito.Mockito.when;
/**
* How to use this Rule:
* <p>
* This rule is to be used whenever you want to have the MdmInterceptor loaded, and be able
* to execute creates/updates/deletes while being assured that all MDM work has been done before exiting.
* This rule is to be used whenever you want to have the EmpiInterceptor loaded, and be able
* to execute creates/updates/deletes while being assured that all EMPI work has been done before exiting.
* Provides two types of method:
* <p>
* 1. doUpdate/doCreate. These methods do not wait for Asynchronous MDM work to be done. Use these when you are expecting
* 1. doUpdate/doCreate. These methods do not wait for Asynchronous EMPI work to be done. Use these when you are expecting
* the calls to fail, as those hooks will never be called.
* <p>
* 2. createWithLatch/updateWithLatch. These methods will await the MDM hooks, which are only triggered post-MDM processing
* You should use these when you are expecting successful processing of the resource, and need to wait for async MDM linking
* 2. createWithLatch/updateWithLatch. These methods will await the EMPI hooks, which are only triggered post-EMPI processing
* You should use these when you are expecting successful processing of the resource, and need to wait for async EMPI linking
* work to be done.
* <p>
* Note: all create/update functions take an optional isExternalHttpRequest boolean, to make it appear as though the request's
* origin is an HTTP request.
*/
public abstract class BaseMdmHelper implements BeforeEachCallback, AfterEachCallback {
public abstract class BaseEmpiHelper implements BeforeEachCallback, AfterEachCallback {
@Mock
protected ServletRequestDetails myMockSrd;
@Mock
@ -50,15 +50,15 @@ public abstract class BaseMdmHelper implements BeforeEachCallback, AfterEachCall
protected RestfulServer myMockRestfulServer;
@Mock
protected FhirContext myMockFhirContext;
protected PointcutLatch myAfterMdmLatch = new PointcutLatch(Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED);
protected PointcutLatch myAfterEmpiLatch = new PointcutLatch(Pointcut.EMPI_AFTER_PERSISTED_RESOURCE_CHECKED);
@Autowired
MdmQueueConsumerLoader myMdmQueueConsumerLoader;
EmpiQueueConsumerLoader myEmpiQueueConsumerLoader;
@Autowired
SubscriptionRegistry mySubscriptionRegistry;
@Autowired
SubscriptionLoader mySubscriptionLoader;
@Autowired
MdmSubscriptionLoader myMdmSubscriptionLoader;
EmpiSubscriptionLoader myEmpiSubscriptionLoader;
@Mock
private IInterceptorBroadcaster myMockInterceptorBroadcaster;
@Autowired
@ -66,9 +66,9 @@ public abstract class BaseMdmHelper implements BeforeEachCallback, AfterEachCall
@Override
public void afterEach(ExtensionContext context) throws Exception {
myInterceptorService.unregisterInterceptor(myAfterMdmLatch);
myAfterMdmLatch.clear();
waitUntilMdmQueueIsEmpty();
myInterceptorService.unregisterInterceptor(myAfterEmpiLatch);
myAfterEmpiLatch.clear();
waitUntilEmpiQueueIsEmpty();
}
@Override
@ -83,11 +83,11 @@ public abstract class BaseMdmHelper implements BeforeEachCallback, AfterEachCall
when(myMockRestfulServer.getFhirContext()).thenReturn(myMockFhirContext);
//This sets up our basic interceptor, and also attached the latch so we can await the hook calls.
myInterceptorService.registerAnonymousInterceptor(Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED, myAfterMdmLatch);
myInterceptorService.registerAnonymousInterceptor(Pointcut.EMPI_AFTER_PERSISTED_RESOURCE_CHECKED, myAfterEmpiLatch);
// We need to call this because subscriptions will get deleted in @After cleanup
waitForActivatedSubscriptionCount(0);
myMdmSubscriptionLoader.daoUpdateMdmSubscriptions();
myEmpiSubscriptionLoader.daoUpdateEmpiSubscriptions();
mySubscriptionLoader.syncSubscriptions();
waitForActivatedSubscriptionCount(2);
}
@ -97,12 +97,12 @@ public abstract class BaseMdmHelper implements BeforeEachCallback, AfterEachCall
await("Active Subscription Count has reached " + theSize).until(() -> mySubscriptionRegistry.size() >= theSize);
}
private void waitUntilMdmQueueIsEmpty() {
private void waitUntilEmpiQueueIsEmpty() {
await().until(() -> getExecutorQueueSize() == 0);
}
public int getExecutorQueueSize() {
LinkedBlockingChannel channel = (LinkedBlockingChannel) myMdmQueueConsumerLoader.getMdmChannelForUnitTest();
LinkedBlockingChannel channel = (LinkedBlockingChannel) myEmpiQueueConsumerLoader.getEmpiChannelForUnitTest();
return channel.getQueueSizeForUnitTest();
}

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.mdm.helper;
package ca.uhn.fhir.jpa.empi.helper;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.rules.config.MdmRuleValidator;
import ca.uhn.fhir.mdm.rules.config.MdmSettings;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator;
import ca.uhn.fhir.empi.rules.config.EmpiSettings;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Value;
@ -13,27 +13,27 @@ import org.springframework.core.io.Resource;
import java.io.IOException;
public class MdmHelperConfig {
public class EmpiHelperConfig {
@Bean
public MdmHelperR4 mdmHelperR4() {
return new MdmHelperR4();
public EmpiHelperR4 empiHelperR4() {
return new EmpiHelperR4();
}
@Value("${mdm.prevent_eid_updates:false}")
@Value("${empi.prevent_eid_updates:false}")
boolean myPreventEidUpdates;
@Value("${mdm.prevent_multiple_eids:true}")
@Value("${empi.prevent_multiple_eids:true}")
boolean myPreventMultipleEids;
@Primary
@Bean
IMdmSettings mdmSettings(MdmRuleValidator theMdmRuleValidator) throws IOException {
IEmpiSettings empiSettings(EmpiRuleValidator theEmpiRuleValidator) throws IOException {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
Resource resource = resourceLoader.getResource("mdm/mdm-rules.json");
Resource resource = resourceLoader.getResource("empi/empi-rules.json");
String json = IOUtils.toString(resource.getInputStream(), Charsets.UTF_8);
// Set Enabled to true, and set strict mode.
return new MdmSettings(theMdmRuleValidator)
return new EmpiSettings(theEmpiRuleValidator)
.setEnabled(true)
.setScriptText(json)
.setPreventEidUpdates(myPreventEidUpdates)

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.jpa.mdm.helper;
package ca.uhn.fhir.jpa.empi.helper;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
@ -9,7 +9,7 @@ import ca.uhn.fhir.rest.server.TransactionLogMessages;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
public class MdmHelperR4 extends BaseMdmHelper {
public class EmpiHelperR4 extends BaseEmpiHelper {
@Autowired
private FhirContext myFhirContext;
@Autowired
@ -20,10 +20,10 @@ public class MdmHelperR4 extends BaseMdmHelper {
}
public OutcomeAndLogMessageWrapper createWithLatch(IBaseResource theBaseResource, boolean isExternalHttpRequest) throws InterruptedException {
myAfterMdmLatch.setExpectedCount(1);
myAfterEmpiLatch.setExpectedCount(1);
DaoMethodOutcome daoMethodOutcome = doCreateResource(theBaseResource, isExternalHttpRequest);
myAfterMdmLatch.awaitExpected();
return new OutcomeAndLogMessageWrapper(daoMethodOutcome, myAfterMdmLatch.getLatchInvocationParameterOfType(TransactionLogMessages.class));
myAfterEmpiLatch.awaitExpected();
return new OutcomeAndLogMessageWrapper(daoMethodOutcome, myAfterEmpiLatch.getLatchInvocationParameterOfType(TransactionLogMessages.class));
}
public OutcomeAndLogMessageWrapper updateWithLatch(IBaseResource theIBaseResource) throws InterruptedException {
@ -31,10 +31,10 @@ public class MdmHelperR4 extends BaseMdmHelper {
}
public OutcomeAndLogMessageWrapper updateWithLatch(IBaseResource theIBaseResource, boolean isExternalHttpRequest) throws InterruptedException {
myAfterMdmLatch.setExpectedCount(1);
myAfterEmpiLatch.setExpectedCount(1);
DaoMethodOutcome daoMethodOutcome = doUpdateResource(theIBaseResource, isExternalHttpRequest);
myAfterMdmLatch.awaitExpected();
return new OutcomeAndLogMessageWrapper(daoMethodOutcome, myAfterMdmLatch.getLatchInvocationParameterOfType(TransactionLogMessages.class));
myAfterEmpiLatch.awaitExpected();
return new OutcomeAndLogMessageWrapper(daoMethodOutcome, myAfterEmpiLatch.getLatchInvocationParameterOfType(TransactionLogMessages.class));
}
public DaoMethodOutcome doCreateResource(IBaseResource theResource, boolean isExternalHttpRequest) {
@ -53,7 +53,7 @@ public class MdmHelperR4 extends BaseMdmHelper {
/**
* OutcomeAndLogMessageWrapper is a simple wrapper class which is _excellent_. It allows us to skip the fact that java doesn't allow
* multiple returns, and wraps both the Method Outcome of the DAO, _and_ the TransactionLogMessages that were passed to the pointcut
* by the MDM module.
* by the EMPI module.
*/
public class OutcomeAndLogMessageWrapper {
DaoMethodOutcome myDaoMethodOutcome;

View File

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

View File

@ -1,16 +1,18 @@
package ca.uhn.fhir.jpa.mdm.interceptor;
package ca.uhn.fhir.jpa.empi.interceptor;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
import ca.uhn.fhir.jpa.dao.data.IEmpiLinkDao;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
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.BeforeEach;
import org.junit.jupiter.api.Test;
@ -21,16 +23,18 @@ import static org.hamcrest.core.StringContains.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class MdmExpungeTest extends BaseMdmR4Test {
public class EmpiExpungeTest extends BaseEmpiR4Test {
@Autowired
IInterceptorService myInterceptorService;
@Autowired
IMdmStorageInterceptor myMdmStorageInterceptor;
IEmpiStorageInterceptor myEmpiStorageInterceptor;
@Autowired
DaoConfig myDaoConfig;
@Autowired
IEmpiLinkDao myEmpiLinkDao;
private ResourceTable myTargetEntity;
private ResourceTable mySourceEntity;
private ResourceTable myPersonEntity;
private IdDt myTargetId;
@BeforeEach
@ -39,21 +43,21 @@ public class MdmExpungeTest extends BaseMdmR4Test {
myTargetEntity = (ResourceTable) myPatientDao.create(new Patient()).getEntity();
myTargetId = myTargetEntity.getIdDt().toVersionless();
mySourceEntity = (ResourceTable) myPatientDao.create(new Patient()).getEntity();
myPersonEntity = (ResourceTable) myPersonDao.create(new Person()).getEntity();
MdmLink mdmLink = myMdmLinkDaoSvc.newMdmLink();
mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL);
mdmLink.setMatchResult(MdmMatchResultEnum.MATCH);
mdmLink.setGoldenResourcePid(mySourceEntity.getId());
mdmLink.setSourcePid(myTargetEntity.getId());
saveLink(mdmLink);
EmpiLink empiLink = myEmpiLinkDaoSvc.newEmpiLink();
empiLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
empiLink.setMatchResult(EmpiMatchResultEnum.MATCH);
empiLink.setPersonPid(myPersonEntity.getId());
empiLink.setTargetPid(myTargetEntity.getId());
saveLink(empiLink);
}
@Test
public void testUninterceptedDeleteRemovesMdmReference() {
assertEquals(1, myMdmLinkDao.count());
public void testUninterceptedDeleteRemovesEMPIReference() {
assertEquals(1, myEmpiLinkDao.count());
myPatientDao.delete(myTargetEntity.getIdDt());
assertEquals(1, myMdmLinkDao.count());
assertEquals(1, myEmpiLinkDao.count());
ExpungeOptions expungeOptions = new ExpungeOptions();
expungeOptions.setExpungeDeletedResources(true);
try {
@ -63,14 +67,14 @@ public class MdmExpungeTest extends BaseMdmR4Test {
assertThat(e.getMessage(), containsString("ViolationException"));
assertThat(e.getMessage(), containsString("FK_EMPI_LINK_TARGET"));
}
myInterceptorService.registerInterceptor(myMdmStorageInterceptor);
myInterceptorService.registerInterceptor(myEmpiStorageInterceptor);
myPatientDao.expunge(myTargetId.toVersionless(), expungeOptions, null);
assertEquals(0, myMdmLinkDao.count());
assertEquals(0, myEmpiLinkDao.count());
}
@AfterEach
public void afterUnregisterInterceptor() {
myInterceptorService.unregisterInterceptor(myMdmStorageInterceptor);
myInterceptorService.unregisterInterceptor(myEmpiStorageInterceptor);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
package ca.uhn.fhir.jpa.mdm.provider;
package ca.uhn.fhir.jpa.empi.provider;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.entity.MdmLink;
import org.hl7.fhir.instance.model.api.IAnyResource;
import ca.uhn.fhir.jpa.entity.EmpiLink;
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.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -18,20 +18,20 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
public abstract class BaseLinkR4Test extends BaseProviderR4Test {
protected static final StringType NO_MATCH_RESULT = new StringType(MdmMatchResultEnum.NO_MATCH.name());
protected static final StringType MATCH_RESULT = new StringType(MdmMatchResultEnum.MATCH.name());
protected static final StringType POSSIBLE_MATCH_RESULT = new StringType(MdmMatchResultEnum.POSSIBLE_MATCH.name());
protected static final StringType POSSIBLE_DUPLICATE_RESULT = new StringType(MdmMatchResultEnum.POSSIBLE_DUPLICATE.name());
protected static final StringType NO_MATCH_RESULT = new StringType(EmpiMatchResultEnum.NO_MATCH.name());
protected static final StringType MATCH_RESULT = new StringType(EmpiMatchResultEnum.MATCH.name());
protected static final StringType POSSIBLE_MATCH_RESULT = new StringType(EmpiMatchResultEnum.POSSIBLE_MATCH.name());
protected static final StringType POSSIBLE_DUPLICATE_RESULT = new StringType(EmpiMatchResultEnum.POSSIBLE_DUPLICATE.name());
@Autowired
DaoConfig myDaoConfig;
protected Patient myPatient;
protected IAnyResource mySourcePatient;
protected MdmLink myLink;
protected Person myPerson;
protected EmpiLink myLink;
protected StringType myPatientId;
protected StringType mySourcePatientId;
protected StringType myVersionlessGodlenResourceId;
protected StringType myPersonId;
protected StringType myVersionlessPersonId;
@Override
@BeforeEach
@ -41,15 +41,15 @@ public abstract class BaseLinkR4Test extends BaseProviderR4Test {
myPatient = createPatientAndUpdateLinks(buildPaulPatient());
myPatientId = new StringType(myPatient.getIdElement().getValue());
mySourcePatient = getGoldenResourceFromTargetResource(myPatient);
mySourcePatientId = new StringType(mySourcePatient.getIdElement().getValue());
myVersionlessGodlenResourceId = new StringType(mySourcePatient.getIdElement().toVersionless().getValue());
myPerson = getPersonFromTarget(myPatient);
myPersonId = new StringType(myPerson.getIdElement().getValue());
myVersionlessPersonId = new StringType(myPerson.getIdElement().toVersionless().getValue());
myLink = getOnlyPatientLink();
// Tests require our initial link to be a POSSIBLE_MATCH
myLink.setMatchResult(MdmMatchResultEnum.POSSIBLE_MATCH);
myLink.setMatchResult(EmpiMatchResultEnum.POSSIBLE_MATCH);
saveLink(myLink);
assertEquals(MdmLinkSourceEnum.AUTO, myLink.getLinkSource());
assertEquals(EmpiLinkSourceEnum.AUTO, myLink.getLinkSource());
myDaoConfig.setExpungeEnabled(true);
}
@ -60,12 +60,12 @@ public abstract class BaseLinkR4Test extends BaseProviderR4Test {
}
@Nonnull
protected MdmLink getOnlyPatientLink() {
return myMdmLinkDaoSvc.findMdmLinkBySource(myPatient).get();
protected EmpiLink getOnlyPatientLink() {
return myEmpiLinkDaoSvc.findEmpiLinkByTarget(myPatient).get();
}
@Nonnull
protected List<MdmLink> getPatientLinks() {
return myMdmLinkDaoSvc.findMdmLinksBySourceResource(myPatient);
protected List<EmpiLink> getPatientLinks() {
return myEmpiLinkDaoSvc.findEmpiLinksByTarget(myPatient);
}
}

View File

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

View File

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

View File

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

View File

@ -1,12 +1,11 @@
package ca.uhn.fhir.jpa.mdm.provider;
package ca.uhn.fhir.jpa.empi.provider;
import ca.uhn.fhir.mdm.api.MdmConstants;
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.Medication;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.codesystems.MatchGrade;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -16,13 +15,11 @@ import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
public class MdmProviderMatchR4Test extends BaseProviderR4Test {
public class EmpiProviderMatchR4Test extends BaseProviderR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(MdmProviderMatchR4Test.class);
private static final Logger ourLog = LoggerFactory.getLogger(EmpiProviderMatchR4Test.class);
public static final String NAME_GIVEN_JANET = NAME_GIVEN_JANE + "t";
@ -30,6 +27,7 @@ public class MdmProviderMatchR4Test extends BaseProviderR4Test {
@BeforeEach
public void before() {
super.before();
super.loadEmpiSearchParameters();
}
@Test
@ -39,7 +37,7 @@ public class MdmProviderMatchR4Test extends BaseProviderR4Test {
Patient createdJane = createPatient(jane);
Patient newJane = buildJanePatient();
Bundle result = myMdmProviderR4.match(newJane);
Bundle result = myEmpiProviderR4.match(newJane);
assertEquals(1, result.getEntry().size());
Bundle.BundleEntryComponent entry0 = result.getEntry().get(0);
@ -49,56 +47,7 @@ public class MdmProviderMatchR4Test extends BaseProviderR4Test {
assertEquals(Bundle.SearchEntryMode.MATCH, searchComponent.getMode());
assertEquals(2.0 / 3.0, searchComponent.getScore().doubleValue(), 0.01);
Extension matchGradeExtension = searchComponent.getExtensionByUrl(MdmConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE);
assertNotNull(matchGradeExtension);
assertEquals(MatchGrade.CERTAIN.toCode(), matchGradeExtension.getValue().toString());
}
@Test
public void testMedicationMatch() throws Exception {
createDummyOrganization();
Medication medication = buildMedication("Organization/mfr");
Medication createdMedication = createMedication(medication);
Medication newMedication = buildMedication("Organization/mfr");
Bundle result = myMdmProviderR4.serverMatch(newMedication, new StringType("Medication"));
assertEquals(1, result.getEntry().size());
Bundle.BundleEntryComponent entry0 = result.getEntry().get(0);
assertEquals(createdMedication.getId(), entry0.getResource().getId());
Bundle.BundleEntrySearchComponent searchComponent = entry0.getSearch();
assertEquals(Bundle.SearchEntryMode.MATCH, searchComponent.getMode());
//Since there is only
assertEquals(1.0 / 1.0, searchComponent.getScore().doubleValue(), 0.01);
Extension matchGradeExtension = searchComponent.getExtensionByUrl(MdmConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE);
assertNotNull(matchGradeExtension);
assertEquals(MatchGrade.CERTAIN.toCode(), matchGradeExtension.getValue().toString());
}
@Test
public void testServerLevelMatch() throws Exception {
Patient jane = buildJanePatient();
jane.setActive(true);
Patient createdJane = createPatient(jane);
Patient newJane = buildJanePatient();
Bundle result = myMdmProviderR4.serverMatch(newJane, new StringType("Patient"));
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(MdmConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE);
Extension matchGradeExtension = searchComponent.getExtensionByUrl(EmpiConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE);
assertNotNull(matchGradeExtension);
assertEquals(MatchGrade.CERTAIN.toCode(), matchGradeExtension.getValue().toString());
}
@ -114,7 +63,7 @@ public class MdmProviderMatchR4Test extends BaseProviderR4Test {
Patient newJane = buildJanePatient();
Bundle result = myMdmProviderR4.match(newJane);
Bundle result = myEmpiProviderR4.match(newJane);
assertEquals(2, result.getEntry().size());
Bundle.BundleEntryComponent entry0 = result.getEntry().get(0);
@ -138,19 +87,19 @@ public class MdmProviderMatchR4Test extends BaseProviderR4Test {
Patient paul = buildPaulPatient();
paul.setActive(true);
Bundle result = myMdmProviderR4.match(paul);
Bundle result = myEmpiProviderR4.match(paul);
assertEquals(0, result.getEntry().size());
}
@Test
public void testMatchWithEmptySearchParamCandidates() throws Exception {
setMdmRuleJson("mdm/empty-candidate-search-params.json");
setEmpiRuleJson("empi/empty-candidate-search-params.json");
Patient jane = buildJanePatient();
jane.setActive(true);
Patient createdJane = createPatient(jane);
Patient newJane = buildJanePatient();
Bundle result = myMdmProviderR4.match(newJane);
Bundle result = myEmpiProviderR4.match(newJane);
assertEquals(1, result.getEntry().size());
assertEquals(createdJane.getId(), result.getEntryFirstRep().getResource().getId());
}

View File

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

View File

@ -1,16 +1,15 @@
package ca.uhn.fhir.jpa.mdm.provider;
package ca.uhn.fhir.jpa.empi.provider;
import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.jpa.entity.MdmLink;
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.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.IdType;
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.StringType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -26,11 +25,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class MdmProviderQueryLinkR4Test extends BaseLinkR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(MdmProviderQueryLinkR4Test.class);
public class EmpiProviderQueryLinkR4Test extends BaseLinkR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(EmpiProviderQueryLinkR4Test.class);
private StringType myLinkSource;
private StringType myGoldenResource1Id;
private StringType myGoldenResource2Id;
private StringType myPerson1Id;
private StringType myPerson2Id;
@Override
@BeforeEach
@ -41,27 +40,27 @@ public class MdmProviderQueryLinkR4Test extends BaseLinkR4Test {
createPatientAndUpdateLinks(buildJanePatient());
// Add a possible duplicate
myLinkSource = new StringType(MdmLinkSourceEnum.AUTO.name());
Patient sourcePatient1 = createGoldenPatient();
myGoldenResource1Id = new StringType(sourcePatient1.getIdElement().toVersionless().getValue());
Long sourcePatient1Pid = myIdHelperService.getPidOrNull(sourcePatient1);
Patient sourcePatient2 = createGoldenPatient();
myGoldenResource2Id = new StringType(sourcePatient2.getIdElement().toVersionless().getValue());
Long sourcePatient2Pid = myIdHelperService.getPidOrNull(sourcePatient2);
myLinkSource = new StringType(EmpiLinkSourceEnum.AUTO.name());
Person person1 = createPerson();
myPerson1Id = new StringType(person1.getIdElement().toVersionless().getValue());
Long person1Pid = myIdHelperService.getPidOrNull(person1);
Person person2 = createPerson();
myPerson2Id = new StringType(person2.getIdElement().toVersionless().getValue());
Long person2Pid = myIdHelperService.getPidOrNull(person2);
MdmLink possibleDuplicateMdmLink = myMdmLinkDaoSvc.newMdmLink().setGoldenResourcePid(sourcePatient1Pid).setSourcePid(sourcePatient2Pid).setMatchResult(MdmMatchResultEnum.POSSIBLE_DUPLICATE).setLinkSource(MdmLinkSourceEnum.AUTO);
saveLink(possibleDuplicateMdmLink);
EmpiLink possibleDuplicateEmpiLink = myEmpiLinkDaoSvc.newEmpiLink().setPersonPid(person1Pid).setTargetPid(person2Pid).setMatchResult(EmpiMatchResultEnum.POSSIBLE_DUPLICATE).setLinkSource(EmpiLinkSourceEnum.AUTO);
saveLink(possibleDuplicateEmpiLink);
}
@Test
public void testQueryLinkOneMatch() {
Parameters result = myMdmProviderR4.queryLinks(mySourcePatientId, myPatientId, null, null, myRequestDetails);
Parameters result = myEmpiProviderR4.queryLinks(myPersonId, myPatientId, null, null, myRequestDetails);
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(result));
List<Parameters.ParametersParameterComponent> list = result.getParameter();
assertThat(list, hasSize(1));
List<Parameters.ParametersParameterComponent> part = list.get(0).getPart();
assertMdmLink(7, part, mySourcePatientId.getValue(), myPatientId.getValue(), MdmMatchResultEnum.POSSIBLE_MATCH, "false", "true", null);
assertEmpiLink(7, part, myPersonId.getValue(), myPatientId.getValue(), EmpiMatchResultEnum.POSSIBLE_MATCH, "false", "true", null);
}
@Test
@ -69,41 +68,41 @@ public class MdmProviderQueryLinkR4Test extends BaseLinkR4Test {
// Add a third patient
Patient patient = createPatientAndUpdateLinks(buildJanePatient());
IdType patientId = patient.getIdElement().toVersionless();
IAnyResource goldenResource = getGoldenResourceFromTargetResource(patient);
IIdType goldenResourceId = goldenResource.getIdElement().toVersionless();
Person person = getPersonFromTarget(patient);
IdType personId = person.getIdElement().toVersionless();
Parameters result = myMdmProviderR4.queryLinks(null, null, null, myLinkSource, myRequestDetails);
Parameters result = myEmpiProviderR4.queryLinks(null, null, null, myLinkSource, myRequestDetails);
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(result));
List<Parameters.ParametersParameterComponent> list = result.getParameter();
assertThat(list, hasSize(3));
List<Parameters.ParametersParameterComponent> part = list.get(2).getPart();
assertMdmLink(7, part, goldenResourceId.getValue(), patientId.getValue(), MdmMatchResultEnum.MATCH, "false", "false", "2");
assertEmpiLink(7, part, personId.getValue(), patientId.getValue(), EmpiMatchResultEnum.MATCH, "false", "false", "2");
}
@Test
public void testQueryPossibleDuplicates() {
Parameters result = myMdmProviderR4.getDuplicateGoldenResources(myRequestDetails);
Parameters result = myEmpiProviderR4.getDuplicatePersons(myRequestDetails);
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(result));
List<Parameters.ParametersParameterComponent> list = result.getParameter();
assertThat(list, hasSize(1));
List<Parameters.ParametersParameterComponent> part = list.get(0).getPart();
assertMdmLink(2, part, myGoldenResource1Id.getValue(), myGoldenResource2Id.getValue(), MdmMatchResultEnum.POSSIBLE_DUPLICATE, "false", "false", null);
assertEmpiLink(2, part, myPerson1Id.getValue(), myPerson2Id.getValue(), EmpiMatchResultEnum.POSSIBLE_DUPLICATE, "false", "false", null);
}
@Test
public void testNotDuplicate() {
{
Parameters result = myMdmProviderR4.getDuplicateGoldenResources(myRequestDetails);
Parameters result = myEmpiProviderR4.getDuplicatePersons(myRequestDetails);
List<Parameters.ParametersParameterComponent> list = result.getParameter();
assertThat(list, hasSize(1));
}
{
Parameters result = myMdmProviderR4.notDuplicate(myGoldenResource1Id, myGoldenResource2Id, myRequestDetails);
Parameters result = myEmpiProviderR4.notDuplicate(myPerson1Id, myPerson2Id, myRequestDetails);
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(result));
assertEquals("success", result.getParameterFirstRep().getName());
assertTrue(((BooleanType) (result.getParameterFirstRep().getValue())).booleanValue());
}
Parameters result = myMdmProviderR4.getDuplicateGoldenResources(myRequestDetails);
Parameters result = myEmpiProviderR4.getDuplicatePersons(myRequestDetails);
List<Parameters.ParametersParameterComponent> list = result.getParameter();
assertThat(list, hasSize(0));
}
@ -111,18 +110,18 @@ public class MdmProviderQueryLinkR4Test extends BaseLinkR4Test {
@Test
public void testNotDuplicateBadId() {
try {
myMdmProviderR4.notDuplicate(myGoldenResource1Id, new StringType("Patient/notAnId123"), myRequestDetails);
myEmpiProviderR4.notDuplicate(myPerson1Id, new StringType("Person/notAnId123"), myRequestDetails);
fail();
} catch (ResourceNotFoundException e) {
assertEquals("Resource Patient/notAnId123 is not known", e.getMessage());
assertEquals("Resource Person/notAnId123 is not known", e.getMessage());
}
}
private void assertMdmLink(int theExpectedSize, List<Parameters.ParametersParameterComponent> thePart, String theGoldenResourceId, String theTargetId, MdmMatchResultEnum theMatchResult, String theEidMatch, String theNewGoldenResource, String theScore) {
private void assertEmpiLink(int theExpectedSize, List<Parameters.ParametersParameterComponent> thePart, String thePersonId, String theTargetId, EmpiMatchResultEnum theMatchResult, String theEidMatch, String theNewPerson, String theScore) {
assertThat(thePart, hasSize(theExpectedSize));
assertThat(thePart.get(0).getName(), is("goldenResourceId"));
assertThat(thePart.get(0).getValue().toString(), is(removeVersion(theGoldenResourceId)));
assertThat(thePart.get(1).getName(), is("sourceResourceId"));
assertThat(thePart.get(0).getName(), is("personId"));
assertThat(thePart.get(0).getValue().toString(), is(removeVersion(thePersonId)));
assertThat(thePart.get(1).getName(), is("targetId"));
assertThat(thePart.get(1).getValue().toString(), is(removeVersion(theTargetId)));
if (theExpectedSize > 2) {
assertThat(thePart.get(2).getName(), is("matchResult"));
@ -133,8 +132,8 @@ public class MdmProviderQueryLinkR4Test extends BaseLinkR4Test {
assertThat(thePart.get(4).getName(), is("eidMatch"));
assertThat(thePart.get(4).getValue().primitiveValue(), is(theEidMatch));
assertThat(thePart.get(5).getName(), is("hadToCreateNewResource"));
assertThat(thePart.get(5).getValue().primitiveValue(), is(theNewGoldenResource));
assertThat(thePart.get(5).getName(), is("newPerson"));
assertThat(thePart.get(5).getValue().primitiveValue(), is(theNewPerson));
assertThat(thePart.get(6).getName(), is("score"));
assertThat(thePart.get(6).getValue().primitiveValue(), is(theScore));

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.mdm.svc;
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.mdm.rules.json.MdmResourceSearchParamJson;
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmCandidateSearchCriteriaBuilderSvc;
import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiCandidateSearchCriteriaBuilderSvc;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
@ -20,16 +20,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class MdmCandidateSearchCriteriaBuilderSvcTest extends BaseMdmR4Test {
public class EmpiCandidateSearchCriteriaBuilderSvcTest extends BaseEmpiR4Test {
@Autowired
MdmCandidateSearchCriteriaBuilderSvc myMdmCandidateSearchCriteriaBuilderSvc;
EmpiCandidateSearchCriteriaBuilderSvc myEmpiCandidateSearchCriteriaBuilderSvc;
@Test
public void testEmptyCase() {
Patient patient = new Patient();
MdmResourceSearchParamJson searchParamJson = new MdmResourceSearchParamJson();
EmpiResourceSearchParamJson searchParamJson = new EmpiResourceSearchParamJson();
searchParamJson.addSearchParam("family");
Optional<String> result = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson);
Optional<String> result = myEmpiCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson);
assertFalse(result.isPresent());
}
@ -37,9 +37,9 @@ public class MdmCandidateSearchCriteriaBuilderSvcTest extends BaseMdmR4Test {
public void testSimpleCase() {
Patient patient = new Patient();
patient.addName().setFamily("Fernandez");
MdmResourceSearchParamJson searchParamJson = new MdmResourceSearchParamJson();
EmpiResourceSearchParamJson searchParamJson = new EmpiResourceSearchParamJson();
searchParamJson.addSearchParam("family");
Optional<String> result = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson);
Optional<String> result = myEmpiCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson);
assertTrue(result.isPresent());
assertEquals("Patient?family=Fernandez", result.get());
}
@ -51,10 +51,10 @@ public class MdmCandidateSearchCriteriaBuilderSvcTest extends BaseMdmR4Test {
humanName.addGiven("Jose");
humanName.addGiven("Martin");
humanName.setFamily("Fernandez");
MdmResourceSearchParamJson searchParamJson = new MdmResourceSearchParamJson();
EmpiResourceSearchParamJson searchParamJson = new EmpiResourceSearchParamJson();
searchParamJson.addSearchParam("given");
searchParamJson.addSearchParam("family");
Optional<String> result = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson);
Optional<String> result = myEmpiCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson);
assertTrue(result.isPresent());
assertThat(result.get(), anyOf(equalTo("Patient?given=Jose,Martin&family=Fernandez"), equalTo("Patient?given=Martin,Jose&family=Fernandez")));
}
@ -63,9 +63,9 @@ public class MdmCandidateSearchCriteriaBuilderSvcTest extends BaseMdmR4Test {
public void testIdentifier() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:oid:1.2.36.146.595.217.0.1").setValue("12345");
MdmResourceSearchParamJson searchParamJson = new MdmResourceSearchParamJson();
EmpiResourceSearchParamJson searchParamJson = new EmpiResourceSearchParamJson();
searchParamJson.addSearchParam("identifier");
Optional<String> result = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson);
Optional<String> result = myEmpiCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson);
assertTrue(result.isPresent());
assertEquals(result.get(), "Patient?identifier=urn%3Aoid%3A1.2.36.146.595.217.0.1%7C12345");
}
@ -74,9 +74,9 @@ public class MdmCandidateSearchCriteriaBuilderSvcTest extends BaseMdmR4Test {
public void testIdentifierSpaceIsEscaped() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:oid:1.2.36.146.595.217.0.1").setValue("abc def");
MdmResourceSearchParamJson searchParamJson = new MdmResourceSearchParamJson();
EmpiResourceSearchParamJson searchParamJson = new EmpiResourceSearchParamJson();
searchParamJson.addSearchParam("identifier");
Optional<String> result = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson);
Optional<String> result = myEmpiCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson);
assertTrue(result.isPresent());
assertEquals("Patient?identifier=urn%3Aoid%3A1.2.36.146.595.217.0.1%7Cabc%20def", result.get());
}
@ -84,7 +84,7 @@ public class MdmCandidateSearchCriteriaBuilderSvcTest extends BaseMdmR4Test {
@Test
public void testOmittingCandidateSearchParamsIsAllowed() {
Patient patient = new Patient();
Optional<String> result = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), null);
Optional<String> result = myEmpiCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), null);
assertThat(result.isPresent(), is(true));
assertThat(result.get(), is(equalTo("Patient?")));
}
@ -93,7 +93,7 @@ public class MdmCandidateSearchCriteriaBuilderSvcTest extends BaseMdmR4Test {
public void testEmptyCandidateSearchParamsWorksInConjunctionWithFilterParams() {
Patient patient = new Patient();
List<String> filterParams = Collections.singletonList("active=true");
Optional<String> result = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, filterParams, null);
Optional<String> result = myEmpiCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, filterParams, null);
assertThat(result.isPresent(), is(true));
assertThat(result.get(), is(equalTo("Patient?active=true")));
}

View File

@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.mdm.svc;
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmCandidateSearchSvc;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiCandidateSearchSvc;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Practitioner;
@ -17,10 +17,10 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class MdmCandidateSearchSvcTest extends BaseMdmR4Test {
public class EmpiCandidateSearchSvcTest extends BaseEmpiR4Test {
@Autowired
MdmCandidateSearchSvc myMdmCandidateSearchSvc;
EmpiCandidateSearchSvc myEmpiCandidateSearchSvc;
@Test
public void testFindCandidates() {
@ -29,7 +29,7 @@ public class MdmCandidateSearchSvcTest extends BaseMdmR4Test {
createPatient(jane);
Patient newJane = buildJanePatient();
Collection<IAnyResource> result = myMdmCandidateSearchSvc.findCandidates("Patient", newJane);
Collection<IAnyResource> result = myEmpiCandidateSearchSvc.findCandidates("Patient", newJane);
assertEquals(1, result.size());
}
@ -44,7 +44,7 @@ public class MdmCandidateSearchSvcTest extends BaseMdmR4Test {
Patient newJane = buildJaneWithBirthday(today);
Collection<IAnyResource> result = myMdmCandidateSearchSvc.findCandidates("Patient", newJane);
Collection<IAnyResource> result = myEmpiCandidateSearchSvc.findCandidates("Patient", newJane);
assertEquals(1, result.size());
}
@ -62,7 +62,7 @@ public class MdmCandidateSearchSvcTest extends BaseMdmR4Test {
incomingPatient.setActive(true);
incomingPatient.setGeneralPractitioner(Collections.singletonList(new Reference(practitionerAndUpdateLinks.getId())));
Collection<IAnyResource> patient = myMdmCandidateSearchSvc.findCandidates("Patient", incomingPatient);
Collection<IAnyResource> patient = myEmpiCandidateSearchSvc.findCandidates("Patient", incomingPatient);
assertThat(patient, hasSize(1));
}
}

View File

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

View File

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

View File

@ -1,13 +1,14 @@
package ca.uhn.fhir.jpa.mdm.svc;
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
import ca.uhn.fhir.mdm.api.MdmConstants;
import ca.uhn.fhir.mdm.model.CanonicalEID;
import ca.uhn.fhir.mdm.util.EIDHelper;
import org.hl7.fhir.instance.model.api.IAnyResource;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.model.CanonicalEID;
import ca.uhn.fhir.empi.util.EIDHelper;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import org.hl7.fhir.r4.model.Identifier;
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.springframework.beans.factory.annotation.Autowired;
@ -17,9 +18,9 @@ import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static ca.uhn.fhir.mdm.api.MdmMatchResultEnum.MATCH;
import static ca.uhn.fhir.mdm.api.MdmMatchResultEnum.POSSIBLE_DUPLICATE;
import static ca.uhn.fhir.mdm.api.MdmMatchResultEnum.POSSIBLE_MATCH;
import static ca.uhn.fhir.empi.api.EmpiMatchResultEnum.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.equalTo;
import static org.hamcrest.Matchers.hasSize;
@ -29,24 +30,28 @@ import static org.hamcrest.Matchers.not;
import static org.slf4j.LoggerFactory.getLogger;
@TestPropertySource(properties = {
"mdm.prevent_multiple_eids=false"
"empi.prevent_multiple_eids=false"
})
public class MdmMatchLinkSvcMultipleEidModeTest extends BaseMdmR4Test {
private static final Logger ourLog = getLogger(MdmMatchLinkSvcMultipleEidModeTest.class);
public class EmpiMatchLinkSvcMultipleEidModeTest extends BaseEmpiR4Test {
private static final Logger ourLog = getLogger(EmpiMatchLinkSvcMultipleEidModeTest.class);
@Autowired
private EIDHelper myEidHelper;
@BeforeEach
public void before() {
super.loadEmpiSearchParameters();
}
@Test
public void testIncomingPatientWithEIDThatMatchesGoldenResourceWithHapiEidAddsExternalEidsToGoldenResource() {
// Existing GoldenResource with system-assigned EID found linked from matched Patient. incoming Patient has EID.
// Replace GoldenResource system-assigned EID with Patient EID.
public void testIncomingPatientWithEIDThatMatchesPersonWithHapiEidAddsExternalEidsToPerson() {
// 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());
assertLinksMatchResult(MATCH);
assertLinksCreatedNewResource(true);
assertLinksNewPerson(true);
assertLinksMatchedByEid(false);
IAnyResource janeGoldenResource = getGoldenResourceFromTargetResource(patient);
List<CanonicalEID> hapiEid = myEidHelper.getHapiEid(janeGoldenResource);
Person janePerson = getPersonFromTarget(patient);
List<CanonicalEID> hapiEid = myEidHelper.getHapiEid(janePerson);
String foundHapiEid = hapiEid.get(0).getValue();
Patient janePatient = buildJanePatient();
@ -54,28 +59,28 @@ public class MdmMatchLinkSvcMultipleEidModeTest extends BaseMdmR4Test {
addExternalEID(janePatient, "67890");
createPatientAndUpdateLinks(janePatient);
assertLinksMatchResult(MATCH, MATCH);
assertLinksCreatedNewResource(true, false);
assertLinksNewPerson(true, false);
assertLinksMatchedByEid(false, false);
//We want to make sure the patients were linked to the same GoldenResource.
assertThat(patient, is(sameGoldenResourceAs(janePatient)));
//We want to make sure the patients were linked to the same person.
assertThat(patient, is(samePersonAs(janePatient)));
Patient sourcePatient = (Patient) getGoldenResourceFromTargetResource(patient);
Person person = getPersonFromTarget(patient);
List<Identifier> identifier = sourcePatient.getIdentifier();
List<Identifier> identifier = person.getIdentifier();
//The collision should have kept the old identifier
Identifier firstIdentifier = identifier.get(0);
assertThat(firstIdentifier.getSystem(), is(equalTo(MdmConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM)));
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(myMdmSettings.getMdmRules().getEnterpriseEIDSystem())));
assertThat(secondIdentifier.getSystem(), is(equalTo(myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem())));
assertThat(secondIdentifier.getValue(), is(equalTo("12345")));
Identifier thirdIdentifier = identifier.get(2);
assertThat(thirdIdentifier.getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystem())));
assertThat(thirdIdentifier.getSystem(), is(equalTo(myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem())));
assertThat(thirdIdentifier.getValue(), is(equalTo("67890")));
}
@ -90,7 +95,7 @@ public class MdmMatchLinkSvcMultipleEidModeTest extends BaseMdmR4Test {
addExternalEID(patient1, "id_4");
createPatientAndUpdateLinks(patient1);
assertLinksMatchResult(MATCH);
assertLinksCreatedNewResource(true);
assertLinksNewPerson(true);
assertLinksMatchedByEid(false);
Patient patient2 = buildPaulPatient();
@ -98,37 +103,38 @@ public class MdmMatchLinkSvcMultipleEidModeTest extends BaseMdmR4Test {
addExternalEID(patient2, "id_1");
patient2 = createPatientAndUpdateLinks(patient2);
assertLinksMatchResult(MATCH, MATCH);
assertLinksCreatedNewResource(true, false);
assertLinksNewPerson(true, false);
assertLinksMatchedByEid(false, true);
assertThat(patient1, is(sameGoldenResourceAs(patient2)));
assertThat(patient1, is(samePersonAs(patient2)));
clearExternalEIDs(patient2);
addExternalEID(patient2, "id_6");
//At this point, there should be 5 EIDs on the GoldenResource
Patient patientFromTarget = (Patient) getGoldenResourceFromTargetResource(patient2);
assertThat(patientFromTarget.getIdentifier(), hasSize(5));
//At this point, there should be 5 EIDs on the person
Person personFromTarget = getPersonFromTarget(patient2);
assertThat(personFromTarget.getIdentifier(), hasSize(5));
updatePatientAndUpdateLinks(patient2);
assertLinksMatchResult(MATCH, MATCH);
assertLinksCreatedNewResource(true, false);
assertLinksNewPerson(true, false);
assertLinksMatchedByEid(false, true);
assertThat(patient1, is(sameGoldenResourceAs(patient2)));
assertThat(patient1, is(samePersonAs(patient2)));
patientFromTarget = (Patient) getGoldenResourceFromTargetResource(patient2);
assertThat(patientFromTarget.getIdentifier(), hasSize(6));
personFromTarget = getPersonFromTarget(patient2);
assertThat(personFromTarget.getIdentifier(), hasSize(6));
}
@Test
public void testDuplicateGoldenResourceLinkIsCreatedWhenAnIncomingPatientArrivesWithEIDThatMatchesAnotherEIDPatient() {
public void testDuplicatePersonLinkIsCreatedWhenAnIncomingPatientArrivesWithEIDThatMatchesAnotherEIDPatient() {
Patient patient1 = buildJanePatient();
addExternalEID(patient1, "eid-1");
addExternalEID(patient1, "eid-11");
patient1 = createPatientAndUpdateLinks(patient1);
assertLinksMatchResult(MATCH);
assertLinksCreatedNewResource(true);
assertLinksNewPerson(true);
assertLinksMatchedByEid(false);
Patient patient2 = buildJanePatient();
@ -136,32 +142,32 @@ public class MdmMatchLinkSvcMultipleEidModeTest extends BaseMdmR4Test {
addExternalEID(patient2, "eid-22");
patient2 = createPatientAndUpdateLinks(patient2);
assertLinksMatchResult(MATCH, MATCH, POSSIBLE_DUPLICATE);
assertLinksCreatedNewResource(true, true, false);
assertLinksNewPerson(true, true, false);
assertLinksMatchedByEid(false, false, true);
List<MdmLink> possibleDuplicates = myMdmLinkDaoSvc.getPossibleDuplicates();
List<EmpiLink> possibleDuplicates = myEmpiLinkDaoSvc.getPossibleDuplicates();
assertThat(possibleDuplicates, hasSize(1));
List<Long> duplicatePids = Stream.of(patient1, patient2)
.map(this::getGoldenResourceFromTargetResource)
.map(this::getPersonFromTarget)
.map(myIdHelperService::getPidOrNull)
.collect(Collectors.toList());
//The two GoldenResources related to the patients should both show up in the only existing POSSIBLE_DUPLICATE MdmLink.
MdmLink mdmLink = possibleDuplicates.get(0);
assertThat(mdmLink.getGoldenResourcePid(), is(in(duplicatePids)));
assertThat(mdmLink.getSourcePid(), is(in(duplicatePids)));
//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
// Test Case #5
public void testWhenPatientEidUpdateWouldCauseALinkChangeThatDuplicateGoldenResourceIsCreatedInstead() {
public void testWhenPatientEidUpdateWouldCauseALinkChangeThatDuplicatePersonIsCreatedInstead() {
Patient patient1 = buildJanePatient();
addExternalEID(patient1, "eid-1");
addExternalEID(patient1, "eid-11");
patient1 = createPatientAndUpdateLinks(patient1);
assertLinksMatchResult(MATCH);
assertLinksCreatedNewResource(true);
assertLinksNewPerson(true);
assertLinksMatchedByEid(false);
Patient patient2 = buildPaulPatient();
@ -169,36 +175,36 @@ public class MdmMatchLinkSvcMultipleEidModeTest extends BaseMdmR4Test {
addExternalEID(patient2, "eid-22");
patient2 = createPatientAndUpdateLinks(patient2);
assertLinksMatchResult(MATCH, MATCH);
assertLinksCreatedNewResource(true, true);
assertLinksNewPerson(true, true);
assertLinksMatchedByEid(false, false);
Patient patient3 = buildPaulPatient();
addExternalEID(patient3, "eid-22");
patient3 = createPatientAndUpdateLinks(patient3);
assertLinksMatchResult(MATCH, MATCH, MATCH);
assertLinksCreatedNewResource(true, true, false);
assertLinksNewPerson(true, true, false);
assertLinksMatchedByEid(false, false, true);
//Now, Patient 2 and 3 are linked, and the GoldenResource has 2 eids.
assertThat(patient2, is(sameGoldenResourceAs(patient3)));
//Now, Patient 2 and 3 are linked, and the person has 2 eids.
assertThat(patient2, is(samePersonAs(patient3)));
//Now lets change one of the EIDs on the second patient to one that matches our original patient.
//This should create a situation in which the incoming EIDs are matched to _two_ different GoldenResources. In this case, we want to
//set them all to possible_match, and set the two GoldenResources as possible duplicates.
//This should create a situation in which the incoming EIDs are matched to _two_ different persons. In this case, we want to
//set them all to possible_match, and set the two persons as possible duplicates.
patient2.getIdentifier().clear();
addExternalEID(patient2, "eid-11");
addExternalEID(patient2, "eid-22");
patient2 = updatePatientAndUpdateLinks(patient2);
logAllLinks();
assertLinksMatchResult(MATCH, POSSIBLE_MATCH, MATCH, POSSIBLE_MATCH, POSSIBLE_DUPLICATE);
assertLinksCreatedNewResource(true, true, false, false, false);
assertLinksNewPerson(true, true, false, false, false);
assertLinksMatchedByEid(false, true, true, true, true);
assertThat(patient2, is(not(matchedToAGoldenResource())));
assertThat(patient2, is(not(matchedToAPerson())));
assertThat(patient2, is(possibleMatchWith(patient1)));
assertThat(patient2, is(possibleMatchWith(patient3)));
List<MdmLink> possibleDuplicates = myMdmLinkDaoSvc.getPossibleDuplicates();
List<EmpiLink> possibleDuplicates = myEmpiLinkDaoSvc.getPossibleDuplicates();
assertThat(possibleDuplicates, hasSize(1));
assertThat(patient3, is(possibleDuplicateOf(patient1)));
}

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.mdm.svc;
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.rules.json.EmpiRulesJson;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -16,28 +16,26 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
@ExtendWith(SpringExtension.class)
class MdmResourceFilteringSvcMockTest {
class EmpiResourceFilteringSvcMockTest {
@MockBean
private IMdmSettings myMdmSettings;
private IEmpiSettings myEmpiSettings;
@MockBean
MdmSearchParamSvc myMdmSearchParamSvc;
EmpiSearchParamSvc myEmpiSearchParamSvc;
@MockBean
FhirContext myFhirContext;
@Autowired
private MdmResourceFilteringSvc myMdmResourceFilteringSvc;
private EmpiResourceFilteringSvc myEmpiResourceFilteringSvc;
@Configuration
static class SpringConfig {
@Bean
MdmResourceFilteringSvc mdmResourceFilteringSvc() {
return new MdmResourceFilteringSvc();
@Bean EmpiResourceFilteringSvc empiResourceFilteringSvc() {
return new EmpiResourceFilteringSvc();
}
}
@Test
public void testEmptyCriteriaShouldBeProcessed() {
when(myMdmSettings.getMdmRules()).thenReturn(new MdmRulesJson());
assertTrue(myMdmResourceFilteringSvc.shouldBeProcessed(new Patient()));
when(myEmpiSettings.getEmpiRules()).thenReturn(new EmpiRulesJson());
assertTrue(myEmpiResourceFilteringSvc.shouldBeProcessed(new Patient()));
}
}

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