Merge pull request #2223 from jamesagnew/revert-2177-gg_20201105-remove-person-references
Revert "Remove all references to Person from EMPI."
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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>"
|
|
@ -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
|
||||
|
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 55 KiB |
|
@ -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.
|
|
@ -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.
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
```
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.*")
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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"));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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(); }
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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() {
|
|
@ -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));
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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)
|
|
@ -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;
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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")));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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")));
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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)));
|
||||
}
|
|
@ -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)));
|
||||
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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()));
|
||||
}
|
||||
}
|