diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java index 0c1c743e52d..d24e8cd0d70 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/interceptor/api/Pointcut.java @@ -1703,21 +1703,21 @@ public enum Pointcut { ), /** - * MDM(EMPI) Hook: - * 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. + * EMPI Hook: + * 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. *

* Hooks may accept the following parameters: *

*

*

* Hooks should return void. *

*/ - 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"), /** * Performance Tracing Hook: diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index 34118c40667..ac1ede75193 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -38,7 +38,7 @@ ${project.groupId} - hapi-fhir-server-mdm + hapi-fhir-server-empi ${project.version} @@ -103,7 +103,7 @@ ${project.groupId} - hapi-fhir-jpaserver-mdm + hapi-fhir-jpaserver-empi ${project.version} diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2021-empi b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2021-empi index 8d7227ab096..5e2757c426a 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2021-empi +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_1_0/2021-empi @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2177-remove-Person-references-to-support-MDM.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2177-remove-Person-references-to-support-MDM.yaml deleted file mode 100644 index 6b2099bf6d1..00000000000 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_0/2177-remove-Person-references-to-support-MDM.yaml +++ /dev/null @@ -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: - -
- Code-level changes include the following changes: - " diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties index 9c188cc1093..d2e44605c57 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties @@ -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 diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-1.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-1.svg index ac9f5cb2df5..ace08f4b7e4 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-1.svg +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-1.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-2.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-2.svg index f61dafba9d2..fec85fd9d6a 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-2.svg +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-2.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-3.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-3.svg index 8aaf6e7a593..cba6d93dedd 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-3.svg +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-3.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-4.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-4.svg index 266810020aa..2634dd16209 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-4.svg +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-4.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-5.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-5.svg index 553c9fc3abb..deccf53d490 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-5.svg +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-5.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi.md new file mode 100644 index 00000000000..e334acd738b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi.md @@ -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. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md new file mode 100644 index 00000000000..ced336803d8 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md @@ -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. + +EMPI links + +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. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_eid.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_eid.md new file mode 100644 index 00000000000..8287bac64f5 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_eid.md @@ -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 + +EMPI Create 1 + +EMPI Create 1 + +EMPI Create 1 + +EMPI Create 1 + +EMPI Create 1 + +## EMPI EID Update Scenarios + +EMPI Create 1 + +EMPI Create 1 + +EMPI Create 1 + +EMPI Create 1 + +EMPI Create 1 + +EMPI Create 1 + diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_operations.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_operations.md similarity index 52% rename from hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_operations.md rename to hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_operations.md index cc003f94d28..b4fdcdc3393 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_operations.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_operations.md @@ -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: @@ -19,19 +19,19 @@ Use the `$mdm-query-links` operation to view MDM links. The results returned are - + - + @@ -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: + +
goldenResourceIdpersonId String 0..1 - The id of the Golden Resource (e.g. Golden Patient Resource). + The id of the Person resource.
resourceIdtargetId String 0..1 - The id of the source resource (e.g. Patient resource). + The id of the Patient or Practitioner resource.
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EMPI matchResultEMPI linkSourcePerson link.assurance
NO_MATCHMANUALNo link present
POSSIBLE_MATCHAUTOlevel2
MATCHAUTOlevel3
MATCHMANUALlevel4
+ +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: @@ -160,19 +200,19 @@ This operation takes the following parameters: - + - + @@ -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:
goldenResourceIdpersonId String 1..1 - The id of the Golden Resource. + The id of the Person resource.
resourceIdtargetId String 1..1 - 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.
@@ -228,19 +268,19 @@ Use the `$mdm-update-link` operation to change the `matchResult` update of an md - + - + @@ -254,26 +294,26 @@ Use the `$mdm-update-link` operation to change the `matchResult` update of an md
goldenResourceIdpersonId String 1..1 - The id of the Golden Resource. + The id of the Person resource.
resourceIdtargetId String 1..1 - The id of the source resource. + The id of the Patient or Practitioner resource.
-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: - fromGoldenResourceId + fromPersonId String 1..1 - The id of the Golden Resource to merge data from. + The id of the Person resource to merge data from. - toGoldenResourceId + toPersonId String 1..1 - The id of the Golden Resource to merge data into. + The id of the Person to merge data into. @@ -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. - sourceType + resourceType String 0..1 - 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. @@ -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 ``` diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_rules.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_rules.md similarity index 83% rename from hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_rules.md rename to hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_rules.md index 6eb89df53e0..823254f4b17 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_rules.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_rules.md @@ -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. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm.md deleted file mode 100644 index 6ebf41dc9ba..00000000000 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm.md +++ /dev/null @@ -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. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_details.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_details.md deleted file mode 100644 index 83d88ef894c..00000000000 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_details.md +++ /dev/null @@ -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. - -MDM links - -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. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_eid.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_eid.md deleted file mode 100644 index d86d3a68ecc..00000000000 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_eid.md +++ /dev/null @@ -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 - -MDM Create 1 - -MDM Create 2 - -MDM Create 3 - -MDM Create 4 - -MDM Create 5 - -## MDM EID Update Scenarios - -MDM Update 1 - -MDM Update 2 - -MDM Update 3 - -MDM Update 4 - -MDM Update 5 - -MDM Update 6 - diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index f91a35b7c22..a9705690eeb 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -33,7 +33,7 @@
ca.uhn.hapi.fhir - hapi-fhir-server-mdm + hapi-fhir-server-empi ${project.version} @@ -108,7 +108,7 @@ ca.uhn.hapi.fhir - hapi-fhir-jpaserver-mdm + hapi-fhir-jpaserver-empi ${project.version} diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 9abe1ce49b6..dd2d3aaf337 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -77,7 +77,7 @@ ca.uhn.hapi.fhir - hapi-fhir-server-mdm + hapi-fhir-server-empi ${project.version} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index 3ac84bf6186..27090bbbc8f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -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.*") diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IMdmLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IEmpiLinkDao.java similarity index 72% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IMdmLinkDao.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IEmpiLinkDao.java index bd519869845..629f8b8c578 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IMdmLinkDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IEmpiLinkDao.java @@ -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 { +public interface IEmpiLinkDao extends JpaRepository { @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); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkDeleteSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/empi/EmpiLinkDeleteSvc.java similarity index 60% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkDeleteSvc.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/empi/EmpiLinkDeleteSvc.java index 9b441a4007b..5ea233500a6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmLinkDeleteSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/empi/EmpiLinkDeleteSvc.java @@ -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; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceTableFKProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceTableFKProvider.java index 6afd6c6d1e3..fecf1e41355 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceTableFKProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ResourceTableFKProvider.java @@ -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")); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/MdmLink.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/EmpiLink.java similarity index 57% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/MdmLink.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/EmpiLink.java index 500f7499d0c..1bb2f4368db 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/MdmLink.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/EmpiLink.java @@ -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; - } - } diff --git a/hapi-fhir-jpaserver-mdm/pom.xml b/hapi-fhir-jpaserver-empi/pom.xml similarity index 94% rename from hapi-fhir-jpaserver-mdm/pom.xml rename to hapi-fhir-jpaserver-empi/pom.xml index 87f1069fbdd..38b2da8f0a3 100644 --- a/hapi-fhir-jpaserver-mdm/pom.xml +++ b/hapi-fhir-jpaserver-empi/pom.xml @@ -10,15 +10,15 @@ ../hapi-deployable-pom/pom.xml - hapi-fhir-jpaserver-mdm + hapi-fhir-jpaserver-empi jar - HAPI FHIR JPA Server - Master Data Management + HAPI FHIR JPA Server - Enterprise Master Patient Index ca.uhn.hapi.fhir - hapi-fhir-server-mdm + hapi-fhir-server-empi ${project.version} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/broker/MdmMessageHandler.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/broker/EmpiMessageHandler.java similarity index 57% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/broker/MdmMessageHandler.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/broker/EmpiMessageHandler.java index 253bb93ea5b..d1bc6211d9c 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/broker/MdmMessageHandler.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/broker/EmpiMessageHandler.java @@ -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); } } diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/broker/MdmQueueConsumerLoader.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/broker/EmpiQueueConsumerLoader.java similarity index 52% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/broker/MdmQueueConsumerLoader.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/broker/EmpiQueueConsumerLoader.java index 0804a9dae61..b6c5350e336 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/broker/MdmQueueConsumerLoader.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/broker/EmpiQueueConsumerLoader.java @@ -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; } } diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiConsumerConfig.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiConsumerConfig.java new file mode 100644 index 00000000000..7c624955eb9 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiConsumerConfig.java @@ -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(); } +} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmLoader.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiLoader.java similarity index 58% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmLoader.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiLoader.java index 84a9c860cf8..46fd60ecf9f 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmLoader.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiLoader.java @@ -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"); } } diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiSearchParameterLoader.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiSearchParameterLoader.java new file mode 100644 index 00000000000..b67d5bd6199 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiSearchParameterLoader.java @@ -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 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; + } +} diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiSubmitterConfig.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiSubmitterConfig.java new file mode 100644 index 00000000000..378a13c65e7 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiSubmitterConfig.java @@ -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(); + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmSubscriptionLoader.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiSubscriptionLoader.java similarity index 62% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmSubscriptionLoader.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiSubscriptionLoader.java index 75b2ac91a6a..c5532081d2f 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmSubscriptionLoader.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiSubscriptionLoader.java @@ -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 mySubscriptionDao; - synchronized public void daoUpdateMdmSubscriptions() { - List subscriptions; - List 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; } diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/dao/EmpiLinkDaoSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/dao/EmpiLinkDaoSvc.java new file mode 100644 index 00000000000..4a16a1d7820 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/dao/EmpiLinkDaoSvc.java @@ -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 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 getLinkByPersonPidAndTargetPid(Long thePersonPid, Long theTargetPid) { + + if (theTargetPid == null || thePersonPid == null) { + return Optional.empty(); + } + EmpiLink link = myEmpiLinkFactory.newEmpiLink(); + link.setTargetPid(theTargetPid); + link.setPersonPid(thePersonPid); + Example 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 getEmpiLinksByTargetPidAndMatchResult(Long theTargetPid, EmpiMatchResultEnum theMatchResult) { + EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink(); + exampleLink.setTargetPid(theTargetPid); + exampleLink.setMatchResult(theMatchResult); + Example 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 getMatchedLinkForTargetPid(Long theTargetPid) { + EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink(); + exampleLink.setTargetPid(theTargetPid); + exampleLink.setMatchResult(EmpiMatchResultEnum.MATCH); + Example 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 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 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 getEmpiLinksByPersonPidTargetPidAndMatchResult(Long thePersonPid, Long theTargetPid, EmpiMatchResultEnum theMatchResult) { + EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink(); + exampleLink.setPersonPid(thePersonPid); + exampleLink.setTargetPid(theTargetPid); + exampleLink.setMatchResult(theMatchResult); + Example 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 getPossibleDuplicates() { + EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink(); + exampleLink.setMatchResult(EmpiMatchResultEnum.POSSIBLE_DUPLICATE); + Example example = Example.of(exampleLink); + return myEmpiLinkDao.findAll(example); + } + + public Optional findEmpiLinkByTarget(IBaseResource theTargetResource) { + @Nullable Long pid = myIdHelperService.getPidOrNull(theTargetResource); + if (pid == null) { + return Optional.empty(); + } + EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink().setTargetPid(pid); + Example 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 findEmpiLinksByPerson(IBaseResource thePersonResource) { + Long pid = myIdHelperService.getPidOrNull(thePersonResource); + if (pid == null) { + return Collections.emptyList(); + } + EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink().setPersonPid(pid); + Example 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 deleteAllEmpiLinksAndReturnPersonPids() { + List all = myEmpiLinkDao.findAll(); + return deleteEmpiLinksAndReturnPersonPids(all); + } + + private List deleteEmpiLinksAndReturnPersonPids(List theLinks) { + Set 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 deleteAllEmpiLinksOfTypeAndReturnPersonPids(String theTargetType) { + EmpiLink link = new EmpiLink(); + link.setEmpiTargetType(theTargetType); + Example exampleLink = Example.of(link); + List 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 findEmpiLinkByExample(Example 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 findEmpiLinksByTarget(IBaseResource theTargetResource) { + Long pid = myIdHelperService.getPidOrNull(theTargetResource); + if (pid == null) { + return Collections.emptyList(); + } + EmpiLink exampleLink = myEmpiLinkFactory.newEmpiLink().setTargetPid(pid); + Example 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(); + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkFactory.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/dao/EmpiLinkFactory.java similarity index 57% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkFactory.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/dao/EmpiLinkFactory.java index 0970dc9455b..7e09f99e2db 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkFactory.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/dao/EmpiLinkFactory.java @@ -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()); } } diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/interceptor/EmpiStorageInterceptor.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/interceptor/EmpiStorageInterceptor.java new file mode 100644 index 00000000000..75ba90af75d --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/interceptor/EmpiStorageInterceptor.java @@ -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 newExternalEids = myEIDHelper.getExternalEid(theNewResource); + List 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)); + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/interceptor/EmpiSubmitterInterceptorLoader.java similarity index 74% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/interceptor/EmpiSubmitterInterceptorLoader.java index 089323a762f..db847aa5104 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmSubmitterInterceptorLoader.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/interceptor/EmpiSubmitterInterceptorLoader.java @@ -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(); } diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/IMdmStorageInterceptor.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/interceptor/IEmpiStorageInterceptor.java similarity index 81% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/IMdmStorageInterceptor.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/interceptor/IEmpiStorageInterceptor.java index 1e98da17c39..50cf4e54a53 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/IMdmStorageInterceptor.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/interceptor/IEmpiStorageInterceptor.java @@ -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 { } diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmChannelSubmitterSvcImpl.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiChannelSubmitterSvcImpl.java similarity index 65% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmChannelSubmitterSvcImpl.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiChannelSubmitterSvcImpl.java index fa6512dc7c7..e841398e1bd 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmChannelSubmitterSvcImpl.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiChannelSubmitterSvcImpl.java @@ -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; } } diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiClearSvcImpl.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiClearSvcImpl.java new file mode 100644 index 00000000000..e4aa76b7391 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiClearSvcImpl.java @@ -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 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 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(); + } +} + diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiControllerSvcImpl.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiControllerSvcImpl.java new file mode 100644 index 00000000000..ae7e2c9215f --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiControllerSvcImpl.java @@ -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 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 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); + } +} diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiEidUpdateService.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiEidUpdateService.java new file mode 100644 index 00000000000..12af900192c --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiEidUpdateService.java @@ -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 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 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; + } + } +} diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkQuerySvcImpl.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkQuerySvcImpl.java new file mode 100644 index 00000000000..f633a895370 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkQuerySvcImpl.java @@ -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 queryLinks(IIdType thePersonId, IIdType theTargetId, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiContext) { + Example exampleLink = exampleLinkFromParameters(thePersonId, theTargetId, theMatchResult, theLinkSource); + return myEmpiLinkDaoSvc.findEmpiLinkByExample(exampleLink).stream() + .filter(empiLink -> empiLink.getMatchResult() != EmpiMatchResultEnum.POSSIBLE_DUPLICATE) + .map(this::toJson); + } + + + + @Override + public Stream getDuplicatePersons(EmpiTransactionContext theEmpiContext) { + Example 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 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); + } +} diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkSvcImpl.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkSvcImpl.java new file mode 100644 index 00000000000..f9c20636330 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkSvcImpl.java @@ -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 empiLinks = myEmpiLinkDaoSvc.findEmpiLinksByPerson(thePersonResource); + + List 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 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 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 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); + } +} diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkUpdaterSvcImpl.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkUpdaterSvcImpl.java new file mode 100644 index 00000000000..bcfd2ed22df --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkUpdaterSvcImpl.java @@ -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 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 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"); + } + + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchFinderSvcImpl.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchFinderSvcImpl.java similarity index 53% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchFinderSvcImpl.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchFinderSvcImpl.java index 3696fd6626f..f8260b00469 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchFinderSvcImpl.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchFinderSvcImpl.java @@ -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 getMatchedTargets(String theResourceType, IAnyResource theResource) { - Collection targetCandidates = myMdmCandidateSearchSvc.findCandidates(theResourceType, theResource); + Collection targetCandidates = myEmpiCandidateSearchSvc.findCandidates(theResourceType, theResource); - List 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; } } diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchLinkSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchLinkSvc.java new file mode 100644 index 00000000000..35e8f228c83 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchLinkSvc.java @@ -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 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); + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmGoldenResourceDeletingSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiPersonDeletingSvc.java similarity index 77% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmGoldenResourceDeletingSvc.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiPersonDeletingSvc.java index bec0ed1cc26..a9e5b517639 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmGoldenResourceDeletingSvc.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiPersonDeletingSvc.java @@ -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 theGoldenResourcePids, String theResourceType, ServletRequestDetails theRequestDetails) { - return myDeleteExpungeService.expungeByResourcePids(ProviderConstants.MDM_CLEAR, theResourceType, new SliceImpl<>(theGoldenResourcePids), theRequestDetails); + public DeleteMethodOutcome expungePersonPids(List thePersonPids, ServletRequestDetails theRequestDetails) { + return myDeleteExpungeService.expungeByResourcePids(ProviderConstants.EMPI_CLEAR, "Person", new SliceImpl<>(thePersonPids), theRequestDetails); } } diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiPersonMergerSvcImpl.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiPersonMergerSvcImpl.java new file mode 100644 index 00000000000..b19c2357391 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiPersonMergerSvcImpl.java @@ -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 fromLinks = myEmpiLinkDaoSvc.findEmpiLinksByPerson(theFromPerson); + List toLinks = myEmpiLinkDaoSvc.findEmpiLinksByPerson(theToPerson); + + // For each incomingLink, either ignore it, move it, or replace the original one + + for (EmpiLink fromLink : fromLinks) { + Optional 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 findFirstLinkWithMatchingTarget(List 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); + } +} diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceDaoSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceDaoSvc.java new file mode 100644 index 00000000000..c640215f608 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceDaoSvc.java @@ -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 myPatientDao; + private IFhirResourceDao myPersonDao; + private IFhirResourceDao 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 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 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)); + } + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceFilteringSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceFilteringSvc.java similarity index 51% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceFilteringSvc.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceFilteringSvc.java index 0cb449e4060..040ea575219 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceFilteringSvc.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceFilteringSvc.java @@ -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 candidateSearchParams = myMdmSettings.getMdmRules().getCandidateSearchParams(); + List 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; } } diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmSearchParamSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiSearchParamSvc.java similarity index 87% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmSearchParamSvc.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiSearchParamSvc.java index b8a53004eef..9955c2fa047 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmSearchParamSvc.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiSearchParamSvc.java @@ -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()); } /** diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmSubmitSvcImpl.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiSubmitSvcImpl.java similarity index 55% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmSubmitSvcImpl.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiSubmitSvcImpl.java index d38a0a11630..cc8b978e738 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmSubmitSvcImpl.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiSubmitSvcImpl.java @@ -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 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 thePidsToSubmit) { + private long loadPidsAndSubmitToEmpiChannel(ISearchBuilder theSearchBuilder, Collection thePidsToSubmit) { List 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); } } } diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/BaseCandidateFinder.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/BaseCandidateFinder.java similarity index 76% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/BaseCandidateFinder.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/BaseCandidateFinder.java index 080112de36c..3a2c577d46e 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/BaseCandidateFinder.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/BaseCandidateFinder.java @@ -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 findMatchGoldenResourceCandidates(IAnyResource theTarget); + protected abstract List findMatchPersonCandidates(IAnyResource theTarget); protected abstract CandidateStrategyEnum getStrategy(); } diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateList.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/CandidateList.java similarity index 73% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateList.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/CandidateList.java index 4ecee05693b..621e0e1ccc2 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateList.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/CandidateList.java @@ -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 myList = new ArrayList<>(); + private final List myList = new ArrayList<>(); public CandidateList(CandidateStrategyEnum theStrategy) { myStrategy = theStrategy; @@ -41,9 +41,9 @@ public class CandidateList { return myList.isEmpty(); } - public void addAll(List theList) { myList.addAll(theList); } + public void addAll(List 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 stream() { + public Stream stream() { return myList.stream(); } - public List getCandidates() { + public List getCandidates() { return Collections.unmodifiableList(myList); } - public MatchedGoldenResourceCandidate getFirstMatch() { + public MatchedPersonCandidate getFirstMatch() { return myList.get(0); } diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateStrategyEnum.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/CandidateStrategyEnum.java similarity index 67% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateStrategyEnum.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/CandidateStrategyEnum.java index 3fbebef7e94..6524c6afc3a 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateStrategyEnum.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/CandidateStrategyEnum.java @@ -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() { diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmCandidateSearchCriteriaBuilderSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/EmpiCandidateSearchCriteriaBuilderSvc.java similarity index 79% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmCandidateSearchCriteriaBuilderSvc.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/EmpiCandidateSearchCriteriaBuilderSvc.java index 3273c786a57..9fee5912336 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmCandidateSearchCriteriaBuilderSvc.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/EmpiCandidateSearchCriteriaBuilderSvc.java @@ -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 buildResourceQueryString(String theResourceType, IAnyResource theResource, List theFilterCriteria, @Nullable MdmResourceSearchParamJson resourceSearchParam) { + public Optional buildResourceQueryString(String theResourceType, IAnyResource theResource, List theFilterCriteria, @Nullable EmpiResourceSearchParamJson resourceSearchParam) { List 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 valuesFromResourceForSearchParam = myMdmSearchParamSvc.getValueFromResourceForSearchParam(theResource, searchParam); + //to compare it to all known PERSON objects, using the overlapping search parameters that they have. + List valuesFromResourceForSearchParam = myEmpiSearchParamSvc.getValueFromResourceForSearchParam(theResource, searchParam); if (!valuesFromResourceForSearchParam.isEmpty()) { criteria.add(buildResourceMatchQuery(searchParam, valuesFromResourceForSearchParam)); } diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmCandidateSearchSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/EmpiCandidateSearchSvc.java similarity index 67% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmCandidateSearchSvc.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/EmpiCandidateSearchSvc.java index 9d6f726663a..bae3f518447 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmCandidateSearchSvc.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/EmpiCandidateSearchSvc.java @@ -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 findCandidates(String theResourceType, IAnyResource theResource) { Map matchedPidsToResources = new HashMap<>(); - List filterSearchParams = myMdmSettings.getMdmRules().getCandidateFilterSearchParams(); + List filterSearchParams = myEmpiConfig.getEmpiRules().getCandidateFilterSearchParams(); List filterCriteria = buildFilterQuery(filterSearchParams, theResourceType); - List candidateSearchParams = myMdmSettings.getMdmRules().getCandidateSearchParams(); + List 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 theMatchedPidsToResources, List theFilterCriteria, MdmResourceSearchParamJson resourceSearchParam) { + private void searchForIdsAndAddToMap(String theResourceType, IAnyResource theResource, Map theMatchedPidsToResources, List theFilterCriteria, EmpiResourceSearchParamJson resourceSearchParam) { //1. - Optional oResourceCriteria = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString(theResourceType, theResource, theFilterCriteria, resourceSearchParam); + Optional 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 buildFilterQuery(List theFilterSearchParams, String theResourceType) { + private List buildFilterQuery(List 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(); } diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/EmpiPersonFindingSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/EmpiPersonFindingSvc.java new file mode 100644 index 00000000000..b106496be6f --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/EmpiPersonFindingSvc.java @@ -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: + *

+ * 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); + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/FindCandidateByEidSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/FindCandidateByEidSvc.java similarity index 55% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/FindCandidateByEidSvc.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/FindCandidateByEidSvc.java index b599e393c45..4581ba4641b 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/FindCandidateByEidSvc.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/FindCandidateByEidSvc.java @@ -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 findMatchGoldenResourceCandidates(IAnyResource theBaseResource) { - List retval = new ArrayList<>(); + protected List findMatchPersonCandidates(IAnyResource theBaseResource) { + List retval = new ArrayList<>(); List eidFromResource = myEIDHelper.getExternalEid(theBaseResource); if (!eidFromResource.isEmpty()) { for (CanonicalEID eid : eidFromResource) { - Optional 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 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); } } diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/FindCandidateByLinkSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/FindCandidateByLinkSvc.java similarity index 60% rename from hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/FindCandidateByLinkSvc.java rename to hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/FindCandidateByLinkSvc.java index 6eb8ba5e7cb..4de065c0708 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/FindCandidateByLinkSvc.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/FindCandidateByLinkSvc.java @@ -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 findMatchGoldenResourceCandidates(IAnyResource theTarget) { - List retval = new ArrayList<>(); + protected List findMatchPersonCandidates(IAnyResource theTarget) { + List retval = new ArrayList<>(); Long targetPid = myIdHelperService.getPidOrNull(theTarget); if (targetPid != null) { - Optional oLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(targetPid); + Optional 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; diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/FindCandidateByScoreSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/FindCandidateByScoreSvc.java new file mode 100644 index 00000000000..9391e4182de --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/FindCandidateByScoreSvc.java @@ -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 findMatchPersonCandidates(IAnyResource theTarget) { + List retval = new ArrayList<>(); + + List personPidsToExclude = getNoMatchPersonPids(theTarget); + List 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 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 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; + } +} diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/MatchedPersonCandidate.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/MatchedPersonCandidate.java new file mode 100644 index 00000000000..dadd064633c --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/candidate/MatchedPersonCandidate.java @@ -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(); + } +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/BaseEmpiR4Test.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/BaseEmpiR4Test.java new file mode 100644 index 00000000000..b71dc7441b9 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/BaseEmpiR4Test.java @@ -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 myPersonDao; + @Autowired + protected IFhirResourceDao myPatientDao; + @Autowired + protected IFhirResourceDao 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 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 samePersonAs(IAnyResource... theBaseResource) { + return IsSamePersonAs.samePersonAs(myIdHelperService, myEmpiLinkDaoSvc, theBaseResource); + } + + protected Matcher linkedTo(IAnyResource... theBaseResource) { + return IsLinkedTo.linkedTo(myIdHelperService, myEmpiLinkDaoSvc, theBaseResource); + } + + protected Matcher possibleLinkedTo(IAnyResource... theBaseResource) { + return IsPossibleLinkedTo.possibleLinkedTo(myIdHelperService, myEmpiLinkDaoSvc, theBaseResource); + } + + protected Matcher possibleMatchWith(IAnyResource... theBaseResource) { + return IsPossibleMatchWith.possibleMatchWith(myIdHelperService, myEmpiLinkDaoSvc, theBaseResource); + } + + protected Matcher possibleDuplicateOf(IAnyResource... theBaseResource) { + return IsPossibleDuplicateOf.possibleDuplicateOf(myIdHelperService, myEmpiLinkDaoSvc, theBaseResource); + } + + protected Matcher matchedToAPerson() { + return IsMatchedToAPerson.matchedToAPerson(myIdHelperService, myEmpiLinkDaoSvc); + } + + protected Person getOnlyActivePerson() { + List resources = getAllActivePersons(); + assertEquals(1, resources.size()); + return (Person) resources.get(0); + } + + @Nonnull + protected List getAllActivePersons() { + return getAllPersons(true); + } + + @Nonnull + protected List getAllPersons() { + return getAllPersons(false); + } + + @Nonnull + private List 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 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 void assertFields(Function theAccessor, T... theExpectedValues) { + List 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"); + } + } + +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/BaseTestMdmConfig.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/config/BaseTestEmpiConfig.java similarity index 54% rename from hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/BaseTestMdmConfig.java rename to hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/config/BaseTestEmpiConfig.java index 5b02acb4570..2eeebefc5e9 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/BaseTestMdmConfig.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/config/BaseTestEmpiConfig.java @@ -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(); } } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/TestMdmConfigR4.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/config/TestEmpiConfigR4.java similarity index 75% rename from hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/TestMdmConfigR4.java rename to hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/config/TestEmpiConfigR4.java index 204db38ffdd..50ae0500ed6 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/TestMdmConfigR4.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/config/TestEmpiConfigR4.java @@ -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 { } diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/dao/EmpiLinkDaoSvcTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/dao/EmpiLinkDaoSvcTest.java new file mode 100644 index 00000000000..a2733bc18a3 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/dao/EmpiLinkDaoSvcTest.java @@ -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()); + } + +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/entity/EmpiEnumTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/entity/EmpiEnumTest.java new file mode 100644 index 00000000000..b7518ed65d8 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/entity/EmpiEnumTest.java @@ -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]); + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/BaseMdmHelper.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/BaseEmpiHelper.java similarity index 73% rename from hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/BaseMdmHelper.java rename to hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/BaseEmpiHelper.java index 1389c4c5ea1..fab4e3c4895 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/BaseMdmHelper.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/BaseEmpiHelper.java @@ -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: *

- * 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: *

- * 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. *

- * 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. *

* 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(); } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/MdmHelperConfig.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/EmpiHelperConfig.java similarity index 58% rename from hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/MdmHelperConfig.java rename to hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/EmpiHelperConfig.java index 42feead36fc..8e9a91e1c56 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/MdmHelperConfig.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/EmpiHelperConfig.java @@ -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) diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/MdmHelperR4.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/EmpiHelperR4.java similarity index 87% rename from hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/MdmHelperR4.java rename to hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/EmpiHelperR4.java index 5bc2e30c6e7..f1faca6fef8 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/MdmHelperR4.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/EmpiHelperR4.java @@ -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; diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/EmpiLinkHelper.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/EmpiLinkHelper.java new file mode 100644 index 00000000000..b4c0cfaed2b --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/EmpiLinkHelper.java @@ -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 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()); + } + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmExpungeTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/interceptor/EmpiExpungeTest.java similarity index 59% rename from hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmExpungeTest.java rename to hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/interceptor/EmpiExpungeTest.java index a0ca44cc446..415f80bfbe1 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmExpungeTest.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/interceptor/EmpiExpungeTest.java @@ -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); } } diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/interceptor/EmpiStorageInterceptorIT.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/interceptor/EmpiStorageInterceptorIT.java new file mode 100644 index 00000000000..e3575c743a7 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/interceptor/EmpiStorageInterceptorIT.java @@ -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 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 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 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); + } + +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/BasePersonMatcher.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/BasePersonMatcher.java new file mode 100644 index 00000000000..e6ee7f961c8 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/BasePersonMatcher.java @@ -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 { + private static final Logger ourLog = LoggerFactory.getLogger(BasePersonMatcher.class); + + protected IdHelperService myIdHelperService; + protected EmpiLinkDaoSvc myEmpiLinkDaoSvc; + protected Collection 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 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 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 getEmpiLinksForTarget(IAnyResource thePatientOrPractitionerResource, EmpiMatchResultEnum theMatchResult) { + Long pidOrNull = myIdHelperService.getPidOrNull(thePatientOrPractitionerResource); + List matchLinkForTarget = myEmpiLinkDaoSvc.getEmpiLinksByTargetPidAndMatchResult(pidOrNull, theMatchResult); + if (!matchLinkForTarget.isEmpty()) { + return matchLinkForTarget; + } else { + return new ArrayList<>(); + } + } +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsLinkedTo.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsLinkedTo.java new file mode 100644 index 00000000000..4a18b40d2c1 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsLinkedTo.java @@ -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 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 linkedTo(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) { + return new IsLinkedTo(theIdHelperService, theEmpiLinkDaoSvc, theBaseResource); + } +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsMatchedToAPerson.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsMatchedToAPerson.java new file mode 100644 index 00000000000..4d4fcd9d1e5 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsMatchedToAPerson.java @@ -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 { + + 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 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 matchedToAPerson(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc) { + return new IsMatchedToAPerson(theIdHelperService, theEmpiLinkDaoSvc); + } +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsPossibleDuplicateOf.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsPossibleDuplicateOf.java new file mode 100644 index 00000000000..4e42a78b57b --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsPossibleDuplicateOf.java @@ -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 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 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 possibleDuplicateOf(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) { + return new IsPossibleDuplicateOf(theIdHelperService, theEmpiLinkDaoSvc, theBaseResource); + } +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsPossibleLinkedTo.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsPossibleLinkedTo.java new file mode 100644 index 00000000000..9e435d7d8a8 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsPossibleLinkedTo.java @@ -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 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 possibleLinkedTo(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) { + return new IsPossibleLinkedTo(theIdHelperService, theEmpiLinkDaoSvc, theBaseResource); + } +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsPossibleMatchWith.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsPossibleMatchWith.java new file mode 100644 index 00000000000..58796bd4239 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsPossibleMatchWith.java @@ -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 empiLinks = getEmpiLinksForTarget(theIncomingResource, EmpiMatchResultEnum.POSSIBLE_MATCH); + + List 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 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 possibleMatchWith(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) { + return new IsPossibleMatchWith(theIdHelperService, theEmpiLinkDaoSvc, theBaseResource); + } +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsSamePersonAs.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsSamePersonAs.java new file mode 100644 index 00000000000..96d4f8b3ccd --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/matcher/IsSamePersonAs.java @@ -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 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 samePersonAs(IdHelperService theIdHelperService, EmpiLinkDaoSvc theEmpiLinkDaoSvc, IAnyResource... theBaseResource) { + return new IsSamePersonAs(theIdHelperService, theEmpiLinkDaoSvc, theBaseResource); + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/BaseLinkR4Test.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/BaseLinkR4Test.java similarity index 53% rename from hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/BaseLinkR4Test.java rename to hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/BaseLinkR4Test.java index d44a313340d..91461942939 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/BaseLinkR4Test.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/BaseLinkR4Test.java @@ -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 getPatientLinks() { - return myMdmLinkDaoSvc.findMdmLinksBySourceResource(myPatient); + protected List getPatientLinks() { + return myEmpiLinkDaoSvc.findEmpiLinksByTarget(myPatient); } } diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/BaseProviderR4Test.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/BaseProviderR4Test.java new file mode 100644 index 00000000000..f610447a760 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/BaseProviderR4Test.java @@ -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); + } +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderBatchR4Test.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderBatchR4Test.java new file mode 100644 index 00000000000..03dd8c52aad --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderBatchR4Test.java @@ -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"))); + } + } +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderClearLinkR4Test.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderClearLinkR4Test.java new file mode 100644 index 00000000000..c5bf15f4b17 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderClearLinkR4Test.java @@ -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 getPractitionerLinks() { + return myEmpiLinkDaoSvc.findEmpiLinksByTarget(myPractitioner); + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderMatchR4Test.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderMatchR4Test.java similarity index 51% rename from hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderMatchR4Test.java rename to hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderMatchR4Test.java index 9b25fb413a6..f7785341f1e 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderMatchR4Test.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderMatchR4Test.java @@ -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()); } diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderMergePersonsR4Test.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderMergePersonsR4Test.java new file mode 100644 index 00000000000..aaa7c06a1b6 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderMergePersonsR4Test.java @@ -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 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/ where 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/ where 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/ where 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()); + } + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderQueryLinkR4Test.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderQueryLinkR4Test.java similarity index 54% rename from hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderQueryLinkR4Test.java rename to hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderQueryLinkR4Test.java index db4a22829df..276119ba1ab 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderQueryLinkR4Test.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderQueryLinkR4Test.java @@ -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 list = result.getParameter(); assertThat(list, hasSize(1)); List 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 list = result.getParameter(); assertThat(list, hasSize(3)); List 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 list = result.getParameter(); assertThat(list, hasSize(1)); List 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 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 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 thePart, String theGoldenResourceId, String theTargetId, MdmMatchResultEnum theMatchResult, String theEidMatch, String theNewGoldenResource, String theScore) { + private void assertEmpiLink(int theExpectedSize, List 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)); diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderUpdateLinkR4Test.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderUpdateLinkR4Test.java new file mode 100644 index 00000000000..01ee296f568 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/provider/EmpiProviderUpdateLinkR4Test.java @@ -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 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 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/ where 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/ or Practitioner/ where 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()); + } + } +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/searchparam/SearchParameterTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/searchparam/SearchParameterTest.java new file mode 100644 index 00000000000..5343272192a --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/searchparam/SearchParameterTest.java @@ -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 links = person.getLink(); + assertEquals(2, links.size()); + assertEquals(Person.IdentityAssuranceLevel.LEVEL2, links.get(0).getAssurance()); + assertEquals(Person.IdentityAssuranceLevel.LEVEL1, links.get(1).getAssurance()); + } +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiBatchSvcImplTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiBatchSvcImplTest.java new file mode 100644 index 00000000000..3961ce70b01 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiBatchSvcImplTest.java @@ -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); + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmCandidateSearchCriteriaBuilderSvcTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiCandidateSearchCriteriaBuilderSvcTest.java similarity index 59% rename from hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmCandidateSearchCriteriaBuilderSvcTest.java rename to hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiCandidateSearchCriteriaBuilderSvcTest.java index db439e71cf9..457d4737d75 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmCandidateSearchCriteriaBuilderSvcTest.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiCandidateSearchCriteriaBuilderSvcTest.java @@ -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 result = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson); + Optional 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 result = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson); + Optional 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 result = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson); + Optional 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 result = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson); + Optional 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 result = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson); + Optional 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 result = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), null); + Optional 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 filterParams = Collections.singletonList("active=true"); - Optional result = myMdmCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, filterParams, null); + Optional result = myEmpiCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, filterParams, null); assertThat(result.isPresent(), is(true)); assertThat(result.get(), is(equalTo("Patient?active=true"))); } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmCandidateSearchSvcTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiCandidateSearchSvcTest.java similarity index 75% rename from hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmCandidateSearchSvcTest.java rename to hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiCandidateSearchSvcTest.java index 55288727bb1..17c56fb94e7 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmCandidateSearchSvcTest.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiCandidateSearchSvcTest.java @@ -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 result = myMdmCandidateSearchSvc.findCandidates("Patient", newJane); + Collection result = myEmpiCandidateSearchSvc.findCandidates("Patient", newJane); assertEquals(1, result.size()); } @@ -44,7 +44,7 @@ public class MdmCandidateSearchSvcTest extends BaseMdmR4Test { Patient newJane = buildJaneWithBirthday(today); - Collection result = myMdmCandidateSearchSvc.findCandidates("Patient", newJane); + Collection 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 patient = myMdmCandidateSearchSvc.findCandidates("Patient", incomingPatient); + Collection patient = myEmpiCandidateSearchSvc.findCandidates("Patient", incomingPatient); assertThat(patient, hasSize(1)); } } diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkSvcTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkSvcTest.java new file mode 100644 index 00000000000..98591556def --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkSvcTest.java @@ -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()); + } +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkUpdaterSvcImplTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkUpdaterSvcImplTest.java new file mode 100644 index 00000000000..3e812f48de4 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkUpdaterSvcImplTest.java @@ -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 { +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcMultipleEidModeTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchLinkSvcMultipleEidModeTest.java similarity index 55% rename from hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcMultipleEidModeTest.java rename to hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchLinkSvcMultipleEidModeTest.java index e08fb1a8453..be9a192688c 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcMultipleEidModeTest.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchLinkSvcMultipleEidModeTest.java @@ -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 hapiEid = myEidHelper.getHapiEid(janeGoldenResource); + Person janePerson = getPersonFromTarget(patient); + List 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 = sourcePatient.getIdentifier(); + List 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 possibleDuplicates = myMdmLinkDaoSvc.getPossibleDuplicates(); + List possibleDuplicates = myEmpiLinkDaoSvc.getPossibleDuplicates(); assertThat(possibleDuplicates, hasSize(1)); List 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 possibleDuplicates = myMdmLinkDaoSvc.getPossibleDuplicates(); + List possibleDuplicates = myEmpiLinkDaoSvc.getPossibleDuplicates(); assertThat(possibleDuplicates, hasSize(1)); assertThat(patient3, is(possibleDuplicateOf(patient1))); } diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchLinkSvcTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchLinkSvcTest.java new file mode 100644 index 00000000000..948b5c86aee --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchLinkSvcTest.java @@ -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 = myEmpiLinkDaoSvc.getMatchedLinkForTargetPid(janePatient.getIdElement().getIdPartAsLong()); + assertThat(empiLink.isPresent(), is(true)); + + Person person = getPersonFromEmpiLink(empiLink.get()); + List 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 = 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 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 = 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 possibleDuplicates = myEmpiLinkDaoSvc.getPossibleDuplicates(); + assertThat(possibleDuplicates, hasSize(1)); + + + List 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 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 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 possibleDuplicates = myEmpiLinkDaoSvc.getPossibleDuplicates(); + assertThat(possibleDuplicates, hasSize(1)); + assertThat(patient3, is(possibleDuplicateOf(patient1))); + + } +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiPersonMergerSvcTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiPersonMergerSvcTest.java new file mode 100644 index 00000000000..c7cd05b2c9f --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiPersonMergerSvcTest.java @@ -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 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 foundLinks = myEmpiLinkDao.findAll(); + assertEquals(1, foundLinks.size()); + assertEquals(EmpiMatchResultEnum.POSSIBLE_DUPLICATE, foundLinks.get(0).getMatchResult()); + } + + mergePersons(); + + { + List 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 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 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 links = getNonRedirectLinksByPerson(myToPerson); + assertEquals(1, links.size()); + assertEquals(EmpiLinkSourceEnum.MANUAL, links.get(0).getLinkSource()); + } + + private List 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 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 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)); + } +} diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceDaoSvcTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceDaoSvcTest.java new file mode 100644 index 00000000000..35ebf5723ec --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceDaoSvcTest.java @@ -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 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 foundPerson = myResourceDaoSvc.searchPersonByEid(TEST_EID); + assertTrue(foundPerson.isPresent()); + assertThat(foundPerson.get().getIdElement().toUnqualifiedVersionless().getValue(), is(goodPerson.getIdElement().toUnqualifiedVersionless().getValue())); + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceFilteringSvcMockTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceFilteringSvcMockTest.java similarity index 58% rename from hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceFilteringSvcMockTest.java rename to hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceFilteringSvcMockTest.java index fa36f796cae..8f73e2c21d5 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceFilteringSvcMockTest.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceFilteringSvcMockTest.java @@ -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())); } } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceFilteringSvcTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceFilteringSvcTest.java similarity index 57% rename from hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceFilteringSvcTest.java rename to hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceFilteringSvcTest.java index 8a57c1fc575..f0fbf66dca9 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceFilteringSvcTest.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiResourceFilteringSvcTest.java @@ -1,6 +1,6 @@ -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.empi.BaseEmpiR4Test; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.Test; @@ -10,29 +10,29 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -class MdmResourceFilteringSvcTest extends BaseMdmR4Test { +class EmpiResourceFilteringSvcTest extends BaseEmpiR4Test { @Autowired - private MdmResourceFilteringSvc myMdmResourceFilteringSvc; + private EmpiResourceFilteringSvc myEmpiResourceFilteringSvc; @Test public void testFilterResourcesWhichHaveNoRelevantAttributes() { Patient patient = new Patient(); - patient.setDeceased(new BooleanType(true)); // MDM rules defined do not care about the deceased attribute. + patient.setDeceased(new BooleanType(true)); //EMPI rules defined do not care about the deceased attribute. //SUT - boolean shouldBeProcessed = myMdmResourceFilteringSvc.shouldBeProcessed(patient); + boolean shouldBeProcessed = myEmpiResourceFilteringSvc.shouldBeProcessed(patient); assertThat(shouldBeProcessed, is(equalTo(false))); } @Test - public void testDoNotFilterResourcesWithMdmAttributes() { + public void testDoNotFilterResourcesWithEMPIAttributes() { Patient patient = new Patient(); - patient.addIdentifier().setValue("Hey I'm an ID! rules defined in mdm-rules.json care about me!"); + patient.addIdentifier().setValue("Hey I'm an ID! rules defined in empi-rules.json care about me!"); //SUT - boolean shouldBeProcessed = myMdmResourceFilteringSvc.shouldBeProcessed(patient); + boolean shouldBeProcessed = myEmpiResourceFilteringSvc.shouldBeProcessed(patient); assertThat(shouldBeProcessed, is(equalTo(true))); } diff --git a/hapi-fhir-jpaserver-empi/src/test/resources/empi/empi-rules.json b/hapi-fhir-jpaserver-empi/src/test/resources/empi/empi-rules.json new file mode 100644 index 00000000000..0ebdca9eb45 --- /dev/null +++ b/hapi-fhir-jpaserver-empi/src/test/resources/empi/empi-rules.json @@ -0,0 +1,66 @@ +{ + "version": "1", + "candidateSearchParams": [ + { + "resourceType": "Patient", + "searchParams": [ + "birthdate" + ] + }, + { + "resourceType": "*", + "searchParams": [ + "identifier" + ] + }, + { + "resourceType": "Patient", + "searchParams": [ + "general-practitioner" + ] + } + ], + "candidateFilterSearchParams": [ + { + "resourceType": "*", + "searchParam": "active", + "fixedValue": "true" + } + ], + "matchFields": [ + { + "name": "cosine-given-name", + "resourceType": "*", + "resourcePath": "name.given", + "similarity": { + "algorithm": "COSINE", + "matchThreshold": 0.8, + "exact": true + } + }, + { + "name": "jaro-last-name", + "resourceType": "*", + "resourcePath": "name.family", + "similarity": { + "algorithm": "JARO_WINKLER", + "matchThreshold": 0.8, + "exact": true + } + }, + { + "name": "medicare-id", + "resourceType": "*", + "resourcePath": "identifier", + "matcher": { + "algorithm": "IDENTIFIER", + "identifierSystem": "http://hl7.org/fhir/sid/us-medicare" + } + } + ], + "matchResultMap": { + "cosine-given-name": "POSSIBLE_MATCH", + "cosine-given-name,jaro-last-name": "MATCH" + }, + "eidSystem": "http://company.io/fhir/NamingSystem/custom-eid-system" +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/resources/mdm/empty-candidate-search-params.json b/hapi-fhir-jpaserver-empi/src/test/resources/empi/empty-candidate-search-params.json similarity index 95% rename from hapi-fhir-jpaserver-mdm/src/test/resources/mdm/empty-candidate-search-params.json rename to hapi-fhir-jpaserver-empi/src/test/resources/empi/empty-candidate-search-params.json index 42eb9cb0829..fd651459d63 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/resources/mdm/empty-candidate-search-params.json +++ b/hapi-fhir-jpaserver-empi/src/test/resources/empi/empty-candidate-search-params.json @@ -1,5 +1,4 @@ { - "mdmTypes": ["Patient", "Practitioner"], "version": "1", "candidateSearchParams": [], "candidateFilterSearchParams": [ diff --git a/hapi-fhir-jpaserver-mdm/src/test/resources/logback-test.xml b/hapi-fhir-jpaserver-empi/src/test/resources/logback-test.xml similarity index 84% rename from hapi-fhir-jpaserver-mdm/src/test/resources/logback-test.xml rename to hapi-fhir-jpaserver-empi/src/test/resources/logback-test.xml index aa595257758..42a6bc8a7b1 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/resources/logback-test.xml +++ b/hapi-fhir-jpaserver-empi/src/test/resources/logback-test.xml @@ -48,14 +48,15 @@ + - + DEBUG - ${smile.basedir}/log/mdm-troubleshooting.log + ${smile.basedir}/log/empi-troubleshooting.log - ${smile.basedir}/log/mdm-troubleshooting.log.%i.gz + ${smile.basedir}/log/empi-troubleshooting.log.%i.gz 1 9 @@ -66,11 +67,12 @@ %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - + + + + diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java deleted file mode 100644 index 9770aca1d27..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmConsumerConfig.java +++ /dev/null @@ -1,235 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.config; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.IMdmControllerSvc; -import ca.uhn.fhir.mdm.api.IMdmExpungeSvc; -import ca.uhn.fhir.mdm.api.IMdmLinkQuerySvc; -import ca.uhn.fhir.mdm.api.IMdmLinkSvc; -import ca.uhn.fhir.mdm.api.IMdmLinkUpdaterSvc; -import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc; -import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc; -import ca.uhn.fhir.mdm.api.IMdmSettings; -import ca.uhn.fhir.mdm.log.Logs; -import ca.uhn.fhir.mdm.provider.MdmControllerHelper; -import ca.uhn.fhir.mdm.provider.MdmProviderLoader; -import ca.uhn.fhir.mdm.rules.config.MdmRuleValidator; -import ca.uhn.fhir.mdm.rules.svc.MdmResourceMatcherSvc; -import ca.uhn.fhir.mdm.util.EIDHelper; -import ca.uhn.fhir.mdm.util.MessageHelper; -import ca.uhn.fhir.mdm.util.GoldenResourceHelper; -import ca.uhn.fhir.jpa.dao.mdm.MdmLinkDeleteSvc; -import ca.uhn.fhir.jpa.mdm.broker.MdmMessageHandler; -import ca.uhn.fhir.jpa.mdm.broker.MdmQueueConsumerLoader; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkFactory; -import ca.uhn.fhir.jpa.mdm.interceptor.MdmStorageInterceptor; -import ca.uhn.fhir.jpa.mdm.interceptor.IMdmStorageInterceptor; -import ca.uhn.fhir.jpa.mdm.svc.MdmClearSvcImpl; -import ca.uhn.fhir.jpa.mdm.svc.MdmControllerSvcImpl; -import ca.uhn.fhir.jpa.mdm.svc.MdmEidUpdateService; -import ca.uhn.fhir.jpa.mdm.svc.MdmLinkQuerySvcImpl; -import ca.uhn.fhir.jpa.mdm.svc.MdmLinkSvcImpl; -import ca.uhn.fhir.jpa.mdm.svc.MdmLinkUpdaterSvcImpl; -import ca.uhn.fhir.jpa.mdm.svc.MdmMatchFinderSvcImpl; -import ca.uhn.fhir.jpa.mdm.svc.MdmMatchLinkSvc; -import ca.uhn.fhir.jpa.mdm.svc.MdmGoldenResourceDeletingSvc; -import ca.uhn.fhir.jpa.mdm.svc.GoldenResourceMergerSvcImpl; -import ca.uhn.fhir.jpa.mdm.svc.MdmResourceDaoSvc; -import ca.uhn.fhir.jpa.mdm.svc.MdmResourceFilteringSvc; -import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmCandidateSearchCriteriaBuilderSvc; -import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmCandidateSearchSvc; -import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc; -import ca.uhn.fhir.jpa.mdm.svc.candidate.FindCandidateByEidSvc; -import ca.uhn.fhir.jpa.mdm.svc.candidate.FindCandidateByLinkSvc; -import ca.uhn.fhir.jpa.mdm.svc.candidate.FindCandidateByExampleSvc; -import ca.uhn.fhir.rest.server.util.ISearchParamRetriever; -import ca.uhn.fhir.validation.IResourceLoader; -import org.slf4j.Logger; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class MdmConsumerConfig { - private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); - - @Bean - IMdmStorageInterceptor mdmStorageInterceptor() { - return new MdmStorageInterceptor(); - } - - @Bean - MdmQueueConsumerLoader mdmQueueConsumerLoader() { - return new MdmQueueConsumerLoader(); - } - - @Bean - MdmMessageHandler mdmMessageHandler() { - return new MdmMessageHandler(); - } - - @Bean - MdmMatchLinkSvc mdmMatchLinkSvc() { - return new MdmMatchLinkSvc(); - } - - @Bean - MdmEidUpdateService eidUpdateService() { - return new MdmEidUpdateService(); - } - - @Bean - MdmResourceDaoSvc mdmResourceDaoSvc() { - return new MdmResourceDaoSvc(); - } - - @Bean - IMdmLinkSvc mdmLinkSvc() { - return new MdmLinkSvcImpl(); - } - - @Bean - GoldenResourceHelper goldenResourceHelper(FhirContext theFhirContext) { - return new GoldenResourceHelper(theFhirContext); - } - - @Bean - MessageHelper messageHelper(IMdmSettings theMdmSettings, FhirContext theFhirContext) { - return new MessageHelper(theMdmSettings, theFhirContext); - } - - @Bean - MdmSubscriptionLoader mdmSubscriptionLoader() { - return new MdmSubscriptionLoader(); - } - - @Bean - MdmGoldenResourceFindingSvc mdmGoldenResourceFindingSvc() { - return new MdmGoldenResourceFindingSvc(); - } - - @Bean - FindCandidateByEidSvc findCandidateByEidSvc() { - return new FindCandidateByEidSvc(); - } - - @Bean - FindCandidateByLinkSvc findCandidateByLinkSvc() { - return new FindCandidateByLinkSvc(); - } - - @Bean - FindCandidateByExampleSvc findCandidateByScoreSvc() { - return new FindCandidateByExampleSvc(); - } - - @Bean - MdmProviderLoader mdmProviderLoader() { - return new MdmProviderLoader(); - } - - @Bean - MdmRuleValidator mdmRuleValidator(FhirContext theFhirContext, ISearchParamRetriever theSearchParamRetriever) { - return new MdmRuleValidator(theFhirContext, theSearchParamRetriever); - } - - @Bean - IMdmMatchFinderSvc mdmMatchFinderSvc() { - return new MdmMatchFinderSvcImpl(); - } - - @Bean - IGoldenResourceMergerSvc mdmGoldenResourceMergerSvc() { - return new GoldenResourceMergerSvcImpl(); - } - - - @Bean - IMdmLinkQuerySvc mdmLinkQuerySvc() { - return new MdmLinkQuerySvcImpl(); - } - - @Bean - IMdmExpungeSvc mdmResetSvc(MdmLinkDaoSvc theMdmLinkDaoSvc, MdmGoldenResourceDeletingSvc theDeletingSvc, IMdmSettings theIMdmSettings) { - return new MdmClearSvcImpl(theMdmLinkDaoSvc, theDeletingSvc, theIMdmSettings); - } - - @Bean - MdmCandidateSearchSvc mdmCandidateSearchSvc() { - return new MdmCandidateSearchSvc(); - } - - @Bean - MdmCandidateSearchCriteriaBuilderSvc mdmCriteriaBuilderSvc() { - return new MdmCandidateSearchCriteriaBuilderSvc(); - } - - @Bean - MdmResourceMatcherSvc mdmResourceComparatorSvc(FhirContext theFhirContext, IMdmSettings theMdmSettings) { - return new MdmResourceMatcherSvc(theFhirContext, theMdmSettings); - } - - @Bean - EIDHelper eidHelper(FhirContext theFhirContext, IMdmSettings theMdmSettings) { - return new EIDHelper(theFhirContext, theMdmSettings); - } - - @Bean - MdmLinkDaoSvc mdmLinkDaoSvc() { - return new MdmLinkDaoSvc(); - } - - @Bean - MdmLinkFactory mdmLinkFactory(IMdmSettings theMdmSettings) { - return new MdmLinkFactory(theMdmSettings); - } - - @Bean - IMdmLinkUpdaterSvc mdmLinkUpdaterSvc() { - return new MdmLinkUpdaterSvcImpl(); - } - - @Bean - MdmLoader mdmLoader() { - return new MdmLoader(); - } - - @Bean - MdmLinkDeleteSvc mdmLinkDeleteSvc() { - return new MdmLinkDeleteSvc(); - } - - @Bean - MdmResourceFilteringSvc mdmResourceFilteringSvc() { - return new MdmResourceFilteringSvc(); - } - - @Bean - MdmControllerHelper mdmProviderHelper(FhirContext theFhirContext, IResourceLoader theResourceLoader, IMdmSettings theMdmSettings, MessageHelper messageHelper) { - return new MdmControllerHelper(theFhirContext, theResourceLoader, theMdmSettings, messageHelper); - } - - @Bean - IMdmControllerSvc mdmControllerSvc() { - return new MdmControllerSvcImpl(); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmSubmitterConfig.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmSubmitterConfig.java deleted file mode 100644 index 1b02b4e63f1..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/config/MdmSubmitterConfig.java +++ /dev/null @@ -1,78 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.config; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.IMdmChannelSubmitterSvc; -import ca.uhn.fhir.mdm.api.IMdmSettings; -import ca.uhn.fhir.mdm.api.IMdmSubmitSvc; -import ca.uhn.fhir.mdm.rules.config.MdmRuleValidator; -import ca.uhn.fhir.jpa.dao.mdm.MdmLinkDeleteSvc; -import ca.uhn.fhir.jpa.mdm.interceptor.MdmSubmitterInterceptorLoader; -import ca.uhn.fhir.jpa.mdm.svc.MdmChannelSubmitterSvcImpl; -import ca.uhn.fhir.jpa.mdm.svc.MdmGoldenResourceDeletingSvc; -import ca.uhn.fhir.jpa.mdm.svc.MdmSearchParamSvc; -import ca.uhn.fhir.jpa.mdm.svc.MdmSubmitSvcImpl; -import ca.uhn.fhir.jpa.subscription.channel.api.IChannelFactory; -import ca.uhn.fhir.mdm.util.MessageHelper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Lazy; - -@Configuration -public class MdmSubmitterConfig { - - @Bean - MdmSubmitterInterceptorLoader mdmSubmitterInterceptorLoader() { - return new MdmSubmitterInterceptorLoader(); - } - - @Bean - MdmSearchParamSvc mdmSearchParamSvc() { - return new MdmSearchParamSvc(); - } - - @Bean - MdmRuleValidator mdmRuleValidator(FhirContext theFhirContext) { - return new MdmRuleValidator(theFhirContext, mdmSearchParamSvc()); - } - - @Bean - MdmLinkDeleteSvc mdmLinkDeleteSvc() { - return new MdmLinkDeleteSvc(); - } - - @Bean - MdmGoldenResourceDeletingSvc mdmGoldenResourceDeletingSvc() { - return new MdmGoldenResourceDeletingSvc(); - } - - @Bean - @Lazy - IMdmChannelSubmitterSvc mdmChannelSubmitterSvc(FhirContext theFhirContext, IChannelFactory theChannelFactory) { - return new MdmChannelSubmitterSvcImpl(theFhirContext, theChannelFactory); - } - - @Bean - IMdmSubmitSvc mdmBatchService(IMdmSettings theMdmSetting) { - return new MdmSubmitSvcImpl(); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvc.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvc.java deleted file mode 100644 index 08b4268c354..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvc.java +++ /dev/null @@ -1,338 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.dao; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao; -import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchOutcome; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.log.Logs; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.slf4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Example; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; - -public class MdmLinkDaoSvc { - - private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); - - @Autowired - private IMdmLinkDao myMdmLinkDao; - @Autowired - private MdmLinkFactory myMdmLinkFactory; - @Autowired - private IdHelperService myIdHelperService; - @Autowired - private FhirContext myFhirContext; - - @Transactional - public MdmLink createOrUpdateLinkEntity(IBaseResource theGoldenResource, IBaseResource theSourceResource, MdmMatchOutcome theMatchOutcome, MdmLinkSourceEnum theLinkSource, @Nullable MdmTransactionContext theMdmTransactionContext) { - Long goldenResourcePid = myIdHelperService.getPidOrNull(theGoldenResource); - Long sourceResourcePid = myIdHelperService.getPidOrNull(theSourceResource); - - MdmLink mdmLink = getOrCreateMdmLinkByGoldenResourcePidAndSourceResourcePid(goldenResourcePid, sourceResourcePid); - mdmLink.setLinkSource(theLinkSource); - mdmLink.setMatchResult(theMatchOutcome.getMatchResultEnum()); - // Preserve these flags for link updates - mdmLink.setEidMatch(theMatchOutcome.isEidMatch() | mdmLink.isEidMatch()); - mdmLink.setHadToCreateNewGoldenResource(theMatchOutcome.isCreatedNewResource() | mdmLink.getHadToCreateNewGoldenResource()); - mdmLink.setMdmSourceType(myFhirContext.getResourceType(theSourceResource)); - if (mdmLink.getScore() != null) { - mdmLink.setScore(Math.max(theMatchOutcome.score, mdmLink.getScore())); - } else { - mdmLink.setScore(theMatchOutcome.score); - } - - String message = String.format("Creating MdmLink from %s to %s -> %s", theGoldenResource.getIdElement().toUnqualifiedVersionless(), theSourceResource.getIdElement().toUnqualifiedVersionless(), theMatchOutcome); - theMdmTransactionContext.addTransactionLogMessage(message); - ourLog.debug(message); - save(mdmLink); - return mdmLink; - } - - @Nonnull - public MdmLink getOrCreateMdmLinkByGoldenResourcePidAndSourceResourcePid(Long theGoldenResourcePid, Long theSourceResourcePid) { - Optional oExisting = getLinkByGoldenResourcePidAndSourceResourcePid(theGoldenResourcePid, theSourceResourcePid); - if (oExisting.isPresent()) { - return oExisting.get(); - } else { - MdmLink newLink = myMdmLinkFactory.newMdmLink(); - newLink.setGoldenResourcePid(theGoldenResourcePid); - newLink.setSourcePid(theSourceResourcePid); - return newLink; - } - } - - public Optional getLinkByGoldenResourcePidAndSourceResourcePid(Long theGoldenResourcePid, Long theSourceResourcePid) { - if (theSourceResourcePid == null || theGoldenResourcePid == null) { - return Optional.empty(); - } - MdmLink link = myMdmLinkFactory.newMdmLink(); - link.setSourcePid(theSourceResourcePid); - link.setGoldenResourcePid(theGoldenResourcePid); - Example example = Example.of(link); - return myMdmLinkDao.findOne(example); - } - - /** - * Given a source resource Pid, and a match result, return all links that match these criteria. - * - * @param theSourcePid the source of the relationship. - * @param theMatchResult the Match Result of the relationship - * @return a list of {@link MdmLink} entities matching these criteria. - */ - public List getMdmLinksBySourcePidAndMatchResult(Long theSourcePid, MdmMatchResultEnum theMatchResult) { - MdmLink exampleLink = myMdmLinkFactory.newMdmLink(); - exampleLink.setSourcePid(theSourcePid); - exampleLink.setMatchResult(theMatchResult); - Example example = Example.of(exampleLink); - return myMdmLinkDao.findAll(example); - } - - /** - * Given a source Pid, return its Matched {@link MdmLink}. There can only ever be at most one of these, but its possible - * the source has no matches, and may return an empty optional. - * - * @param theSourcePid The Pid of the source you wish to find the matching link for. - * @return the {@link MdmLink} that contains the Match information for the source. - */ - public Optional getMatchedLinkForSourcePid(Long theSourcePid) { - MdmLink exampleLink = myMdmLinkFactory.newMdmLink(); - exampleLink.setSourcePid(theSourcePid); - exampleLink.setMatchResult(MdmMatchResultEnum.MATCH); - Example example = Example.of(exampleLink); - return myMdmLinkDao.findOne(example); - } - - /** - * Given an IBaseResource, return its Matched {@link MdmLink}. There can only ever be at most one of these, but its possible - * the source has no matches, and may return an empty optional. - * - * @param theSourceResource The IBaseResource representing the source you wish to find the matching link for. - * @return the {@link MdmLink} that contains the Match information for the source. - */ - public Optional getMatchedLinkForSource(IBaseResource theSourceResource) { - Long pid = myIdHelperService.getPidOrNull(theSourceResource); - if (pid == null) { - return Optional.empty(); - } - - MdmLink exampleLink = myMdmLinkFactory.newMdmLink(); - exampleLink.setSourcePid(pid); - exampleLink.setMatchResult(MdmMatchResultEnum.MATCH); - Example example = Example.of(exampleLink); - return myMdmLinkDao.findOne(example); - } - - /** - * Given a golden resource a source and a match result, return the matching {@link MdmLink}, if it exists. - * - * @param theGoldenResourcePid The Pid of the Golden Resource in the relationship - * @param theSourcePid The Pid of the source in the relationship - * @param theMatchResult The MatchResult you are looking for. - * @return an Optional {@link MdmLink} containing the matched link if it exists. - */ - public Optional getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(Long theGoldenResourcePid, - Long theSourcePid, MdmMatchResultEnum theMatchResult) { - MdmLink exampleLink = myMdmLinkFactory.newMdmLink(); - exampleLink.setGoldenResourcePid(theGoldenResourcePid); - exampleLink.setSourcePid(theSourcePid); - exampleLink.setMatchResult(theMatchResult); - Example example = Example.of(exampleLink); - return myMdmLinkDao.findOne(example); - } - - /** - * Get all {@link MdmLink} which have {@link MdmMatchResultEnum#POSSIBLE_DUPLICATE} as their match result. - * - * @return A list of {@link MdmLink} that hold potential duplicate golden resources. - */ - public List getPossibleDuplicates() { - MdmLink exampleLink = myMdmLinkFactory.newMdmLink(); - exampleLink.setMatchResult(MdmMatchResultEnum.POSSIBLE_DUPLICATE); - Example example = Example.of(exampleLink); - return myMdmLinkDao.findAll(example); - } - - public Optional findMdmLinkBySource(IBaseResource theSourceResource) { - @Nullable Long pid = myIdHelperService.getPidOrNull(theSourceResource); - if (pid == null) { - return Optional.empty(); - } - MdmLink exampleLink = myMdmLinkFactory.newMdmLink().setSourcePid(pid); - Example example = Example.of(exampleLink); - return myMdmLinkDao.findOne(example); - } - - /** - * Delete a given {@link MdmLink}. Note that this does not clear out the Golden resource. - * It is a simple entity delete. - * - * @param theMdmLink the {@link MdmLink} to delete. - */ - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void deleteLink(MdmLink theMdmLink) { - myMdmLinkDao.delete(theMdmLink); - } - - /** - * Given a Golden Resource, return all links in which they are the source Golden Resource of the {@link MdmLink} - * - * @param theGoldenResource The {@link IBaseResource} Golden Resource who's links you would like to retrieve. - * @return A list of all {@link MdmLink} entities in which theGoldenResource is the source Golden Resource - */ - public List findMdmLinksByGoldenResource(IBaseResource theGoldenResource) { - Long pid = myIdHelperService.getPidOrNull(theGoldenResource); - if (pid == null) { - return Collections.emptyList(); - } - MdmLink exampleLink = myMdmLinkFactory.newMdmLink().setGoldenResourcePid(pid); - Example example = Example.of(exampleLink); - return myMdmLinkDao.findAll(example); - } - - /** - * Delete all {@link MdmLink} entities, and return all resource PIDs from the source of the relationship. - * - * @return A list of Long representing the related Golden Resource Pids. - */ - @Transactional - public List deleteAllMdmLinksAndReturnGoldenResourcePids() { - List all = myMdmLinkDao.findAll(); - return deleteMdmLinksAndReturnGoldenResourcePids(all); - } - - private List deleteMdmLinksAndReturnGoldenResourcePids(List theLinks) { - Set goldenResources = theLinks.stream().map(MdmLink::getGoldenResourcePid).collect(Collectors.toSet()); - //TODO GGG this is probably invalid... we are essentially looking for GOLDEN -> GOLDEN links, which are either POSSIBLE_DUPLICATE - //and REDIRECT - goldenResources.addAll(theLinks.stream() - .filter(link -> link.getMatchResult().equals(MdmMatchResultEnum.REDIRECT) - || link.getMatchResult().equals(MdmMatchResultEnum.POSSIBLE_DUPLICATE)) - .map(MdmLink::getSourcePid).collect(Collectors.toSet())); - ourLog.info("Deleting {} MDM link records...", theLinks.size()); - myMdmLinkDao.deleteAll(theLinks); - ourLog.info("{} MDM link records deleted", theLinks.size()); - return new ArrayList<>(goldenResources); - } - - /** - * Given a valid {@link String}, delete all {@link MdmLink} entities for that type, and get the Pids - * for the Golden Resources which were the sources of the links. - * - * @param theSourceType the type of relationship you would like to delete. - * @return A list of longs representing the Pids of the Golden Resources resources used as the sources of the relationships that were deleted. - */ - public List deleteAllMdmLinksOfTypeAndReturnGoldenResourcePids(String theSourceType) { - MdmLink link = new MdmLink(); - link.setMdmSourceType(theSourceType); - Example exampleLink = Example.of(link); - List allOfType = myMdmLinkDao.findAll(exampleLink); - return deleteMdmLinksAndReturnGoldenResourcePids(allOfType); - } - - /** - * Persist an MDM link to the database. - * - * @param theMdmLink the link to save. - * @return the persisted {@link MdmLink} entity. - */ - public MdmLink save(MdmLink theMdmLink) { - if (theMdmLink.getCreated() == null) { - theMdmLink.setCreated(new Date()); - } - theMdmLink.setUpdated(new Date()); - return myMdmLinkDao.save(theMdmLink); - } - - - /** - * Given an example {@link MdmLink}, return all links from the database which match the example. - * - * @param theExampleLink The MDM link containing the data we would like to search for. - * @return a list of {@link MdmLink} entities which match the example. - */ - public List findMdmLinkByExample(Example theExampleLink) { - return myMdmLinkDao.findAll(theExampleLink); - } - - /** - * Given a source {@link IBaseResource}, return all {@link MdmLink} entities in which this source is the source - * of the relationship. This will show you all links for a given Patient/Practitioner. - * - * @param theSourceResource the source resource to find links for. - * @return all links for the source. - */ - public List findMdmLinksBySourceResource(IBaseResource theSourceResource) { - Long pid = myIdHelperService.getPidOrNull(theSourceResource); - if (pid == null) { - return Collections.emptyList(); - } - MdmLink exampleLink = myMdmLinkFactory.newMdmLink().setSourcePid(pid); - Example example = Example.of(exampleLink); - return myMdmLinkDao.findAll(example); - } - - /** - * Finds all {@link MdmLink} entities in which theGoldenResource's PID is the source - * of the relationship. - * - * @param theGoldenResource the source resource to find links for. - * @return all links for the source. - */ - public List findMdmMatchLinksByGoldenResource(IBaseResource theGoldenResource) { - Long pid = myIdHelperService.getPidOrNull(theGoldenResource); - if (pid == null) { - return Collections.emptyList(); - } - MdmLink exampleLink = myMdmLinkFactory.newMdmLink().setGoldenResourcePid(pid); - exampleLink.setMatchResult(MdmMatchResultEnum.MATCH); - Example example = Example.of(exampleLink); - return myMdmLinkDao.findAll(example); - } - - /** - * Factory delegation method, whenever you need a new MdmLink, use this factory method. - * //TODO Should we make the constructor private for MdmLink? or work out some way to ensure they can only be instantiated via factory. - * - * @return A new {@link MdmLink}. - */ - public MdmLink newMdmLink() { - return myMdmLinkFactory.newMdmLink(); - } - -} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptor.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptor.java deleted file mode 100644 index 52f9ae3bc16..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptor.java +++ /dev/null @@ -1,191 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.interceptor; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.MdmConstants; -import ca.uhn.fhir.mdm.api.IMdmSettings; -import ca.uhn.fhir.mdm.model.CanonicalEID; -import ca.uhn.fhir.mdm.util.EIDHelper; -import ca.uhn.fhir.mdm.util.MdmResourceUtil; -import ca.uhn.fhir.mdm.util.GoldenResourceHelper; -import ca.uhn.fhir.interceptor.api.Hook; -import ca.uhn.fhir.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.dao.mdm.MdmLinkDeleteSvc; -import ca.uhn.fhir.jpa.dao.expunge.ExpungeEverythingService; -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -@Service -public class MdmStorageInterceptor implements IMdmStorageInterceptor { - private static final Logger ourLog = LoggerFactory.getLogger(MdmStorageInterceptor.class); - @Autowired - private ExpungeEverythingService myExpungeEverythingService; - @Autowired - private MdmLinkDeleteSvc myMdmLinkDeleteSvc; - @Autowired - private FhirContext myFhirContext; - @Autowired - private EIDHelper myEIDHelper; - @Autowired - private IMdmSettings myMdmSettings; - @Autowired - private GoldenResourceHelper myGoldenResourceHelper; - - - @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED) - public void blockManualResourceManipulationOnCreate(IBaseResource theBaseResource, RequestDetails theRequestDetails, ServletRequestDetails theServletRequestDetails) { - - //If running in single EID mode, forbid multiple eids. - if (myMdmSettings.isPreventMultipleEids()) { - forbidIfHasMultipleEids(theBaseResource); - } - - // TODO GGG MDM find a better way to identify internal calls? - if (isInternalRequest(theRequestDetails)) { - return; - } - - forbidIfMdmManagedTagIsPresent(theBaseResource); - } - - @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED) - public void blockManualGoldenResourceManipulationOnUpdate(IBaseResource theOldResource, IBaseResource theUpdatedResource, RequestDetails theRequestDetails, ServletRequestDetails theServletRequestDetails) { - //If running in single EID mode, forbid multiple eids. - if (myMdmSettings.isPreventMultipleEids()) { - forbidIfHasMultipleEids(theUpdatedResource); - } - - if (MdmResourceUtil.isGoldenRecordRedirected(theUpdatedResource)) { - ourLog.debug("Deleting MDM links to deactivated Golden resource {}", theUpdatedResource.getIdElement().toUnqualifiedVersionless()); - int deleted = myMdmLinkDeleteSvc.deleteNonRedirectWithAnyReferenceTo(theUpdatedResource); - if (deleted > 0) { - ourLog.debug("Deleted {} MDM links", deleted); - } - } - - if (isInternalRequest(theRequestDetails)) { - return; - } - forbidIfMdmManagedTagIsPresent(theOldResource); - forbidModifyingMdmTag(theUpdatedResource, theOldResource); - - if (myMdmSettings.isPreventEidUpdates()) { - forbidIfModifyingExternalEidOnTarget(theUpdatedResource, theOldResource); - } - } - - @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED) - public void deleteMdmLinks(RequestDetails theRequest, IBaseResource theResource) { - if (!myMdmSettings.isSupportedMdmType(myFhirContext.getResourceType(theResource))) { - return; - } - myMdmLinkDeleteSvc.deleteWithAnyReferenceTo(theResource); - } - - private void forbidIfModifyingExternalEidOnTarget(IBaseResource theNewResource, IBaseResource theOldResource) { - List newExternalEids = myEIDHelper.getExternalEid(theNewResource); - List oldExternalEids = myEIDHelper.getExternalEid(theOldResource); - if (oldExternalEids.isEmpty()) { - return; - } - - if (!myEIDHelper.eidMatchExists(newExternalEids, oldExternalEids)) { - throwBlockEidChange(); - } - } - - private void throwBlockEidChange() { - throw new ForbiddenOperationException("While running with EID updates disabled, EIDs may not be updated on source resources"); - } - - /* - * Will throw a forbidden error if a request attempts to add/remove the MDM tag on a Resource. - */ - private void forbidModifyingMdmTag(IBaseResource theNewResource, IBaseResource theOldResource) { - if (MdmResourceUtil.isMdmManaged(theNewResource) != MdmResourceUtil.isMdmManaged(theOldResource)) { - throwBlockMdmManagedTagChange(); - } - } - - private void forbidIfHasMultipleEids(IBaseResource theResource) { - String resourceType = extractResourceType(theResource); - if (myMdmSettings.isSupportedMdmType(resourceType)) { - if (myEIDHelper.getExternalEid(theResource).size() > 1) { - throwBlockMultipleEids(); - } - } - } - - /* - * We assume that if we have RequestDetails, then this was an HTTP request and not an internal one. - */ - private boolean isInternalRequest(RequestDetails theRequestDetails) { - return theRequestDetails == null; - } - - private void forbidIfMdmManagedTagIsPresent(IBaseResource theResource) { - if (MdmResourceUtil.isMdmManaged(theResource)) { - throwModificationBlockedByMdm(); - } - if (MdmResourceUtil.hasGoldenRecordSystemTag(theResource)) { - throwModificationBlockedByMdm(); - } - } - - private void throwBlockMdmManagedTagChange() { - throw new ForbiddenOperationException("The " + MdmConstants.CODE_HAPI_MDM_MANAGED + " tag on a resource may not be changed once created."); - } - - private void throwModificationBlockedByMdm() { - throw new ForbiddenOperationException("Cannot create or modify Resources that are managed by MDM. This resource contains a tag with one of these systems: " + MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS + " or " + MdmConstants.SYSTEM_MDM_MANAGED); - } - - private void throwBlockMultipleEids() { - throw new ForbiddenOperationException("While running with multiple EIDs disabled, source resources may have at most one EID."); - } - - private String extractResourceType(IBaseResource theResource) { - return myFhirContext.getResourceType(theResource); - } - - @Hook(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING) - public void expungeAllMdmLinks(AtomicInteger theCounter) { - ourLog.debug("Expunging all MdmLink records"); - theCounter.addAndGet(myExpungeEverythingService.expungeEverythingByType(MdmLink.class)); - } - - @Hook(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE) - public void expungeAllMatchedMdmLinks(AtomicInteger theCounter, IBaseResource theResource) { - ourLog.debug("Expunging MdmLink records with reference to {}", theResource.getIdElement()); - theCounter.addAndGet(myMdmLinkDeleteSvc.deleteWithAnyReferenceTo(theResource)); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/GoldenResourceMergerSvcImpl.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/GoldenResourceMergerSvcImpl.java deleted file mode 100644 index 3304c140946..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/GoldenResourceMergerSvcImpl.java +++ /dev/null @@ -1,164 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.api.IMdmLinkSvc; -import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc; -import ca.uhn.fhir.mdm.log.Logs; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.mdm.util.GoldenResourceHelper; -import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.mdm.util.MdmResourceUtil; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import org.hl7.fhir.instance.model.api.IAnyResource; -import org.slf4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -@Service -public class GoldenResourceMergerSvcImpl implements IGoldenResourceMergerSvc { - - private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); - - @Autowired - GoldenResourceHelper myGoldenResourceHelper; - @Autowired - MdmLinkDaoSvc myMdmLinkDaoSvc; - @Autowired - IMdmLinkSvc myMdmLinkSvc; - @Autowired - IdHelperService myIdHelperService; - @Autowired - MdmResourceDaoSvc myMdmResourceDaoSvc; - - @Override - @Transactional - public IAnyResource mergeGoldenResources(IAnyResource theFromGoldenResource, IAnyResource theToGoldenResource, MdmTransactionContext theMdmTransactionContext) { - Long fromGoldenResourcePid = myIdHelperService.getPidOrThrowException(theFromGoldenResource); - Long toGoldenResourcePid = myIdHelperService.getPidOrThrowException(theToGoldenResource); - String resourceType = theMdmTransactionContext.getResourceType(); - - //Merge attributes, to be determined when survivorship is solved. - myGoldenResourceHelper.mergeFields(theFromGoldenResource, theToGoldenResource); - - //Merge the links from the FROM to the TO resource. Clean up dangling links. - mergeGoldenResourceLinks(theFromGoldenResource, theToGoldenResource, toGoldenResourcePid, theMdmTransactionContext); - - //Create the new REDIRECT link - addMergeLink(toGoldenResourcePid, fromGoldenResourcePid, resourceType); - - //Strip the golden resource tag from the now-deprecated resource. - myMdmResourceDaoSvc.removeGoldenResourceTag(theFromGoldenResource, resourceType); - - //Add the REDIRECT tag to that same deprecated resource. - MdmResourceUtil.setGoldenResourceRedirected(theFromGoldenResource); - - //Save the deprecated resource. - myMdmResourceDaoSvc.upsertGoldenResource(theFromGoldenResource, resourceType); - - log(theMdmTransactionContext, "Merged " + theFromGoldenResource.getIdElement().toVersionless() + " into " + theToGoldenResource.getIdElement().toVersionless()); - return theToGoldenResource; - } - - private void addMergeLink(Long theGoldenResourcePidAkaActive, Long theTargetResourcePidAkaDeactivated, String theResourceType) { - MdmLink mdmLink = myMdmLinkDaoSvc - .getOrCreateMdmLinkByGoldenResourcePidAndSourceResourcePid(theGoldenResourcePidAkaActive, theTargetResourcePidAkaDeactivated); - - mdmLink - .setMdmSourceType(theResourceType) - .setMatchResult(MdmMatchResultEnum.REDIRECT) - .setLinkSource(MdmLinkSourceEnum.MANUAL); - myMdmLinkDaoSvc.save(mdmLink); - } - - - /** - * Helper method which performs merger of links between resources, and cleans up dangling links afterwards. - * - * For each incomingLink, either ignore it, move it, or replace the original one - * 1. If the link already exists on the TO resource, and it is an automatic link, ignore the link, and subsequently delete it. - * 2. If the link does not exist on the TO resource, redirect the link from the FROM resource to the TO resource - * 3. If an incoming link is MANUAL, and theres a matching link on the FROM resource which is AUTOMATIC, the manual link supercedes the automatic one. - * 4. Manual link collisions cause invalid request exception. - * - * @param theFromResource - * @param theToResource - * @param theToResourcePid - * @param theMdmTransactionContext - */ - private void mergeGoldenResourceLinks(IAnyResource theFromResource, IAnyResource theToResource, Long theToResourcePid, MdmTransactionContext theMdmTransactionContext) { - List fromLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theFromResource); // fromLinks - links going to theFromResource - List toLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theToResource); // toLinks - links going to theToResource - List toDelete = new ArrayList<>(); - - for (MdmLink fromLink : fromLinks) { - Optional optionalToLink = findFirstLinkWithMatchingSource(toLinks, fromLink); - if (optionalToLink.isPresent()) { - - // The original links already contain this target, so move it over to the toResource - MdmLink toLink = optionalToLink.get(); - if (fromLink.isManual()) { - switch (toLink.getLinkSource()) { - case AUTO: - //3 - log(theMdmTransactionContext, String.format("MANUAL overrides AUT0. Deleting link %s", toLink.toString())); - myMdmLinkDaoSvc.deleteLink(toLink); - break; - case MANUAL: - if (fromLink.getMatchResult() != toLink.getMatchResult()) { - throw new InvalidRequestException("A MANUAL " + fromLink.getMatchResult() + " link may not be merged into a MANUAL " + toLink.getMatchResult() + " link for the same target"); - } - } - } else { - //1 - toDelete.add(fromLink); - continue; - } - } - //2 The original TO links didn't contain this target, so move it over to the toGoldenResource - fromLink.setGoldenResourcePid(theToResourcePid); - ourLog.trace("Saving link {}", fromLink); - myMdmLinkDaoSvc.save(fromLink); - } - //1 Delete dangling links - toDelete.forEach(link -> myMdmLinkDaoSvc.deleteLink(link)); - } - - private Optional findFirstLinkWithMatchingSource(List theMdmLinks, MdmLink theLinkWithSourceToMatch) { - return theMdmLinks.stream() - .filter(mdmLink -> mdmLink.getSourcePid().equals(theLinkWithSourceToMatch.getSourcePid())) - .findFirst(); - } - - private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) { - theMdmTransactionContext.addTransactionLogMessage(theMessage); - ourLog.debug(theMessage); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmClearSvcImpl.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmClearSvcImpl.java deleted file mode 100644 index 9142c517c93..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmClearSvcImpl.java +++ /dev/null @@ -1,84 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.api.IMdmExpungeSvc; -import ca.uhn.fhir.mdm.api.IMdmSettings; -import ca.uhn.fhir.mdm.log.Logs; -import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.provider.ProviderConstants; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import org.slf4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.List; - -/** - * This class is responsible for clearing out existing MDM links, as well as deleting all Golden Resources related to those MDM Links. - */ -public class MdmClearSvcImpl implements IMdmExpungeSvc { - private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); - - final MdmLinkDaoSvc myMdmLinkDaoSvc; - final MdmGoldenResourceDeletingSvc myMdmGoldenResourceDeletingSvcImpl; - final IMdmSettings myMdmSettings; - - @Autowired - public MdmClearSvcImpl(MdmLinkDaoSvc theMdmLinkDaoSvc, MdmGoldenResourceDeletingSvc theMdmGoldenResourceDeletingSvcImpl, IMdmSettings theIMdmSettings) { - myMdmLinkDaoSvc = theMdmLinkDaoSvc; - myMdmGoldenResourceDeletingSvcImpl = theMdmGoldenResourceDeletingSvcImpl; - myMdmSettings = theIMdmSettings; - } - - @Override - public long expungeAllMdmLinksOfSourceType(String theSourceResourceType, ServletRequestDetails theRequestDetails) { - throwExceptionIfInvalidSourceResourceType(theSourceResourceType); - ourLog.info("Clearing all MDM Links for resource type {}...", theSourceResourceType); - List goldenResourcePids = myMdmLinkDaoSvc.deleteAllMdmLinksOfTypeAndReturnGoldenResourcePids(theSourceResourceType); - DeleteMethodOutcome deleteOutcome = myMdmGoldenResourceDeletingSvcImpl.expungeGoldenResourcePids(goldenResourcePids, theSourceResourceType, theRequestDetails); - ourLog.info("MDM clear operation complete. Removed {} MDM links and {} Golden Resources.", goldenResourcePids.size(), deleteOutcome.getExpungedResourcesCount()); - return goldenResourcePids.size(); - } - - private void throwExceptionIfInvalidSourceResourceType(String theResourceType) { - if (!myMdmSettings.isSupportedMdmType(theResourceType)) { - throw new InvalidRequestException(ProviderConstants.MDM_CLEAR + " does not support resource type: " + theResourceType); - } - } - - @Override - public long expungeAllMdmLinks(ServletRequestDetails theRequestDetails) { - ourLog.info("Clearing all MDM Links..."); - long retVal = 0; - - for(String mdmType : myMdmSettings.getMdmRules().getMdmTypes()) { - List goldenResourcePids = myMdmLinkDaoSvc.deleteAllMdmLinksAndReturnGoldenResourcePids(); - DeleteMethodOutcome deleteOutcome = myMdmGoldenResourceDeletingSvcImpl.expungeGoldenResourcePids(goldenResourcePids, null, theRequestDetails); - ourLog.info("MDM clear operation on type {} complete. Removed {} MDM links and expunged {} Golden resources.", mdmType, goldenResourcePids.size(), deleteOutcome.getExpungedResourcesCount()); - retVal += goldenResourcePids.size(); - } - ourLog.info("MDM clear completed expunged with a total of {} golden resources cleared.", retVal); - return retVal; - } -} - diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmControllerSvcImpl.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmControllerSvcImpl.java deleted file mode 100644 index ffd93ef3fa9..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmControllerSvcImpl.java +++ /dev/null @@ -1,100 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.api.MdmLinkJson; -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.api.IMdmControllerSvc; -import ca.uhn.fhir.mdm.api.IMdmLinkQuerySvc; -import ca.uhn.fhir.mdm.api.IMdmLinkUpdaterSvc; -import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.mdm.provider.MdmControllerHelper; -import ca.uhn.fhir.mdm.provider.MdmControllerUtil; -import ca.uhn.fhir.rest.server.provider.ProviderConstants; -import org.hl7.fhir.instance.model.api.IAnyResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.jetbrains.annotations.Nullable; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.stream.Stream; - -/** - * This class acts as a layer between MdmProviders and MDM services to support a REST API that's not a FHIR Operation API. - */ -@Service -public class MdmControllerSvcImpl implements IMdmControllerSvc { - @Autowired - MdmControllerHelper myMdmControllerHelper; - @Autowired - IGoldenResourceMergerSvc myGoldenResourceMergerSvc; - @Autowired - IMdmLinkQuerySvc myMdmLinkQuerySvc; - @Autowired - IMdmLinkUpdaterSvc myIMdmLinkUpdaterSvc; - - @Override - public IAnyResource mergeGoldenResources(String theFromGoldenResourceId, String theToGoldenResourceId, MdmTransactionContext theMdmTransactionContext) { - IAnyResource fromGoldenResource = myMdmControllerHelper.getLatestGoldenResourceFromIdOrThrowException(ProviderConstants.MDM_MERGE_GR_FROM_GOLDEN_RESOURCE_ID, theFromGoldenResourceId); - IAnyResource toGoldenResource = myMdmControllerHelper.getLatestGoldenResourceFromIdOrThrowException(ProviderConstants.MDM_MERGE_GR_TO_GOLDEN_RESOURCE_ID, theToGoldenResourceId); - myMdmControllerHelper.validateMergeResources(fromGoldenResource, toGoldenResource); - myMdmControllerHelper.validateSameVersion(fromGoldenResource, theFromGoldenResourceId); - myMdmControllerHelper.validateSameVersion(toGoldenResource, theToGoldenResourceId); - - return myGoldenResourceMergerSvc.mergeGoldenResources(fromGoldenResource, toGoldenResource, theMdmTransactionContext); - } - - @Override - public Stream queryLinks(@Nullable String theGoldenResourceId, @Nullable String theSourceResourceId, @Nullable String theMatchResult, @Nullable String theLinkSource, MdmTransactionContext theMdmTransactionContext) { - IIdType goldenResourceId = MdmControllerUtil.extractGoldenResourceIdDtOrNull(ProviderConstants.MDM_QUERY_LINKS_GOLDEN_RESOURCE_ID, theGoldenResourceId); - IIdType sourceId = MdmControllerUtil.extractSourceIdDtOrNull(ProviderConstants.MDM_QUERY_LINKS_RESOURCE_ID, theSourceResourceId); - MdmMatchResultEnum matchResult = MdmControllerUtil.extractMatchResultOrNull(theMatchResult); - MdmLinkSourceEnum linkSource = MdmControllerUtil.extractLinkSourceOrNull(theLinkSource); - - return myMdmLinkQuerySvc.queryLinks(goldenResourceId, sourceId, matchResult, linkSource, theMdmTransactionContext); - } - - @Override - public Stream getDuplicateGoldenResources(MdmTransactionContext theMdmTransactionContext) { - return myMdmLinkQuerySvc.getDuplicateGoldenResources(theMdmTransactionContext); - } - - @Override - public IAnyResource updateLink(String theGoldenResourceId, String theSourceResourceId, String theMatchResult, MdmTransactionContext theMdmTransactionContext) { - MdmMatchResultEnum matchResult = MdmControllerUtil.extractMatchResultOrNull(theMatchResult); - IAnyResource goldenResource = myMdmControllerHelper.getLatestGoldenResourceFromIdOrThrowException(ProviderConstants.MDM_UPDATE_LINK_GOLDEN_RESOURCE_ID, theGoldenResourceId); - IAnyResource source = myMdmControllerHelper.getLatestSourceFromIdOrThrowException(ProviderConstants.MDM_UPDATE_LINK_RESOURCE_ID, theSourceResourceId); - myMdmControllerHelper.validateSameVersion(goldenResource, theGoldenResourceId); - myMdmControllerHelper.validateSameVersion(source, theSourceResourceId); - - return myIMdmLinkUpdaterSvc.updateLink(goldenResource, source, matchResult, theMdmTransactionContext); - } - - @Override - public void notDuplicateGoldenResource(String theGoldenResourceId, String theTargetGoldenResourceId, MdmTransactionContext theMdmTransactionContext) { - IAnyResource goldenResource = myMdmControllerHelper.getLatestGoldenResourceFromIdOrThrowException(ProviderConstants.MDM_UPDATE_LINK_GOLDEN_RESOURCE_ID, theGoldenResourceId); - IAnyResource target = myMdmControllerHelper.getLatestGoldenResourceFromIdOrThrowException(ProviderConstants.MDM_UPDATE_LINK_RESOURCE_ID, theTargetGoldenResourceId); - - myIMdmLinkUpdaterSvc.notDuplicateGoldenResource(goldenResource, target, theMdmTransactionContext); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmEidUpdateService.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmEidUpdateService.java deleted file mode 100644 index 70bd9a756e5..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmEidUpdateService.java +++ /dev/null @@ -1,182 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchOutcome; -import ca.uhn.fhir.mdm.api.IMdmLinkSvc; -import ca.uhn.fhir.mdm.api.IMdmSettings; -import ca.uhn.fhir.mdm.log.Logs; -import ca.uhn.fhir.mdm.model.CanonicalEID; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.mdm.util.EIDHelper; -import ca.uhn.fhir.mdm.util.GoldenResourceHelper; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc; -import ca.uhn.fhir.jpa.mdm.svc.candidate.MatchedGoldenResourceCandidate; -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; -import org.hl7.fhir.instance.model.api.IAnyResource; -import org.slf4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Optional; - -@Service -public class MdmEidUpdateService { - - private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); - - @Autowired - private MdmResourceDaoSvc myMdmResourceDaoSvc; - @Autowired - private IMdmLinkSvc myMdmLinkSvc; - @Autowired - private MdmGoldenResourceFindingSvc myMdmGoldenResourceFindingSvc; - @Autowired - private GoldenResourceHelper myGoldenResourceHelper; - @Autowired - private EIDHelper myEIDHelper; - @Autowired - private MdmLinkDaoSvc myMdmLinkDaoSvc; - @Autowired - private IMdmSettings myMdmSettings; - - void handleMdmUpdate(IAnyResource theResource, MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext) { - MdmUpdateContext updateContext = new MdmUpdateContext(theMatchedGoldenResourceCandidate, theResource); - if (updateContext.isRemainsMatchedToSameGoldenResource()) { - // Copy over any new external EIDs which don't already exist. - // TODO NG - Eventually this call will use terser to clone data in, once the surviorship rules for copying data will be confirmed - // myPersonHelper.updatePersonFromUpdatedEmpiTarget(updateContext.getMatchedPerson(), theResource, theEmpiTransactionContext); - if (!updateContext.isIncomingResourceHasAnEid() || updateContext.isHasEidsInCommon()) { - //update to patient that uses internal EIDs only. - myMdmLinkSvc.updateLink(updateContext.getMatchedGoldenResource(), theResource, theMatchedGoldenResourceCandidate.getMatchResult(), MdmLinkSourceEnum.AUTO, theMdmTransactionContext); - } else if (!updateContext.isHasEidsInCommon()) { - handleNoEidsInCommon(theResource, theMatchedGoldenResourceCandidate, theMdmTransactionContext, updateContext); - } - } else { - //This is a new linking scenario. we have to break the existing link and link to the new Golden Resource. For now, we create duplicate. - //updated patient has an EID that matches to a new candidate. Link them, and set the Golden Resources possible duplicates - linkToNewGoldenResourceAndFlagAsDuplicate(theResource, updateContext.getExistingGoldenResource(), updateContext.getMatchedGoldenResource(), theMdmTransactionContext); - } - } - - private void handleNoEidsInCommon(IAnyResource theResource, MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext, MdmUpdateContext theUpdateContext) { - // the user is simply updating their EID. We propagate this change to the GoldenResource. - //overwrite. No EIDS in common, but still same GoldenResource. - if (myMdmSettings.isPreventMultipleEids()) { - if (myMdmLinkDaoSvc.findMdmMatchLinksByGoldenResource(theUpdateContext.getMatchedGoldenResource()).size() <= 1) { // If there is only 0/1 link on the GoldenResource, we can safely overwrite the EID. - handleExternalEidOverwrite(theUpdateContext.getMatchedGoldenResource(), theResource, theMdmTransactionContext); - } else { // If the GoldenResource has multiple targets tied to it, we can't just overwrite the EID, so we split the GoldenResource. - createNewGoldenResourceAndFlagAsDuplicate(theResource, theMdmTransactionContext, theUpdateContext.getExistingGoldenResource()); - } - } else { - myGoldenResourceHelper.handleExternalEidAddition(theUpdateContext.getMatchedGoldenResource(), theResource, theMdmTransactionContext); - } - myMdmLinkSvc.updateLink(theUpdateContext.getMatchedGoldenResource(), theResource, theMatchedGoldenResourceCandidate.getMatchResult(), MdmLinkSourceEnum.AUTO, theMdmTransactionContext); - } - - private void handleExternalEidOverwrite(IAnyResource theGoldenResource, IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) { - List eidFromResource = myEIDHelper.getExternalEid(theResource); - if (!eidFromResource.isEmpty()) { - myGoldenResourceHelper.overwriteExternalEids(theGoldenResource, eidFromResource); - } - } - - private boolean candidateIsSameAsMdmLinkGoldenResource(MdmLink theExistingMatchLink, MatchedGoldenResourceCandidate theGoldenResourceCandidate) { - return theExistingMatchLink.getGoldenResourcePid().equals(theGoldenResourceCandidate.getCandidateGoldenResourcePid().getIdAsLong()); - } - - private void createNewGoldenResourceAndFlagAsDuplicate(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext, IAnyResource theOldGoldenResource) { - log(theMdmTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs."); - IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theResource); - - myMdmLinkSvc.updateLink(newGoldenResource, theResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); - myMdmLinkSvc.updateLink(newGoldenResource, theOldGoldenResource, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); - } - - private void linkToNewGoldenResourceAndFlagAsDuplicate(IAnyResource theResource, IAnyResource theOldGoldenResource, IAnyResource theNewGoldenResource, MdmTransactionContext theMdmTransactionContext) { - log(theMdmTransactionContext, "Changing a match link!"); - myMdmLinkSvc.deleteLink(theOldGoldenResource, theResource, theMdmTransactionContext); - myMdmLinkSvc.updateLink(theNewGoldenResource, theResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); - log(theMdmTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs."); - myMdmLinkSvc.updateLink(theNewGoldenResource, theOldGoldenResource, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); - } - - private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) { - theMdmTransactionContext.addTransactionLogMessage(theMessage); - ourLog.debug(theMessage); - } - - /** - * Data class to hold context surrounding an update operation for an MDM target. - */ - class MdmUpdateContext { - - private final boolean myHasEidsInCommon; - private final boolean myIncomingResourceHasAnEid; - private IAnyResource myExistingGoldenResource; - private boolean myRemainsMatchedToSameGoldenResource; - private final IAnyResource myMatchedGoldenResource; - - public IAnyResource getMatchedGoldenResource() { - return myMatchedGoldenResource; - } - - MdmUpdateContext(MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, IAnyResource theResource) { - final String resourceType = theResource.getIdElement().getResourceType(); - myMatchedGoldenResource = myMdmGoldenResourceFindingSvc.getGoldenResourceFromMatchedGoldenResourceCandidate(theMatchedGoldenResourceCandidate, resourceType); - - myHasEidsInCommon = myEIDHelper.hasEidOverlap(myMatchedGoldenResource, theResource); - myIncomingResourceHasAnEid = !myEIDHelper.getExternalEid(theResource).isEmpty(); - - Optional theExistingMatchLink = myMdmLinkDaoSvc.getMatchedLinkForSource(theResource); - myExistingGoldenResource = null; - - if (theExistingMatchLink.isPresent()) { - MdmLink mdmLink = theExistingMatchLink.get(); - Long existingGoldenResourcePid = mdmLink.getGoldenResourcePid(); - myExistingGoldenResource = myMdmResourceDaoSvc.readGoldenResourceByPid(new ResourcePersistentId(existingGoldenResourcePid), resourceType); - myRemainsMatchedToSameGoldenResource = candidateIsSameAsMdmLinkGoldenResource(mdmLink, theMatchedGoldenResourceCandidate); - } else { - myRemainsMatchedToSameGoldenResource = false; - } - } - - public boolean isHasEidsInCommon() { - return myHasEidsInCommon; - } - - public boolean isIncomingResourceHasAnEid() { - return myIncomingResourceHasAnEid; - } - - public IAnyResource getExistingGoldenResource() { - return myExistingGoldenResource; - } - - public boolean isRemainsMatchedToSameGoldenResource() { - return myRemainsMatchedToSameGoldenResource; - } - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkQuerySvcImpl.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkQuerySvcImpl.java deleted file mode 100644 index 6de2be07136..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkQuerySvcImpl.java +++ /dev/null @@ -1,96 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.api.MdmLinkJson; -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.api.IMdmLinkQuerySvc; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -import ca.uhn.fhir.jpa.entity.MdmLink; -import org.hl7.fhir.instance.model.api.IIdType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Example; - -import java.util.stream.Stream; - -public class MdmLinkQuerySvcImpl implements IMdmLinkQuerySvc { - - private static final Logger ourLog = LoggerFactory.getLogger(MdmLinkQuerySvcImpl.class); - - @Autowired - IdHelperService myIdHelperService; - @Autowired - MdmLinkDaoSvc myMdmLinkDaoSvc; - - @Override - public Stream queryLinks(IIdType theGoldenResourceId, IIdType theSourceResourceId, MdmMatchResultEnum theMatchResult, MdmLinkSourceEnum theLinkSource, MdmTransactionContext theMdmContext) { - Example exampleLink = exampleLinkFromParameters(theGoldenResourceId, theSourceResourceId, theMatchResult, theLinkSource); - return myMdmLinkDaoSvc.findMdmLinkByExample(exampleLink).stream() - .filter(mdmLink -> mdmLink.getMatchResult() != MdmMatchResultEnum.POSSIBLE_DUPLICATE) - .map(this::toJson); - } - - @Override - public Stream getDuplicateGoldenResources(MdmTransactionContext theMdmContext) { - Example exampleLink = exampleLinkFromParameters(null, null, MdmMatchResultEnum.POSSIBLE_DUPLICATE, null); - return myMdmLinkDaoSvc.findMdmLinkByExample(exampleLink).stream().map(this::toJson); - } - - private MdmLinkJson toJson(MdmLink theLink) { - MdmLinkJson retval = new MdmLinkJson(); - String sourceId = myIdHelperService.resourceIdFromPidOrThrowException(theLink.getSourcePid()).toVersionless().getValue(); - retval.setSourceId(sourceId); - String goldenResourceId = myIdHelperService.resourceIdFromPidOrThrowException(theLink.getGoldenResourcePid()).toVersionless().getValue(); - retval.setGoldenResourceId(goldenResourceId); - retval.setCreated(theLink.getCreated()); - retval.setEidMatch(theLink.getEidMatch()); - retval.setLinkSource(theLink.getLinkSource()); - retval.setMatchResult(theLink.getMatchResult()); - retval.setLinkCreatedNewResource(theLink.getHadToCreateNewGoldenResource()); - retval.setScore(theLink.getScore()); - retval.setUpdated(theLink.getUpdated()); - retval.setVector(theLink.getVector()); - retval.setVersion(theLink.getVersion()); - return retval; - } - - private Example exampleLinkFromParameters(IIdType theGoldenResourceId, IIdType theSourceId, MdmMatchResultEnum theMatchResult, MdmLinkSourceEnum theLinkSource) { - MdmLink mdmLink = myMdmLinkDaoSvc.newMdmLink(); - if (theGoldenResourceId != null) { - mdmLink.setGoldenResourcePid(myIdHelperService.getPidOrThrowException(theGoldenResourceId)); - } - if (theSourceId != null) { - mdmLink.setSourcePid(myIdHelperService.getPidOrThrowException(theSourceId)); - } - if (theMatchResult != null) { - mdmLink.setMatchResult(theMatchResult); - } - if (theLinkSource != null) { - mdmLink.setLinkSource(theLinkSource); - } - return Example.of(mdmLink); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkSvcImpl.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkSvcImpl.java deleted file mode 100644 index 4758868bdb5..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkSvcImpl.java +++ /dev/null @@ -1,140 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -import ca.uhn.fhir.mdm.api.IMdmLinkSvc; -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchOutcome; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.log.Logs; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import org.hl7.fhir.instance.model.api.IAnyResource; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.slf4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.transaction.Transactional; -import java.util.Optional; - -/** - * This class is in charge of managing MdmLinks between Golden Resources and source resources - */ -@Service -public class MdmLinkSvcImpl implements IMdmLinkSvc { - - private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); - - @Autowired - private MdmResourceDaoSvc myMdmResourceDaoSvc; - @Autowired - private MdmLinkDaoSvc myMdmLinkDaoSvc; - @Autowired - private IdHelperService myIdHelperService; - - @Override - @Transactional - public void updateLink(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmMatchOutcome theMatchOutcome, MdmLinkSourceEnum theLinkSource, MdmTransactionContext theMdmTransactionContext) { - if (theMatchOutcome.isPossibleDuplicate() && goldenResourceLinkedAsNoMatch(theGoldenResource, theSourceResource)) { - log(theMdmTransactionContext, theGoldenResource.getIdElement().toUnqualifiedVersionless() + - " is linked as NO_MATCH with " + - theSourceResource.getIdElement().toUnqualifiedVersionless() + - " not linking as POSSIBLE_DUPLICATE."); - return; - } - - MdmMatchResultEnum matchResultEnum = theMatchOutcome.getMatchResultEnum(); - validateRequestIsLegal(theGoldenResource, theSourceResource, matchResultEnum, theLinkSource); - - myMdmResourceDaoSvc.upsertGoldenResource(theGoldenResource, theMdmTransactionContext.getResourceType()); - createOrUpdateLinkEntity(theGoldenResource, theSourceResource, theMatchOutcome, theLinkSource, theMdmTransactionContext); - } - - private boolean goldenResourceLinkedAsNoMatch(IAnyResource theGoldenResource, IAnyResource theSourceResource) { - Long goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource); - Long sourceId = myIdHelperService.getPidOrThrowException(theSourceResource); - // TODO perf collapse into one query - return myMdmLinkDaoSvc.getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(goldenResourceId, sourceId, MdmMatchResultEnum.NO_MATCH).isPresent() || - myMdmLinkDaoSvc.getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(sourceId, goldenResourceId, MdmMatchResultEnum.NO_MATCH).isPresent(); - } - - @Override - public void deleteLink(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmTransactionContext theMdmTransactionContext) { - Optional optionalMdmLink = getMdmLinkForGoldenResourceSourceResourcePair(theGoldenResource, theSourceResource); - if (optionalMdmLink.isPresent()) { - MdmLink mdmLink = optionalMdmLink.get(); - log(theMdmTransactionContext, "Deleting MdmLink [" + theGoldenResource.getIdElement().toVersionless() + " -> " + theSourceResource.getIdElement().toVersionless() + "] with result: " + mdmLink.getMatchResult()); - myMdmLinkDaoSvc.deleteLink(mdmLink); - } - } - - /** - * Helper function which runs various business rules about what types of requests are allowed. - */ - private void validateRequestIsLegal(IAnyResource theGoldenResource, IAnyResource theResource, MdmMatchResultEnum theMatchResult, MdmLinkSourceEnum theLinkSource) { - Optional oExistingLink = getMdmLinkForGoldenResourceSourceResourcePair(theGoldenResource, theResource); - if (oExistingLink.isPresent() && systemIsAttemptingToModifyManualLink(theLinkSource, oExistingLink.get())) { - throw new InternalErrorException("MDM system is not allowed to modify links on manually created links"); - } - - if (systemIsAttemptingToAddNoMatch(theLinkSource, theMatchResult)) { - throw new InternalErrorException("MDM system is not allowed to automatically NO_MATCH a resource"); - } - } - - /** - * Helper function which detects when the MDM system is attempting to add a NO_MATCH link, which is not allowed. - */ - private boolean systemIsAttemptingToAddNoMatch(MdmLinkSourceEnum theLinkSource, MdmMatchResultEnum theMatchResult) { - return theLinkSource == MdmLinkSourceEnum.AUTO && theMatchResult == MdmMatchResultEnum.NO_MATCH; - } - - /** - * Helper function to let us catch when System MDM rules are attempting to override a manually defined link. - */ - private boolean systemIsAttemptingToModifyManualLink(MdmLinkSourceEnum theIncomingSource, MdmLink theExistingSource) { - return theIncomingSource == MdmLinkSourceEnum.AUTO && theExistingSource.isManual(); - } - - private Optional getMdmLinkForGoldenResourceSourceResourcePair(IAnyResource theGoldenResource, IAnyResource theCandidate) { - if (theGoldenResource.getIdElement().getIdPart() == null || theCandidate.getIdElement().getIdPart() == null) { - return Optional.empty(); - } else { - return myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid( - myIdHelperService.getPidOrNull(theGoldenResource), - myIdHelperService.getPidOrNull(theCandidate) - ); - } - } - - private void createOrUpdateLinkEntity(IBaseResource theGoldenResource, IBaseResource theSourceResource, MdmMatchOutcome theMatchOutcome, MdmLinkSourceEnum theLinkSource, MdmTransactionContext theMdmTransactionContext) { - myMdmLinkDaoSvc.createOrUpdateLinkEntity(theGoldenResource, theSourceResource, theMatchOutcome, theLinkSource, theMdmTransactionContext); - } - - private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) { - theMdmTransactionContext.addTransactionLogMessage(theMessage); - ourLog.debug(theMessage); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkUpdaterSvcImpl.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkUpdaterSvcImpl.java deleted file mode 100644 index dec5516928a..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkUpdaterSvcImpl.java +++ /dev/null @@ -1,165 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.api.IMdmLinkSvc; -import ca.uhn.fhir.mdm.api.IMdmLinkUpdaterSvc; -import ca.uhn.fhir.mdm.api.IMdmSettings; -import ca.uhn.fhir.mdm.log.Logs; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.mdm.util.MdmResourceUtil; -import ca.uhn.fhir.mdm.util.MessageHelper; -import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.provider.ProviderConstants; -import org.hl7.fhir.instance.model.api.IAnyResource; -import org.slf4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Objects; -import java.util.Optional; - -public class MdmLinkUpdaterSvcImpl implements IMdmLinkUpdaterSvc { - - private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); - - @Autowired - FhirContext myFhirContext; - @Autowired - IdHelperService myIdHelperService; - @Autowired - MdmLinkDaoSvc myMdmLinkDaoSvc; - @Autowired - IMdmLinkSvc myMdmLinkSvc; - @Autowired - MdmResourceDaoSvc myMdmResourceDaoSvc; - @Autowired - MdmMatchLinkSvc myMdmMatchLinkSvc; - @Autowired - IMdmSettings myMdmSettings; - @Autowired - MessageHelper myMessageHelper; - - @Transactional - @Override - public IAnyResource updateLink(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmMatchResultEnum theMatchResult, MdmTransactionContext theMdmContext) { - String sourceType = myFhirContext.getResourceType(theSourceResource); - - validateUpdateLinkRequest(theGoldenResource, theSourceResource, theMatchResult, sourceType); - - Long goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource); - Long targetId = myIdHelperService.getPidOrThrowException(theSourceResource); - - Optional optionalMdmLink = myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourceId, targetId); - if (!optionalMdmLink.isPresent()) { - throw new InvalidRequestException(myMessageHelper.getMessageForNoLink(theGoldenResource, theSourceResource)); - } - - MdmLink mdmLink = optionalMdmLink.get(); - if (mdmLink.getMatchResult() == theMatchResult) { - ourLog.warn("MDM Link for " + theGoldenResource.getIdElement().toVersionless() + ", " + theSourceResource.getIdElement().toVersionless() + " already has value " + theMatchResult + ". Nothing to do."); - return theGoldenResource; - } - - ourLog.info("Manually updating MDM Link for " + theGoldenResource.getIdElement().toVersionless() + ", " + theSourceResource.getIdElement().toVersionless() + " from " + mdmLink.getMatchResult() + " to " + theMatchResult + "."); - mdmLink.setMatchResult(theMatchResult); - mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL); - myMdmLinkDaoSvc.save(mdmLink); - myMdmResourceDaoSvc.upsertGoldenResource(theGoldenResource, theMdmContext.getResourceType()); - if (theMatchResult == MdmMatchResultEnum.NO_MATCH) { - // Need to find a new Golden Resource to link this target to - myMdmMatchLinkSvc.updateMdmLinksForMdmSource(theSourceResource, theMdmContext); - } - return theGoldenResource; - } - - private void validateUpdateLinkRequest(IAnyResource theGoldenRecord, IAnyResource theSourceResource, MdmMatchResultEnum theMatchResult, String theSourceType) { - String goldenRecordType = myFhirContext.getResourceType(theGoldenRecord); - - if (theMatchResult != MdmMatchResultEnum.NO_MATCH && - theMatchResult != MdmMatchResultEnum.MATCH) { - throw new InvalidRequestException(myMessageHelper.getMessageForUnsupportedMatchResult()); - } - - if (!myMdmSettings.isSupportedMdmType(goldenRecordType)) { - throw new InvalidRequestException(myMessageHelper.getMessageForUnsupportedFirstArgumentTypeInUpdate(goldenRecordType)); - } - - if (!myMdmSettings.isSupportedMdmType(theSourceType)) { - throw new InvalidRequestException(myMessageHelper.getMessageForUnsupportedSecondArgumentTypeInUpdate(theSourceType)); - } - - if (!Objects.equals(goldenRecordType, theSourceType)) { - throw new InvalidRequestException(myMessageHelper.getMessageForArgumentTypeMismatchInUpdate(goldenRecordType, theSourceType)); - } - - if (!MdmResourceUtil.isMdmManaged(theGoldenRecord)) { - throw new InvalidRequestException(myMessageHelper.getMessageForUnmanagedResource()); - } - - if (!MdmResourceUtil.isMdmAllowed(theSourceResource)) { - throw new InvalidRequestException(myMessageHelper.getMessageForUnsupportedSourceResource()); - } - } - - @Transactional - @Override - public void notDuplicateGoldenResource(IAnyResource theGoldenResource, IAnyResource theTargetGoldenResource, MdmTransactionContext theMdmContext) { - validateNotDuplicateGoldenResourceRequest(theGoldenResource, theTargetGoldenResource); - - Long goldenResourceId = myIdHelperService.getPidOrThrowException(theGoldenResource); - Long targetId = myIdHelperService.getPidOrThrowException(theTargetGoldenResource); - - Optional oMdmLink = myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourceId, targetId); - if (!oMdmLink.isPresent()) { - throw new InvalidRequestException("No link exists between " + theGoldenResource.getIdElement().toVersionless() + " and " + theTargetGoldenResource.getIdElement().toVersionless()); - } - - MdmLink mdmLink = oMdmLink.get(); - if (!mdmLink.isPossibleDuplicate()) { - throw new InvalidRequestException(theGoldenResource.getIdElement().toVersionless() + " and " + theTargetGoldenResource.getIdElement().toVersionless() + " are not linked as POSSIBLE_DUPLICATE."); - } - mdmLink.setMatchResult(MdmMatchResultEnum.NO_MATCH); - mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL); - myMdmLinkDaoSvc.save(mdmLink); - } - - /** - * Ensure that the two resources are of the same type and both are managed by HAPI-MDM - */ - private void validateNotDuplicateGoldenResourceRequest(IAnyResource theGoldenResource, IAnyResource theTarget) { - String goldenResourceType = myFhirContext.getResourceType(theGoldenResource); - String targetType = myFhirContext.getResourceType(theTarget); - if (!goldenResourceType.equalsIgnoreCase(targetType)) { - throw new InvalidRequestException("First argument to " + ProviderConstants.MDM_UPDATE_LINK + " must be the same resource type as the second argument. Was " + goldenResourceType + "/" + targetType); - } - - if (!MdmResourceUtil.isMdmManaged(theGoldenResource) || !MdmResourceUtil.isMdmManaged(theTarget)) { - throw new InvalidRequestException("Only MDM Managed Golden Resources may be updated via this operation. The resource provided is not tagged as managed by HAPI-MDM"); - } - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java deleted file mode 100644 index 5bea352a56b..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java +++ /dev/null @@ -1,164 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchOutcome; -import ca.uhn.fhir.mdm.api.IMdmLinkSvc; -import ca.uhn.fhir.mdm.log.Logs; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.mdm.util.MdmResourceUtil; -import ca.uhn.fhir.mdm.util.GoldenResourceHelper; -import ca.uhn.fhir.jpa.mdm.svc.candidate.CandidateList; -import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc; -import ca.uhn.fhir.jpa.mdm.svc.candidate.MatchedGoldenResourceCandidate; -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; - -/** - * MdmMatchLinkSvc is the entrypoint for HAPI's MDM system. An incoming resource can call - * updateMdmLinksForMdmSource and the underlying MDM system will take care of matching it to a GoldenResource, - * or creating a new GoldenResource if a suitable one was not found. - */ -@Service -public class MdmMatchLinkSvc { - private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); - - @Autowired - private IMdmLinkSvc myMdmLinkSvc; - @Autowired - private MdmGoldenResourceFindingSvc myMdmGoldenResourceFindingSvc; - @Autowired - private GoldenResourceHelper myGoldenResourceHelper; - @Autowired - private MdmEidUpdateService myEidUpdateService; - - /** - * Given an MDM source (consisting of any supported MDM type), find a suitable Golden Resource candidate for them, - * or create one if one does not exist. Performs matching based on rules defined in mdm-rules.json. - * Does nothing if resource is determined to be not managed by MDM. - * - * @param theResource the incoming MDM source, which can be any supported MDM type. - * @param theMdmTransactionContext - * @return an {@link TransactionLogMessages} which contains all informational messages related to MDM processing of this resource. - */ - public MdmTransactionContext updateMdmLinksForMdmSource(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) { - if (MdmResourceUtil.isMdmAllowed(theResource)) { - return doMdmUpdate(theResource, theMdmTransactionContext); - } else { - return null; - } - } - - private MdmTransactionContext doMdmUpdate(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) { - CandidateList candidateList = myMdmGoldenResourceFindingSvc.findGoldenResourceCandidates(theResource); - - if (candidateList.isEmpty()) { - handleMdmWithNoCandidates(theResource, theMdmTransactionContext); - } else if (candidateList.exactlyOneMatch()) { - handleMdmWithSingleCandidate(theResource, candidateList.getOnlyMatch(), theMdmTransactionContext); - } else { - handleMdmWithMultipleCandidates(theResource, candidateList, theMdmTransactionContext); - } - return theMdmTransactionContext; - } - - private void handleMdmWithMultipleCandidates(IAnyResource theResource, CandidateList theCandidateList, MdmTransactionContext theMdmTransactionContext) { - MatchedGoldenResourceCandidate firstMatch = theCandidateList.getFirstMatch(); - Long sampleGoldenResourcePid = firstMatch.getCandidateGoldenResourcePid().getIdAsLong(); - boolean allSameGoldenResource = theCandidateList.stream() - .allMatch(candidate -> candidate.getCandidateGoldenResourcePid().getIdAsLong().equals(sampleGoldenResourcePid)); - - if (allSameGoldenResource) { - log(theMdmTransactionContext, "MDM received multiple match candidates, but they are all linked to the same Golden Resource."); - handleMdmWithSingleCandidate(theResource, firstMatch, theMdmTransactionContext); - } else { - log(theMdmTransactionContext, "MDM received multiple match candidates, that were linked to different Golden Resources. Setting POSSIBLE_DUPLICATES and POSSIBLE_MATCHES."); - //Set them all as POSSIBLE_MATCH - List goldenResources = new ArrayList<>(); - for (MatchedGoldenResourceCandidate matchedGoldenResourceCandidate : theCandidateList.getCandidates()) { - IAnyResource goldenResource = myMdmGoldenResourceFindingSvc - .getGoldenResourceFromMatchedGoldenResourceCandidate(matchedGoldenResourceCandidate, theMdmTransactionContext.getResourceType()); - MdmMatchOutcome outcome = MdmMatchOutcome.POSSIBLE_MATCH; - outcome.setEidMatch(theCandidateList.isEidMatch()); - myMdmLinkSvc.updateLink(goldenResource, theResource, outcome, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); - goldenResources.add(goldenResource); - } - - //Set all GoldenResources as POSSIBLE_DUPLICATE of the last GoldenResource. - IAnyResource firstGoldenResource = goldenResources.get(0); - goldenResources.subList(1, goldenResources.size()) - .forEach(possibleDuplicateGoldenResource -> { - MdmMatchOutcome outcome = MdmMatchOutcome.POSSIBLE_DUPLICATE; - outcome.setEidMatch(theCandidateList.isEidMatch()); - myMdmLinkSvc.updateLink(firstGoldenResource, possibleDuplicateGoldenResource, outcome, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); - }); - } - } - - private void handleMdmWithNoCandidates(IAnyResource theResource, MdmTransactionContext theMdmTransactionContext) { - log(theMdmTransactionContext, String.format("There were no matched candidates for MDM, creating a new %s.", theResource.getIdElement().getResourceType())); - IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theResource); - // TODO GGG :) - // 1. Get the right helper - // 2. Create source resource for the MDM source - // 3. UPDATE MDM LINK TABLE - myMdmLinkSvc.updateLink(newGoldenResource, theResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); - } - - private void handleMdmCreate(IAnyResource theSourceResource, MatchedGoldenResourceCandidate theGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext) { - log(theMdmTransactionContext, "MDM has narrowed down to one candidate for matching."); - IAnyResource golenResource = myMdmGoldenResourceFindingSvc.getGoldenResourceFromMatchedGoldenResourceCandidate(theGoldenResourceCandidate, theMdmTransactionContext.getResourceType()); - - if (myGoldenResourceHelper.isPotentialDuplicate(golenResource, theSourceResource)) { - log(theMdmTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs."); - IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theSourceResource); - myMdmLinkSvc.updateLink(newGoldenResource, theSourceResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); - myMdmLinkSvc.updateLink(newGoldenResource, golenResource, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); - } else { - if (theGoldenResourceCandidate.isMatch()) { - myGoldenResourceHelper.handleExternalEidAddition(golenResource, theSourceResource, theMdmTransactionContext); - //TODO MDM GGG/NG: eventually we need to add survivorship rules of attributes here. Currently no data is copied over except EIDs. - } - myMdmLinkSvc.updateLink(golenResource, theSourceResource, theGoldenResourceCandidate.getMatchResult(), MdmLinkSourceEnum.AUTO, theMdmTransactionContext); - } - } - - private void handleMdmWithSingleCandidate(IAnyResource theResource, MatchedGoldenResourceCandidate theGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext) { - log(theMdmTransactionContext, "MDM has narrowed down to one candidate for matching."); - if (theMdmTransactionContext.getRestOperation().equals(MdmTransactionContext.OperationType.UPDATE_RESOURCE)) { - myEidUpdateService.handleMdmUpdate(theResource, theGoldenResourceCandidate, theMdmTransactionContext); - } else { - handleMdmCreate(theResource, theGoldenResourceCandidate, theMdmTransactionContext); - } - } - - private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) { - theMdmTransactionContext.addTransactionLogMessage(theMessage); - ourLog.debug(theMessage); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceDaoSvc.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceDaoSvc.java deleted file mode 100644 index 2240e31582d..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceDaoSvc.java +++ /dev/null @@ -1,110 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.api.MdmConstants; -import ca.uhn.fhir.mdm.api.IMdmSettings; -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.model.entity.TagTypeEnum; -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.jetbrains.annotations.NotNull; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Optional; - -@Service -public class MdmResourceDaoSvc { - - private static final int MAX_MATCHING_GOLDEN_RESOURCES = 1000; - - @Autowired - DaoRegistry myDaoRegistry; - @Autowired - IMdmSettings myMdmSettings; - - public DaoMethodOutcome upsertGoldenResource(IAnyResource theGoldenResource, String theResourceType) { - IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theResourceType); - if (theGoldenResource.getIdElement().hasIdPart()) { - return resourceDao.update(theGoldenResource); - } else { - return resourceDao.create(theGoldenResource); - } - } - - /** - * Given a resource, remove its Golden Resource tag. - * @param theGoldenResource the {@link IAnyResource} to remove the tag from. - * @param theResourcetype the type of that resource - */ - public void removeGoldenResourceTag(IAnyResource theGoldenResource, String theResourcetype) { - IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theResourcetype); - resourceDao.removeTag(theGoldenResource.getIdElement(), TagTypeEnum.TAG, MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS, MdmConstants.CODE_GOLDEN_RECORD); - } - - public IAnyResource readGoldenResourceByPid(ResourcePersistentId theGoldenResourcePid, String theResourceType) { - IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theResourceType); - return (IAnyResource) resourceDao.readByPid(theGoldenResourcePid); - } - - //TODO GGG MDM address this - public Optional searchGoldenResourceByEID(String theEid, String theResourceType) { - SearchParameterMap map = buildEidSearchParameterMap(theEid); - - IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theResourceType); - IBundleProvider search = resourceDao.search(map); - List resources = search.getResources(0, MAX_MATCHING_GOLDEN_RESOURCES); - - if (resources.isEmpty()) { - return Optional.empty(); - } else if (resources.size() > 1) { - throw new InternalErrorException("Found more than one active " + - MdmConstants.CODE_HAPI_MDM_MANAGED + - " Golden Resource with EID " + - theEid + - ": " + - resources.get(0).getIdElement().getValue() + - ", " + - resources.get(1).getIdElement().getValue() - ); - } else { - return Optional.of((IAnyResource) resources.get(0)); - } - } - - @NotNull - private SearchParameterMap buildEidSearchParameterMap(String theTheEid) { - SearchParameterMap map = new SearchParameterMap(); - map.setLoadSynchronous(true); - map.add("identifier", new TokenParam(myMdmSettings.getMdmRules().getEnterpriseEIDSystem(), theTheEid)); - map.add("_tag", new TokenParam(MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS, MdmConstants.CODE_GOLDEN_RECORD)); - return map; - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/FindCandidateByExampleSvc.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/FindCandidateByExampleSvc.java deleted file mode 100644 index bc8b43efafb..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/FindCandidateByExampleSvc.java +++ /dev/null @@ -1,111 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc.candidate; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -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.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; -import org.hl7.fhir.instance.model.api.IAnyResource; -import org.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 FindCandidateByExampleSvc extends BaseCandidateFinder { - private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); - - @Autowired - private FhirContext myFhirContext; - @Autowired - IdHelperService myIdHelperService; - @Autowired - private MdmLinkDaoSvc myMdmLinkDaoSvc; - @Autowired - private IMdmMatchFinderSvc myMdmMatchFinderSvc; - - /** - * Attempt to find matching Golden Resources by resolving them from similar Matching target resources, where target resource - * can be either Patient or Practitioner. Runs MDM logic over the existing target resources, then finds their - * entries in the MdmLink table, and returns all the matches found therein. - * - * @param theTarget the {@link IBaseResource} which we want to find candidate Golden Resources for. - * @return an Optional list of {@link MatchedGoldenResourceCandidate} indicating matches. - */ - @Override - protected List findMatchGoldenResourceCandidates(IAnyResource theTarget) { - List retval = new ArrayList<>(); - - List goldenResourcePidsToExclude = getNoMatchGoldenResourcePids(theTarget); - - List matchedCandidates = myMdmMatchFinderSvc.getMatchedTargets(myFhirContext.getResourceType(theTarget), theTarget); - - //Convert all possible match targets to their equivalent Golden Resources by looking up in the MdmLink table, - //while ensuring that the matches aren't in our NO_MATCH list. - // The data flow is as follows -> - // MatchedTargetCandidate -> Golden Resource -> MdmLink -> MatchedGoldenResourceCandidate - matchedCandidates = matchedCandidates.stream().filter(mc -> mc.isMatch() || mc.isPossibleMatch()).collect(Collectors.toList()); - for (MatchedTarget match : matchedCandidates) { - Optional optionalMdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(myIdHelperService.getPidOrNull(match.getTarget())); - if (!optionalMdmLink.isPresent()) { - continue; - } - - MdmLink matchMdmLink = optionalMdmLink.get(); - if (goldenResourcePidsToExclude.contains(matchMdmLink.getGoldenResourcePid())) { - ourLog.info("Skipping MDM on candidate Golden Resource with PID {} due to manual NO_MATCH", matchMdmLink.getGoldenResourcePid()); - continue; - } - - MatchedGoldenResourceCandidate candidate = new MatchedGoldenResourceCandidate(getResourcePersistentId(matchMdmLink.getGoldenResourcePid()), match.getMatchResult()); - retval.add(candidate); - } - return retval; - } - - private List getNoMatchGoldenResourcePids(IBaseResource theBaseResource) { - Long targetPid = myIdHelperService.getPidOrNull(theBaseResource); - return myMdmLinkDaoSvc.getMdmLinksBySourcePidAndMatchResult(targetPid, MdmMatchResultEnum.NO_MATCH) - .stream() - .map(MdmLink::getGoldenResourcePid) - .collect(Collectors.toList()); - } - - private ResourcePersistentId getResourcePersistentId(Long theGoldenResourcePid) { - return new ResourcePersistentId(theGoldenResourcePid); - } - - @Override - protected CandidateStrategyEnum getStrategy() { - return CandidateStrategyEnum.SCORE; - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MatchedGoldenResourceCandidate.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MatchedGoldenResourceCandidate.java deleted file mode 100644 index 2c6f83f8120..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MatchedGoldenResourceCandidate.java +++ /dev/null @@ -1,53 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc.candidate; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.api.MdmMatchOutcome; -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; - -public class MatchedGoldenResourceCandidate { - - private final ResourcePersistentId myCandidateGoldenResourcePid; - private final MdmMatchOutcome myMdmMatchOutcome; - - public MatchedGoldenResourceCandidate(ResourcePersistentId theCandidate, MdmMatchOutcome theMdmMatchOutcome) { - myCandidateGoldenResourcePid = theCandidate; - myMdmMatchOutcome = theMdmMatchOutcome; - } - - public MatchedGoldenResourceCandidate(ResourcePersistentId theGoldenResourcePid, MdmLink theMdmLink) { - myCandidateGoldenResourcePid = theGoldenResourcePid; - myMdmMatchOutcome = new MdmMatchOutcome(theMdmLink.getVector(), theMdmLink.getScore()).setMatchResultEnum(theMdmLink.getMatchResult()); - } - - public ResourcePersistentId getCandidateGoldenResourcePid() { - return myCandidateGoldenResourcePid; - } - - public MdmMatchOutcome getMatchResult() { - return myMdmMatchOutcome; - } - - public boolean isMatch() { - return myMdmMatchOutcome.isMatch(); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmGoldenResourceFindingSvc.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmGoldenResourceFindingSvc.java deleted file mode 100644 index 718ccc4e7ed..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmGoldenResourceFindingSvc.java +++ /dev/null @@ -1,83 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc.candidate; - -/*- - * #%L - * HAPI FHIR JPA Server - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.log.Logs; -import ca.uhn.fhir.jpa.mdm.svc.MdmResourceDaoSvc; -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 MdmGoldenResourceFindingSvc { - - private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); - - @Autowired - private MdmResourceDaoSvc myMdmResourceDaoSvc; - - @Autowired - private FindCandidateByEidSvc myFindCandidateByEidSvc; - - @Autowired - private FindCandidateByLinkSvc myFindCandidateByLinkSvc; - - @Autowired - private FindCandidateByExampleSvc myFindCandidateByExampleSvc; - - /** - * Given an incoming IBaseResource, limited to the supported MDM type, return a list of {@link MatchedGoldenResourceCandidate} - * indicating possible candidates for a matching Golden Resource. Uses several separate methods for finding candidates: - *

- * 0. First, check the incoming Resource for an EID. If it is present, and we can find a Golden Resource with this EID, it automatically matches. - * 1. First, check link table for any entries where this baseresource is the source of a Golden Resource. If found, return. - * 2. If none are found, attempt to find Golden Resources which link to this theResource. - * 3. If none are found, attempt to find Golden Resources similar to our incoming resource based on the MDM rules and field matchers. - * 4. If none are found, attempt to find Golden Resources that are linked to sources that are similar to our incoming resource based on the MDM rules and - * field matchers. - * - * @param theResource the {@link IBaseResource} we are attempting to find matching candidate Golden Resources for. - * @return A list of {@link MatchedGoldenResourceCandidate} indicating all potential Golden Resource matches. - */ - public CandidateList findGoldenResourceCandidates(IAnyResource theResource) { - CandidateList matchedGoldenResourceCandidates = myFindCandidateByEidSvc.findCandidates(theResource); - - if (matchedGoldenResourceCandidates.isEmpty()) { - matchedGoldenResourceCandidates = myFindCandidateByLinkSvc.findCandidates(theResource); - } - - if (matchedGoldenResourceCandidates.isEmpty()) { - //OK, so we have not found any links in the MdmLink table with us as a source. Next, let's find - //possible Golden Resources matches by following MDM rules. - matchedGoldenResourceCandidates = myFindCandidateByExampleSvc.findCandidates(theResource); - } - - return matchedGoldenResourceCandidates; - } - - public IAnyResource getGoldenResourceFromMatchedGoldenResourceCandidate(MatchedGoldenResourceCandidate theMatchedGoldenResourceCandidate, String theResourceType) { - ResourcePersistentId goldenResourcePid = theMatchedGoldenResourceCandidate.getCandidateGoldenResourcePid(); - return myMdmResourceDaoSvc.readGoldenResourceByPid(goldenResourcePid, theResourceType); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java deleted file mode 100644 index 4d3eb16ea80..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/BaseMdmR4Test.java +++ /dev/null @@ -1,534 +0,0 @@ -package ca.uhn.fhir.jpa.mdm; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; -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.dao.data.IMdmLinkDao; -import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.jpa.mdm.config.MdmConsumerConfig; -import ca.uhn.fhir.jpa.mdm.config.MdmSubmitterConfig; -import ca.uhn.fhir.jpa.mdm.config.TestMdmConfigR4; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -import ca.uhn.fhir.jpa.mdm.matcher.IsLinkedTo; -import ca.uhn.fhir.jpa.mdm.matcher.IsMatchedToAGoldenResource; -import ca.uhn.fhir.jpa.mdm.matcher.IsPossibleDuplicateOf; -import ca.uhn.fhir.jpa.mdm.matcher.IsPossibleLinkedTo; -import ca.uhn.fhir.jpa.mdm.matcher.IsPossibleMatchWith; -import ca.uhn.fhir.jpa.mdm.matcher.IsSameGoldenResourceAs; -import ca.uhn.fhir.jpa.mdm.svc.MdmMatchLinkSvc; -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.mdm.api.IMdmSettings; -import ca.uhn.fhir.mdm.api.MdmConstants; -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.mdm.rules.svc.MdmResourceMatcherSvc; -import ca.uhn.fhir.mdm.util.EIDHelper; -import ca.uhn.fhir.mdm.util.MdmResourceUtil; -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.apache.commons.lang3.StringUtils; -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.CodeableConcept; -import org.hl7.fhir.r4.model.ContactPoint; -import org.hl7.fhir.r4.model.DateType; -import org.hl7.fhir.r4.model.Medication; -import org.hl7.fhir.r4.model.Organization; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.Practitioner; -import org.hl7.fhir.r4.model.Reference; -import org.jetbrains.annotations.NotNull; -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 = {MdmSubmitterConfig.class, MdmConsumerConfig.class, TestMdmConfigR4.class, SubscriptionProcessorConfig.class}) -abstract public class BaseMdmR4Test extends BaseJpaR4Test { - private static final Logger ourLog = getLogger(BaseMdmR4Test.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"; - protected static final String DUMMY_ORG_ID = "Organization/mfr"; - - @Autowired - protected FhirContext myFhirContext; - @Autowired - protected IFhirResourceDao myPatientDao; - @Autowired - protected IFhirResourceDao myOrganizationDao; - @Autowired - protected IFhirResourceDao myMedicationDao; - @Autowired - protected IFhirResourceDao myPractitionerDao; - @Autowired - protected MdmResourceMatcherSvc myMdmResourceMatcherSvc; - @Autowired - protected IMdmLinkDao myMdmLinkDao; - @Autowired - protected MdmLinkDaoSvc myMdmLinkDaoSvc; - @Autowired - protected IdHelperService myIdHelperService; - @Autowired - protected IMdmSettings myMdmSettings; - @Autowired - protected MdmMatchLinkSvc myMdmMatchLinkSvc; - @Autowired - protected EIDHelper myEIDHelper; - @Autowired - SearchParamRegistryImpl mySearchParamRegistry; - @Autowired - private IInterceptorBroadcaster myInterceptorBroadcaster; - - protected ServletRequestDetails myRequestDetails; - - @Autowired - private DaoRegistry myDaoRegistry; - - @BeforeEach - public void beforeSetRequestDetails() { - myRequestDetails = new ServletRequestDetails(myInterceptorBroadcaster); - } - - @Override - @AfterEach - public void after() throws IOException { - myMdmLinkDao.deleteAll(); - assertEquals(0, myMdmLinkDao.count()); - super.after(); - } - - protected void saveLink(MdmLink theMdmLink) { - myMdmLinkDaoSvc.save(theMdmLink); - } - - @Nonnull - protected Patient createGoldenPatient() { - return createPatient(new Patient(), true, false); - } - - @Nonnull - protected Patient createPatient() { - return createPatient(new Patient()); - } - - @Nonnull - protected Patient createGoldenPatient(Patient thePatient) { - return createPatient(thePatient, true, false); - } - - @Nonnull - protected Patient createRedirectedGoldenPatient(Patient thePatient) { - return createPatient(thePatient, true, true); - } - - @Nonnull - protected Patient createPatient(Patient thePatient, boolean theMdmManaged, boolean isRedirect) { - if (theMdmManaged) { - MdmResourceUtil.setMdmManaged(thePatient); - if (isRedirect) { - MdmResourceUtil.setGoldenResourceRedirected(thePatient); - } else { - MdmResourceUtil.setGoldenResource(thePatient); - } - } - - DaoMethodOutcome outcome = myPatientDao.create(thePatient); - Patient patient = (Patient) outcome.getResource(); - patient.setId(outcome.getId()); - return patient; - } - - @Nonnull - protected Patient createPatient(Patient thePatient) { - //Note that since our mdm-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 Medication createMedication(Medication theMedication) { - //Note that since our mdm-rules block on active=true, all patients must be active. - DaoMethodOutcome outcome = myMedicationDao.create(theMedication); - Medication medication = (Medication) outcome.getResource(); - medication.setId(outcome.getId()); - return medication; - } - - @Nonnull - protected Practitioner createPractitioner(Practitioner thePractitioner) { - //Note that since our mdm-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); - } - - /** - * Use {@link #buildPatientWithNameAndId(String, String)} instead - */ - @Deprecated - @Nonnull - protected Patient buildSourcePaitentWithNameAndId(String theGivenName, String theId) { - return buildSourcePatientWithNameIdAndBirthday(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; - } - - /** - * Use {@link #buildPatientWithNameAndId(String, String)} instead. - */ - @Deprecated - @Nonnull - protected Patient buildSourcePatientWithNameIdAndBirthday(String theGivenName, String theId, Date theBirthday) { - Patient patient = new Patient(); - patient.addName().addGiven(theGivenName); - patient.addName().setFamily(TEST_NAME_FAMILY); - patient.addIdentifier().setSystem(TEST_ID_SYSTEM).setValue(theId); - patient.setBirthDate(theBirthday); - DateType dateType = new DateType(theBirthday); - dateType.setPrecision(TemporalPrecisionEnum.DAY); - patient.setBirthDateElement(dateType); - return patient; - } - - @Nonnull - protected Patient buildJanePatient() { - return buildPatientWithNameAndId(NAME_GIVEN_JANE, JANE_ID); - } - - @Nonnull - protected Practitioner buildJanePractitioner() { - return buildPractitionerWithNameAndId(NAME_GIVEN_JANE, JANE_ID); - } - - /** - * Use {@link #buildJanePatient()} instead - */ - @Nonnull - @Deprecated - protected Patient buildJaneSourcePatient() { - return buildSourcePaitentWithNameAndId(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, myMdmLinkDao.count()); - } - - protected IAnyResource getGoldenResourceFromTargetResource(IAnyResource theBaseResource) { - String resourceType = theBaseResource.getIdElement().getResourceType(); - IFhirResourceDao relevantDao = myDaoRegistry.getResourceDao(resourceType); - - Optional matchedLinkForTargetPid = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(myIdHelperService.getPidOrNull(theBaseResource)); - if (matchedLinkForTargetPid.isPresent()) { - Long goldenResourcePid = matchedLinkForTargetPid.get().getGoldenResourcePid(); - return (IAnyResource) relevantDao.readByPid(new ResourcePersistentId(goldenResourcePid)); - } else { - return null; - } - } - - protected T getTargetResourceFromMdmLink(MdmLink theMdmLink, String theResourceType) { - IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theResourceType); - return (T) resourceDao.readByPid(new ResourcePersistentId(theMdmLink.getGoldenResourcePid())); - } - - protected Patient addExternalEID(Patient thePatient, String theEID) { - thePatient.addIdentifier().setSystem(myMdmSettings.getMdmRules().getEnterpriseEIDSystem()).setValue(theEID); - return thePatient; - } - - protected Patient clearExternalEIDs(Patient thePatient) { - thePatient.getIdentifier().removeIf(theIdentifier -> theIdentifier.getSystem().equalsIgnoreCase(myMdmSettings.getMdmRules().getEnterpriseEIDSystem())); - return thePatient; - } - - protected Patient createPatientAndUpdateLinks(Patient thePatient) { - thePatient = createPatient(thePatient); - myMdmMatchLinkSvc.updateMdmLinksForMdmSource(thePatient, createContextForCreate("Patient")); - return thePatient; - } - - protected Medication buildMedication(String theManufacturerReference) { - Medication medication = new Medication(); - medication.setManufacturer(new Reference(theManufacturerReference)); - CodeableConcept codeableConcept = new CodeableConcept(); - codeableConcept.addCoding().setSystem("zoop").setCode("boop"); - medication.setCode(codeableConcept); - return medication; - } - - protected Medication buildMedicationWithDummyOrganization() { - return buildMedication(DUMMY_ORG_ID); - } - - protected Medication createMedicationAndUpdateLinks(Medication theMedication) { - theMedication = createMedication(theMedication); - myMdmMatchLinkSvc.updateMdmLinksForMdmSource(theMedication, createContextForCreate("Medication")); - return theMedication; - } - - protected MdmTransactionContext createContextForCreate(String theResourceType) { - MdmTransactionContext ctx = new MdmTransactionContext(); - ctx.setRestOperation(MdmTransactionContext.OperationType.CREATE_RESOURCE); - ctx.setResourceType(theResourceType); - ctx.setTransactionLogMessages(null); - return ctx; - } - - protected MdmTransactionContext createContextForUpdate(String theResourceType) { - MdmTransactionContext ctx = new MdmTransactionContext(); - ctx.setRestOperation(MdmTransactionContext.OperationType.UPDATE_RESOURCE); - ctx.setTransactionLogMessages(null); - ctx.setResourceType(theResourceType); - return ctx; - } - - protected Patient updatePatientAndUpdateLinks(Patient thePatient) { - thePatient = (Patient) myPatientDao.update(thePatient).getResource(); - myMdmMatchLinkSvc.updateMdmLinksForMdmSource(thePatient, createContextForUpdate(thePatient.getIdElement().getResourceType())); - return thePatient; - } - - protected Practitioner createPractitionerAndUpdateLinks(Practitioner thePractitioner) { - thePractitioner.setActive(true); - DaoMethodOutcome daoMethodOutcome = myPractitionerDao.create(thePractitioner); - thePractitioner.setId(daoMethodOutcome.getId()); - myMdmMatchLinkSvc.updateMdmLinksForMdmSource(thePractitioner, createContextForCreate("Practitioner")); - return thePractitioner; - } - - protected Matcher sameGoldenResourceAs(IAnyResource... theBaseResource) { - return IsSameGoldenResourceAs.sameGoldenResourceAs(myIdHelperService, myMdmLinkDaoSvc, theBaseResource); - } - - protected Matcher linkedTo(IAnyResource... theBaseResource) { - return IsLinkedTo.linkedTo(myIdHelperService, myMdmLinkDaoSvc, theBaseResource); - } - - protected Matcher possibleLinkedTo(IAnyResource... theBaseResource) { - return IsPossibleLinkedTo.possibleLinkedTo(myIdHelperService, myMdmLinkDaoSvc, theBaseResource); - } - - protected Matcher possibleMatchWith(IAnyResource... theBaseResource) { - return IsPossibleMatchWith.possibleMatchWith(myIdHelperService, myMdmLinkDaoSvc, theBaseResource); - } - - protected Matcher possibleDuplicateOf(IAnyResource... theBaseResource) { - return IsPossibleDuplicateOf.possibleDuplicateOf(myIdHelperService, myMdmLinkDaoSvc, theBaseResource); - } - - protected Matcher matchedToAGoldenResource() { - return IsMatchedToAGoldenResource.matchedToAGoldenResource(myIdHelperService, myMdmLinkDaoSvc); - } - - protected Patient getOnlyGoldenPatient() { - List resources = getAllGoldenPatients(); - assertEquals(1, resources.size()); - return (Patient) resources.get(0); - } - - - @Nonnull - protected List getAllGoldenPatients() { - return getPatientsByTag(MdmConstants.CODE_GOLDEN_RECORD); - } - - @Nonnull - protected List getAllRedirectedGoldenPatients() { - return getPatientsByTag(MdmConstants.CODE_GOLDEN_RECORD_REDIRECTED); - } - - @NotNull - private List getPatientsByTag(String theCode) { - SearchParameterMap map = new SearchParameterMap(); - map.setLoadSynchronous(true); - //TODO GGG ensure that this tag search works effectively. - map.add("_tag", new TokenParam(MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS, theCode)); - IBundleProvider bundle = myPatientDao.search(map); - return bundle.getResources(0, 999); - } - - - @Nonnull - protected MdmLink createResourcesAndBuildTestMDMLink() { - Patient sourcePatient = createGoldenPatient(); - Patient patient = createPatient(); - - MdmLink mdmLink = myMdmLinkDaoSvc.newMdmLink(); - mdmLink.setLinkSource(MdmLinkSourceEnum.MANUAL); - mdmLink.setMatchResult(MdmMatchResultEnum.MATCH); - mdmLink.setGoldenResourcePid(myIdHelperService.getPidOrNull(sourcePatient)); - mdmLink.setSourcePid(myIdHelperService.getPidOrNull(patient)); - return mdmLink; - } - -// protected void loadMdmSearchParameters() { -// myMdmSearchParameterLoader.daoUpdateMdmSearchParameters(); -// mySearchParamRegistry.forceRefresh(); -// } - - protected void logAllLinks() { - ourLog.info("Logging all MDM Links:"); - List links = myMdmLinkDao.findAll(); - for (MdmLink link : links) { - ourLog.info(link.toString()); - } - } - - protected void assertLinksMatchResult(MdmMatchResultEnum... theExpectedValues) { - assertFields(MdmLink::getMatchResult, theExpectedValues); - } - - protected void assertLinksCreatedNewResource(Boolean... theExpectedValues) { - assertFields(MdmLink::getHadToCreateNewGoldenResource, theExpectedValues); - } - - protected void assertLinksMatchedByEid(Boolean... theExpectedValues) { - assertFields(MdmLink::getEidMatch, theExpectedValues); - } - public SearchParameterMap buildGoldenResourceSearchParameterMap() { - SearchParameterMap spMap = new SearchParameterMap(); - spMap.setLoadSynchronous(true); - spMap.add("_tag", new TokenParam(MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS, MdmConstants.CODE_GOLDEN_RECORD)); - return spMap; - } - - private void assertFields(Function theAccessor, T... theExpectedValues) { - List links = myMdmLinkDao.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"); - } - } - - - protected void print(String message, IBaseResource ... theResource) { - if (StringUtils.isNotEmpty(message)) { - ourLog.info(message); - } - - for (IBaseResource r : theResource) { - ourLog.info(myFhirContext.newJsonParser().encodeResourceToString(r)); - } - } - - protected void print(IBaseResource ... theResource) { - print(null, theResource); - } - - - - protected void printResources(String theResourceType) { - IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResourceType); - IBundleProvider search = dao.search(new SearchParameterMap()); - search.getResources(0, search.size()).forEach(r -> { - print(r); - }); - } - - - protected void printLinks() { - myMdmLinkDao.findAll().forEach(mdmLink -> { - ourLog.info(String.valueOf(mdmLink)); - }); - } - - protected DaoMethodOutcome createDummyOrganization() { - Organization org = new Organization(); - org.setId(DUMMY_ORG_ID); - return myOrganizationDao.update(org); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java deleted file mode 100644 index 3d8c392e9a7..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvcTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.dao; - -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.IMdmSettings; -import ca.uhn.fhir.mdm.rules.json.MdmRulesJson; -import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; -import ca.uhn.fhir.jpa.entity.MdmLink; -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 MdmLinkDaoSvcTest extends BaseMdmR4Test { - - @Test - public void testCreate() { - MdmLink mdmLink = createResourcesAndBuildTestMDMLink(); - assertThat(mdmLink.getCreated(), is(nullValue())); - assertThat(mdmLink.getUpdated(), is(nullValue())); - myMdmLinkDaoSvc.save(mdmLink); - assertThat(mdmLink.getCreated(), is(notNullValue())); - assertThat(mdmLink.getUpdated(), is(notNullValue())); - assertTrue(mdmLink.getUpdated().getTime() - mdmLink.getCreated().getTime() < 1000); - } - - @Test - public void testUpdate() { - MdmLink createdLink = myMdmLinkDaoSvc.save(createResourcesAndBuildTestMDMLink()); - assertThat(createdLink.getLinkSource(), is(MdmLinkSourceEnum.MANUAL)); - TestUtil.sleepOneClick(); - createdLink.setLinkSource(MdmLinkSourceEnum.AUTO); - MdmLink updatedLink = myMdmLinkDaoSvc.save(createdLink); - assertNotEquals(updatedLink.getCreated(), updatedLink.getUpdated()); - } - - @Test - public void testNew() { - MdmLink newLink = myMdmLinkDaoSvc.newMdmLink(); - MdmRulesJson rules = myMdmSettings.getMdmRules(); - assertEquals("1", rules.getVersion()); - assertEquals(rules.getVersion(), newLink.getVersion()); - } - -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/entity/MdmEnumTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/entity/MdmEnumTest.java deleted file mode 100644 index e3a0a370572..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/entity/MdmEnumTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.entity; - -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class MdmEnumTest { - - @Test - public void mdmEnumOrdinals() { - // This test is here to enforce that new values in these enums are always added to the end - assertEquals(6, MdmMatchResultEnum.values().length); - assertEquals(MdmMatchResultEnum.REDIRECT, MdmMatchResultEnum.values()[MdmMatchResultEnum.values().length - 1]); - - assertEquals(2, MdmLinkSourceEnum.values().length); - assertEquals(MdmLinkSourceEnum.MANUAL, MdmLinkSourceEnum.values()[MdmLinkSourceEnum.values().length - 1]); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/MdmLinkHelper.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/MdmLinkHelper.java deleted file mode 100644 index 33f2a87cfb1..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/MdmLinkHelper.java +++ /dev/null @@ -1,31 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.helper; - -import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao; -import ca.uhn.fhir.jpa.entity.MdmLink; -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 MdmLinkHelper { - private static final Logger ourLog = LoggerFactory.getLogger(MdmLinkHelper.class); - - @Autowired - IMdmLinkDao myMdmLinkDao; - - @Transactional - public void logMdmLinks() { - List links = myMdmLinkDao.findAll(); - ourLog.info("All MDM Links:"); - for (MdmLink link : links) { - IdDt goldenResourceId = link.getGoldenResource().getIdDt().toVersionless(); - IdDt targetId = link.getSource().getIdDt().toVersionless(); - ourLog.info("{}: {}, {}, {}, {}", link.getId(), goldenResourceId, targetId, link.getMatchResult(), link.getLinkSource()); - } - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java deleted file mode 100644 index 5ad4b9ebe84..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/interceptor/MdmStorageInterceptorIT.java +++ /dev/null @@ -1,316 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.interceptor; - -import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; -import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; -import ca.uhn.fhir.jpa.mdm.helper.MdmHelperConfig; -import ca.uhn.fhir.jpa.mdm.helper.MdmHelperR4; -import ca.uhn.fhir.mdm.model.CanonicalEID; -import ca.uhn.fhir.mdm.rules.config.MdmSettings; -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.IAnyResource; -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.Medication; -import org.hl7.fhir.r4.model.Organization; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.SearchParameter; -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.mdm.api.MdmConstants.CODE_GOLDEN_RECORD; -import static ca.uhn.fhir.mdm.api.MdmConstants.CODE_GOLDEN_RECORD_REDIRECTED; -import static ca.uhn.fhir.mdm.api.MdmConstants.CODE_HAPI_MDM_MANAGED; -import static ca.uhn.fhir.mdm.api.MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS; -import static ca.uhn.fhir.mdm.api.MdmConstants.SYSTEM_MDM_MANAGED; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.startsWith; -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 = {MdmHelperConfig.class}) -public class MdmStorageInterceptorIT extends BaseMdmR4Test { - - private static final Logger ourLog = getLogger(MdmStorageInterceptorIT.class); - - @RegisterExtension - @Autowired - public MdmHelperR4 myMdmHelper; - @Autowired - private IdHelperService myIdHelperService; - - @Test - public void testCreatePractitioner() throws InterruptedException { - myMdmHelper.createWithLatch(buildPractitionerWithNameAndId("somename", "some_id")); - assertLinkCount(1); - } - - @Test - public void testDeleteGoldenResourceDeletesLinks() throws InterruptedException { - myMdmHelper.createWithLatch(buildPaulPatient()); - assertLinkCount(1); - Patient sourcePatient = getOnlyGoldenPatient(); - myPatientDao.delete(sourcePatient.getIdElement()); - assertLinkCount(0); - } - - @Test - public void testCreatePatientWithMdmTagForbidden() throws InterruptedException { - //Creating a golden resource with the MDM-MANAGED tag should fail - Patient patient = new Patient(); - patient.getMeta().addTag(SYSTEM_MDM_MANAGED, CODE_HAPI_MDM_MANAGED, "User is managed by MDM"); - try { - myMdmHelper.doCreateResource(patient, true); - fail(); - } catch (ForbiddenOperationException e) { - assertThat(e.getMessage(), startsWith("Cannot create or modify Resources that are managed by MDM.")); - } - } - - @Test - public void testCreatePatientWithGoldenRecordTagForbidden() throws InterruptedException { - Patient patient = new Patient(); - patient.getMeta().addTag(SYSTEM_GOLDEN_RECORD_STATUS, CODE_GOLDEN_RECORD, "Golden Record"); - try { - myMdmHelper.doCreateResource(patient, true); - fail(); - } catch (ForbiddenOperationException e) { - assertThat(e.getMessage(), startsWith("Cannot create or modify Resources that are managed by MDM.")); - } - } - - @Test - public void testCreateMedicationWithGoldenRecordRedirectTagForbidden() throws InterruptedException { - Medication medication = new Medication(); - medication.getMeta().addTag(SYSTEM_GOLDEN_RECORD_STATUS, CODE_GOLDEN_RECORD_REDIRECTED, "Golden Record"); - try { - myMdmHelper.doCreateResource(medication, true); - fail(); - } catch (ForbiddenOperationException e) { - assertThat(e.getMessage(), startsWith("Cannot create or modify Resources that are managed by MDM.")); - } - } - - @Test - public void testCreatingGoldenResourceWithInsufficentMDMAttributesIsNotMDMProcessed() throws InterruptedException { - myMdmHelper.doCreateResource(new Patient(), true); - assertLinkCount(0); - } - - @Test - public void testCreatingPatientWithOneOrMoreMatchingAttributesIsMDMProcessed() throws InterruptedException { - myMdmHelper.createWithLatch(buildPaulPatient()); - assertLinkCount(1); - } - - @Test - public void testCreateOrganizationWithMdmTagForbidden() throws InterruptedException { - //Creating a organization with the MDM-MANAGED tag should fail - Organization organization = new Organization(); - organization.getMeta().addTag(SYSTEM_MDM_MANAGED, CODE_HAPI_MDM_MANAGED, "User is managed by MDM"); - try { - myMdmHelper.doCreateResource(organization, true); - fail(); - } catch (ForbiddenOperationException e) { - assertThat(e.getMessage(), startsWith("Cannot create or modify Resources that are managed by MDM.")); - } - } - - @Test - public void testUpdateOrganizationWithMdmTagForbidden() throws InterruptedException { - //Creating a organization with the MDM-MANAGED tag should fail - Organization organization = new Organization(); - myMdmHelper.doCreateResource(organization, true); - organization.getMeta().addTag(SYSTEM_MDM_MANAGED, CODE_HAPI_MDM_MANAGED, "User is managed by MDM"); - try { - myMdmHelper.doUpdateResource(organization, true); - fail(); - } catch (ForbiddenOperationException e) { - assertEquals("The HAPI-MDM tag on a resource may not be changed once created.", e.getMessage()); - } - } - - @Test - public void testGoldenResourceRecordsManagedByMdmAllShareSameTag() throws InterruptedException { - myMdmHelper.createWithLatch(buildJanePatient()); - myMdmHelper.createWithLatch(buildPaulPatient()); - - //TODO GGG MDM: this test is out of date, since we now are using golden record Patients - IBundleProvider search = myPatientDao.search(buildGoldenResourceSearchParameterMap()); - List resources = search.getResources(0, search.size()); - - for (IBaseResource r : resources) { - assertThat(r.getMeta().getTag(SYSTEM_MDM_MANAGED, CODE_HAPI_MDM_MANAGED), is(notNullValue())); - } - } - - @Test - public void testNonMdmManagedGoldenResourceCannotHaveMdmManagedTagAddedToThem() { - // GoldenResource created manually. - Patient patient = new Patient(); - DaoMethodOutcome daoMethodOutcome = myMdmHelper.doCreateResource(patient, true); - assertNotNull(daoMethodOutcome.getId()); - - //Updating that patient to set them as MDM managed is not allowed. - patient.getMeta().addTag(SYSTEM_MDM_MANAGED, CODE_HAPI_MDM_MANAGED, "User is managed by MDM"); - try { - myMdmHelper.doUpdateResource(patient, true); - fail(); - } catch (ForbiddenOperationException e) { - assertEquals("The HAPI-MDM tag on a resource may not be changed once created.", e.getMessage()); - } - } - - @Test - public void testMdmManagedGoldenResourceCannotBeModifiedByGoldenResourceUpdateRequest() throws InterruptedException { - // When MDM is enabled, only the MDM system is allowed to modify GoldenResource links of GoldenResources with the MDM-MANAGED tag. - Patient patient = new Patient(); - IIdType patientId = myMdmHelper.createWithLatch(buildPaulPatient()).getDaoMethodOutcome().getId().toUnqualifiedVersionless(); - - patient.setId(patientId); - - // Updating a Golden Resource Patient who was created via MDM should fail. - MdmLink mdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(myIdHelperService.getPidOrNull(patient)).get(); - Long sourcePatientPid = mdmLink.getGoldenResourcePid(); - Patient goldenResourcePatient = (Patient) myPatientDao.readByPid(new ResourcePersistentId(sourcePatientPid)); - goldenResourcePatient.setGender(Enumerations.AdministrativeGender.MALE); - try { - myMdmHelper.doUpdateResource(goldenResourcePatient, true); - fail(); - } catch (ForbiddenOperationException e) { - assertThat(e.getMessage(), startsWith("Cannot create or modify Resources that are managed by MDM.")); - } - } - - @Test - public void testMdmPointcutReceivesTransactionLogMessages() throws InterruptedException { - MdmHelperR4.OutcomeAndLogMessageWrapper wrapper = myMdmHelper.createWithLatch(buildJanePatient()); - - TransactionLogMessages mdmTransactionLogMessages = wrapper.getLogMessages(); - - //There is no TransactionGuid here as there is no TransactionLog in this context. - assertThat(mdmTransactionLogMessages.getTransactionGuid(), is(nullValue())); - - List messages = mdmTransactionLogMessages.getValues(); - assertThat(messages.isEmpty(), is(false)); - } - - @Test - public void testWhenASingularPatientUpdatesExternalEidThatGoldenResourceEidIsUpdated() throws InterruptedException { - Patient jane = addExternalEID(buildJanePatient(), "some_eid"); - MdmHelperR4.OutcomeAndLogMessageWrapper latch = myMdmHelper.createWithLatch(jane); - jane.setId(latch.getDaoMethodOutcome().getId()); - clearExternalEIDs(jane); - jane = addExternalEID(jane, "some_new_eid"); - - MdmHelperR4.OutcomeAndLogMessageWrapper outcomeWrapper = myMdmHelper.updateWithLatch(jane); - IAnyResource patient = getGoldenResourceFromTargetResource(jane); - List externalEids = myEIDHelper.getExternalEid(patient); - 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"); - MdmHelperR4.OutcomeAndLogMessageWrapper latch = myMdmHelper.createWithLatch(jane); - jane.setId(latch.getDaoMethodOutcome().getId()); - clearExternalEIDs(jane); - jane = addExternalEID(jane, "some_new_eid"); - try { - myMdmHelper.doUpdateResource(jane, true); - fail(); - } catch (ForbiddenOperationException e) { - assertThat(e.getMessage(), is(equalTo("While running with EID updates disabled, EIDs may not be updated on source resources"))); - } - setPreventEidUpdates(false); - } - - @Test - public void testWhenMultipleEidsAreDisabledThatTheInterceptorRejectsCreatesWithThem() { - setPreventMultipleEids(true); - Patient patient = buildJanePatient(); - addExternalEID(patient, "123"); - addExternalEID(patient, "456"); - try { - myMdmHelper.doCreateResource(patient, true); - fail(); - } catch (ForbiddenOperationException e) { - assertThat(e.getMessage(), is(equalTo("While running with multiple EIDs disabled, source resources may have at most one EID."))); - } - - setPreventMultipleEids(false); - } - - @Test - public void testInterceptorHandlesNonMdmResources() { - 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); - - myMdmHelper.doCreateResource(fooSp, true); - fooSp.setXpathUsage(SearchParameter.XPathUsageType.PHONETIC); - myMdmHelper.doUpdateResource(fooSp, true); - } - - @Test - public void testPatientsWithNoEIDCanBeUpdated() throws InterruptedException { - setPreventEidUpdates(true); - Patient p = buildPaulPatient(); - MdmHelperR4.OutcomeAndLogMessageWrapper wrapper = myMdmHelper.createWithLatch(p); - - p.setId(wrapper.getDaoMethodOutcome().getId()); - p.setBirthDate(new Date()); - myMdmHelper.updateWithLatch(p); - setPreventEidUpdates(false); - } - - @Test - public void testPatientsCanHaveEIDAddedInStrictMode() throws InterruptedException { - setPreventEidUpdates(true); - Patient p = buildPaulPatient(); - MdmHelperR4.OutcomeAndLogMessageWrapper messageWrapper = myMdmHelper.createWithLatch(p); - p.setId(messageWrapper.getDaoMethodOutcome().getId()); - addExternalEID(p, "external eid"); - myMdmHelper.updateWithLatch(p); - setPreventEidUpdates(false); - } - - private void setPreventEidUpdates(boolean thePrevent) { - ((MdmSettings) myMdmSettings).setPreventEidUpdates(thePrevent); - } - - private void setPreventMultipleEids(boolean thePrevent) { - ((MdmSettings) myMdmSettings).setPreventMultipleEids(thePrevent); - } - -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/BaseGoldenResourceMatcher.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/BaseGoldenResourceMatcher.java deleted file mode 100644 index a72ba74c9e9..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/BaseGoldenResourceMatcher.java +++ /dev/null @@ -1,78 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.matcher; - -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.util.MdmResourceUtil; -import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -import ca.uhn.fhir.jpa.entity.MdmLink; -import org.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 BaseGoldenResourceMatcher extends TypeSafeMatcher { - - private static final Logger ourLog = LoggerFactory.getLogger(BaseGoldenResourceMatcher.class); - - protected IdHelperService myIdHelperService; - protected MdmLinkDaoSvc myMdmLinkDaoSvc; - protected Collection myBaseResources; - protected String myTargetType; - - protected BaseGoldenResourceMatcher(IdHelperService theIdHelperService, MdmLinkDaoSvc theMdmLinkDaoSvc, IAnyResource... theBaseResource) { - myIdHelperService = theIdHelperService; - myMdmLinkDaoSvc = theMdmLinkDaoSvc; - myBaseResources = Arrays.stream(theBaseResource).collect(Collectors.toList()); - } - - @Nullable - protected Long getMatchedResourcePidFromResource(IAnyResource theResource) { - Long retval; - - boolean isGoldenRecord = MdmResourceUtil.isMdmManaged(theResource); - if (isGoldenRecord) { - return myIdHelperService.getPidOrNull(theResource); - } - MdmLink matchLink = getMatchedMdmLink(theResource); - - if (matchLink == null) { - return null; - } else { - retval = matchLink.getGoldenResourcePid(); - myTargetType = matchLink.getMdmSourceType(); - } - return retval; - } - - protected List getPossibleMatchedGoldenResourcePidsFromTarget(IAnyResource theBaseResource) { - return getMdmLinksForTarget(theBaseResource, MdmMatchResultEnum.POSSIBLE_MATCH).stream().map(MdmLink::getGoldenResourcePid).collect(Collectors.toList()); - } - - protected MdmLink getMatchedMdmLink(IAnyResource thePatientOrPractitionerResource) { - List mdmLinks = getMdmLinksForTarget(thePatientOrPractitionerResource, MdmMatchResultEnum.MATCH); - if (mdmLinks.size() == 0) { - return null; - } else if (mdmLinks.size() == 1) { - return mdmLinks.get(0); - } else { - throw new IllegalStateException("Its illegal to have more than 1 match for a given target! we found " + mdmLinks.size() + " for resource with id: " + thePatientOrPractitionerResource.getIdElement().toUnqualifiedVersionless()); - } - } - - protected List getMdmLinksForTarget(IAnyResource theTargetResource, MdmMatchResultEnum theMatchResult) { - Long pidOrNull = myIdHelperService.getPidOrNull(theTargetResource); - List matchLinkForTarget = myMdmLinkDaoSvc.getMdmLinksBySourcePidAndMatchResult(pidOrNull, theMatchResult); - if (!matchLinkForTarget.isEmpty()) { - return matchLinkForTarget; - } else { - return new ArrayList<>(); - } - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsLinkedTo.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsLinkedTo.java deleted file mode 100644 index 67267bfbe75..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsLinkedTo.java +++ /dev/null @@ -1,48 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.matcher; - -import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -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 golden resource. - * - */ -public class IsLinkedTo extends BaseGoldenResourceMatcher { - - private List baseResourceGoldenResourcePids; - private Long incomingResourceGoldenResourcePid; - - protected IsLinkedTo(IdHelperService theIdHelperService, MdmLinkDaoSvc theMdmLinkDaoSvc, IAnyResource... theBaseResource) { - super(theIdHelperService, theMdmLinkDaoSvc, theBaseResource); - } - - - @Override - protected boolean matchesSafely(IAnyResource theIncomingResource) { - incomingResourceGoldenResourcePid = getMatchedResourcePidFromResource(theIncomingResource); - - //OK, lets grab all the golden resource pids of the resources passed in via the constructor. - baseResourceGoldenResourcePids = myBaseResources.stream() - .map(this::getMatchedResourcePidFromResource) - .collect(Collectors.toList()); - - //The resources are linked if all golden resource pids match the incoming golden resource pid. - return baseResourceGoldenResourcePids.stream() - .allMatch(pid -> pid.equals(incomingResourceGoldenResourcePid)); - } - - @Override - public void describeTo(Description theDescription) { - } - - public static Matcher linkedTo(IdHelperService theIdHelperService, MdmLinkDaoSvc theMdmLinkDaoSvc, IAnyResource... theBaseResource) { - return new IsLinkedTo(theIdHelperService, theMdmLinkDaoSvc, theBaseResource); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsMatchedToAGoldenResource.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsMatchedToAGoldenResource.java deleted file mode 100644 index 1ef0994b454..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsMatchedToAGoldenResource.java +++ /dev/null @@ -1,37 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.matcher; - -import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -import ca.uhn.fhir.jpa.entity.MdmLink; -import org.hamcrest.Description; -import org.hamcrest.Matcher; -import org.hamcrest.TypeSafeMatcher; -import org.hl7.fhir.instance.model.api.IAnyResource; - -import java.util.Optional; - -public class IsMatchedToAGoldenResource extends TypeSafeMatcher { - - private final IdHelperService myIdHelperService; - private final MdmLinkDaoSvc myMdmLinkDaoSvc; - - public IsMatchedToAGoldenResource(IdHelperService theIdHelperService, MdmLinkDaoSvc theMdmLinkDaoSvc) { - myIdHelperService = theIdHelperService; - myMdmLinkDaoSvc = theMdmLinkDaoSvc; - } - - @Override - protected boolean matchesSafely(IAnyResource theIncomingResource) { - Optional matchedLinkForTargetPid = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(myIdHelperService.getPidOrNull(theIncomingResource)); - return matchedLinkForTargetPid.isPresent(); - } - - @Override - public void describeTo(Description theDescription) { - theDescription.appendText("target was not linked to a Golden Resource."); - } - - public static Matcher matchedToAGoldenResource(IdHelperService theIdHelperService, MdmLinkDaoSvc theMdmLinkDaoSvc) { - return new IsMatchedToAGoldenResource(theIdHelperService, theMdmLinkDaoSvc); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsPossibleDuplicateOf.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsPossibleDuplicateOf.java deleted file mode 100644 index fb7cf3af9cf..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsPossibleDuplicateOf.java +++ /dev/null @@ -1,61 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.matcher; - -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -import ca.uhn.fhir.jpa.entity.MdmLink; -import org.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 BaseGoldenResourceMatcher { - - /** - * Matcher with tells us if there is an MdmLink with between these two resources that are considered POSSIBLE DUPLICATE. - * For use only on GoldenResource. - */ - private Long incomingGoldenResourcePid; - - protected IsPossibleDuplicateOf(IdHelperService theIdHelperService, MdmLinkDaoSvc theMdmLinkDaoSvc, IAnyResource... theBaseResource) { - super(theIdHelperService, theMdmLinkDaoSvc, theBaseResource); - } - - @Override - protected boolean matchesSafely(IAnyResource theIncomingResource) { - incomingGoldenResourcePid = getMatchedResourcePidFromResource(theIncomingResource); - - List goldenResourcePidsToMatch = myBaseResources.stream() - .map(this::getMatchedResourcePidFromResource) - .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 goldenResourcePidsToMatch.stream() - .map(baseResourcePid -> { - Optional duplicateLink = myMdmLinkDaoSvc.getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(baseResourcePid, incomingGoldenResourcePid, MdmMatchResultEnum.POSSIBLE_DUPLICATE); - if (!duplicateLink.isPresent()) { - duplicateLink = myMdmLinkDaoSvc.getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(incomingGoldenResourcePid, baseResourcePid, MdmMatchResultEnum.POSSIBLE_DUPLICATE); - } - return duplicateLink; - }).allMatch(Optional::isPresent); - } - - @Override - public void describeTo(Description theDescription) { - theDescription.appendText("Resource was not duplicate of Resource/" + incomingGoldenResourcePid); - } - - @Override - protected void describeMismatchSafely(IAnyResource item, Description mismatchDescription) { - super.describeMismatchSafely(item, mismatchDescription); - mismatchDescription.appendText("No MdmLink With POSSIBLE_DUPLICATE was found"); - } - - public static Matcher possibleDuplicateOf(IdHelperService theIdHelperService, MdmLinkDaoSvc theMdmLinkDaoSvc, IAnyResource... theBaseResource) { - return new IsPossibleDuplicateOf(theIdHelperService, theMdmLinkDaoSvc, theBaseResource); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsPossibleLinkedTo.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsPossibleLinkedTo.java deleted file mode 100644 index 82bc9d5e9f6..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsPossibleLinkedTo.java +++ /dev/null @@ -1,46 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.matcher; - -import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -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 resource at a given link level - * is linked to a set of target resources via a golden resource. - */ -public class IsPossibleLinkedTo extends BaseGoldenResourceMatcher { - - private List baseResourceGoldenResourcePids; - private Long incomingResourceGoldenResourcePid; - - protected IsPossibleLinkedTo(IdHelperService theIdHelperService, MdmLinkDaoSvc theMdmLinkDaoSvc, IAnyResource... theTargetResources) { - super(theIdHelperService, theMdmLinkDaoSvc, theTargetResources); - } - - @Override - protected boolean matchesSafely(IAnyResource theGoldenResource) { - incomingResourceGoldenResourcePid = myIdHelperService.getPidOrNull(theGoldenResource); - - //OK, lets grab all the golden resource pids of the resources passed in via the constructor. - baseResourceGoldenResourcePids = myBaseResources.stream() - .flatMap(iBaseResource -> getPossibleMatchedGoldenResourcePidsFromTarget(iBaseResource).stream()) - .collect(Collectors.toList()); - - //The resources are linked if all golden resource pids match the incoming golden resource pid. - return baseResourceGoldenResourcePids.stream() - .allMatch(pid -> pid.equals(incomingResourceGoldenResourcePid)); - } - - @Override - public void describeTo(Description theDescription) { - } - - public static Matcher possibleLinkedTo(IdHelperService theIdHelperService, MdmLinkDaoSvc theMdmLinkDaoSvc, IAnyResource... theBaseResource) { - return new IsPossibleLinkedTo(theIdHelperService, theMdmLinkDaoSvc, theBaseResource); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsPossibleMatchWith.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsPossibleMatchWith.java deleted file mode 100644 index e4b62a686bc..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsPossibleMatchWith.java +++ /dev/null @@ -1,60 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.matcher; - -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -import ca.uhn.fhir.jpa.entity.MdmLink; -import org.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 MdmLink with between these two resources that are considered POSSIBLE_MATCH - */ -public class IsPossibleMatchWith extends BaseGoldenResourceMatcher { - - protected IsPossibleMatchWith(IdHelperService theIdHelperService, MdmLinkDaoSvc theMdmLinkDaoSvc, IAnyResource... theBaseResource) { - super(theIdHelperService, theMdmLinkDaoSvc, theBaseResource); - } - - @Override - protected boolean matchesSafely(IAnyResource theIncomingResource) { - List mdmLinks = getMdmLinksForTarget(theIncomingResource, MdmMatchResultEnum.POSSIBLE_MATCH); - - List goldenResourcePidsToMatch = myBaseResources.stream() - .map(this::getMatchedResourcePidFromResource) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - - if (goldenResourcePidsToMatch.isEmpty()) { - goldenResourcePidsToMatch = myBaseResources.stream() - .flatMap(iBaseResource -> getPossibleMatchedGoldenResourcePidsFromTarget(iBaseResource).stream()) - .collect(Collectors.toList()); - } - - List mdmLinkGoldenResourcePids = mdmLinks - .stream().map(MdmLink::getGoldenResourcePid) - .collect(Collectors.toList()); - - return mdmLinkGoldenResourcePids.containsAll(goldenResourcePidsToMatch); - } - - @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 MDM Link With POSSIBLE_MATCH was found"); - } - - public static Matcher possibleMatchWith(IdHelperService theIdHelperService, MdmLinkDaoSvc theMdmLinkDaoSvc, IAnyResource... theBaseResource) { - return new IsPossibleMatchWith(theIdHelperService, theMdmLinkDaoSvc, theBaseResource); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsSameGoldenResourceAs.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsSameGoldenResourceAs.java deleted file mode 100644 index 16ddb5bd510..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/matcher/IsSameGoldenResourceAs.java +++ /dev/null @@ -1,46 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.matcher; - -import ca.uhn.fhir.jpa.dao.index.IdHelperService; -import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; -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 IsSameGoldenResourceAs extends BaseGoldenResourceMatcher { - - private List goldenResourcePidsToMatch; - private Long incomingGoldenResourcePid; - - public IsSameGoldenResourceAs(IdHelperService theIdHelperService, MdmLinkDaoSvc theMdmLinkDaoSvc, IAnyResource... theBaseResource) { - super(theIdHelperService, theMdmLinkDaoSvc, theBaseResource); - } - - @Override - protected boolean matchesSafely(IAnyResource theIncomingResource) { - incomingGoldenResourcePid = getMatchedResourcePidFromResource(theIncomingResource); - goldenResourcePidsToMatch = myBaseResources.stream().map(this::getMatchedResourcePidFromResource).collect(Collectors.toList()); - boolean allToCheckAreSame = goldenResourcePidsToMatch.stream().allMatch(pid -> pid.equals(goldenResourcePidsToMatch.get(0))); - if (!allToCheckAreSame) { - throw new IllegalStateException("You wanted to do a source resource comparison, but the pool of source resources you submitted for checking don't match! We won't even check the incoming source resource against them."); - } - return goldenResourcePidsToMatch.contains(incomingGoldenResourcePid); - } - - @Override - public void describeTo(Description theDescription) { - theDescription.appendText(String.format(" %s linked to source resource %s/%s", myTargetType, myTargetType, goldenResourcePidsToMatch)); - } - - @Override - protected void describeMismatchSafely(IAnyResource item, Description mismatchDescription) { - super.describeMismatchSafely(item, mismatchDescription); - mismatchDescription.appendText(String.format(" was actually linked to %s/%s", myTargetType, incomingGoldenResourcePid)); - } - - public static Matcher sameGoldenResourceAs(IdHelperService theIdHelperService, MdmLinkDaoSvc theMdmLinkDaoSvc, IAnyResource... theBaseResource) { - return new IsSameGoldenResourceAs(theIdHelperService, theMdmLinkDaoSvc, theBaseResource); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/BaseProviderR4Test.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/BaseProviderR4Test.java deleted file mode 100644 index ed3eb1d6d4f..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/BaseProviderR4Test.java +++ /dev/null @@ -1,53 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.provider; - -import ca.uhn.fhir.mdm.api.IMdmControllerSvc; -import ca.uhn.fhir.mdm.api.IMdmExpungeSvc; -import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc; -import ca.uhn.fhir.mdm.api.IMdmSubmitSvc; -import ca.uhn.fhir.mdm.provider.MdmProviderR4; -import ca.uhn.fhir.mdm.rules.config.MdmSettings; -import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; -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 BaseMdmR4Test { - MdmProviderR4 myMdmProviderR4; - @Autowired - private IMdmMatchFinderSvc myMdmMatchFinderSvc; - @Autowired - private IMdmControllerSvc myMdmControllerSvc; - @Autowired - private IMdmExpungeSvc myMdmExpungeSvc; - @Autowired - private IMdmSubmitSvc myMdmSubmitSvc; - @Autowired - private MdmSettings myMdmSettings; - - private String defaultScript; - - protected void setMdmRuleJson(String theString) throws IOException { - DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); - Resource resource = resourceLoader.getResource(theString); - String json = IOUtils.toString(resource.getInputStream(), Charsets.UTF_8); - myMdmSettings.setScriptText(json); - - } - - @BeforeEach - public void before() { - myMdmProviderR4 = new MdmProviderR4(myFhirContext, myMdmControllerSvc, myMdmMatchFinderSvc, myMdmExpungeSvc, myMdmSubmitSvc); - defaultScript = myMdmSettings.getScriptText(); - } - @AfterEach - public void after() throws IOException { - super.after(); - myMdmSettings.setScriptText(defaultScript); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderBatchR4Test.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderBatchR4Test.java deleted file mode 100644 index 188cf4fd0c3..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderBatchR4Test.java +++ /dev/null @@ -1,155 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.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.instance.model.api.IAnyResource; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Medication; -import org.hl7.fhir.r4.model.Organization; -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 MdmProviderBatchR4Test extends BaseLinkR4Test { - - public static final String ORGANIZATION_DUMMY = "Organization/dummy"; - protected Practitioner myPractitioner; - protected StringType myPractitionerId; - protected IAnyResource myGoldenPractitioner; - protected StringType myGoldenPractitionerId; - protected Medication myMedication; - protected StringType myMedicationId; - protected IAnyResource myGoldenMedication; - protected StringType myGoldenMedicationId; - - - @Autowired - IInterceptorService myInterceptorService; - PointcutLatch afterMdmLatch = new PointcutLatch(Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED); - - - @BeforeEach - public void before() { - super.before(); - myPractitioner = createPractitionerAndUpdateLinks(buildPractitionerWithNameAndId("some_pract", "some_pract_id")); - myPractitionerId = new StringType(myPractitioner.getIdElement().getValue()); - myGoldenPractitioner = getGoldenResourceFromTargetResource(myPractitioner); - myGoldenPractitionerId = new StringType(myGoldenPractitioner.getIdElement().getValue()); - - Organization dummyOrganization = new Organization(); - dummyOrganization.setId(ORGANIZATION_DUMMY); - myOrganizationDao.update(dummyOrganization); - - myMedication = createMedicationAndUpdateLinks(buildMedication(ORGANIZATION_DUMMY)); - myMedicationId = new StringType(myMedication.getIdElement().getValue()); - myGoldenMedication = getGoldenResourceFromTargetResource(myMedication); - myGoldenMedicationId = new StringType(myGoldenMedication.getIdElement().getValue()); - - - myInterceptorService.registerAnonymousInterceptor(Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED, afterMdmLatch); - } - - @AfterEach - public void after() throws IOException { - myInterceptorService.unregisterInterceptor(afterMdmLatch); - super.after(); - } - - @Test - public void testBatchRunOnAllMedications() throws InterruptedException { - StringType criteria = null; - myMdmProviderR4.clearMdmLinks(null, myRequestDetails); - - afterMdmLatch.runWithExpectedCount(1, () -> myMdmProviderR4.mdmBatchOnAllSourceResources(new StringType("Medication"), criteria, null)); - assertLinkCount(1); - } - - @Test - public void testBatchRunOnAllPractitioners() throws InterruptedException { - StringType criteria = null; - myMdmProviderR4.clearMdmLinks(null, myRequestDetails); - - afterMdmLatch.runWithExpectedCount(1, () -> myMdmProviderR4.mdmBatchPractitionerType(criteria, null)); - assertLinkCount(1); - } - @Test - public void testBatchRunOnSpecificPractitioner() throws InterruptedException { - myMdmProviderR4.clearMdmLinks(null, myRequestDetails); - afterMdmLatch.runWithExpectedCount(1, () -> myMdmProviderR4.mdmBatchPractitionerInstance(myPractitioner.getIdElement(), null)); - assertLinkCount(1); - } - - @Test - public void testBatchRunOnNonExistentSpecificPractitioner() { - myMdmProviderR4.clearMdmLinks(null, myRequestDetails); - try { - myMdmProviderR4.mdmBatchPractitionerInstance(new IdType("Practitioner/999"), null); - fail(); - } catch (ResourceNotFoundException e){} - } - - @Test - public void testBatchRunOnAllPatients() throws InterruptedException { - assertLinkCount(3); - StringType criteria = null; - myMdmProviderR4.clearMdmLinks(null, myRequestDetails); - afterMdmLatch.runWithExpectedCount(1, () -> myMdmProviderR4.mdmBatchPatientType(criteria, null)); - assertLinkCount(1); - } - - @Test - public void testBatchRunOnSpecificPatient() throws InterruptedException { - assertLinkCount(3); - myMdmProviderR4.clearMdmLinks(null, myRequestDetails); - afterMdmLatch.runWithExpectedCount(1, () -> myMdmProviderR4.mdmBatchPatientInstance(myPatient.getIdElement(), null)); - assertLinkCount(1); - } - - @Test - public void testBatchRunOnNonExistentSpecificPatient() { - assertLinkCount(3); - myMdmProviderR4.clearMdmLinks(null, myRequestDetails); - try { - myMdmProviderR4.mdmBatchPatientInstance(new IdType("Patient/999"), null); - fail(); - } catch (ResourceNotFoundException e){} - } - - @Test - public void testBatchRunOnAllTypes() throws InterruptedException { - assertLinkCount(3); - StringType criteria = new StringType(""); - myMdmProviderR4.clearMdmLinks(null, myRequestDetails); - afterMdmLatch.runWithExpectedCount(3, () -> { - myMdmProviderR4.mdmBatchOnAllSourceResources(null, criteria, null); - }); - assertLinkCount(3); - } - - @Test - public void testBatchRunOnAllTypesWithInvalidCriteria() { - assertLinkCount(3); - StringType criteria = new StringType("death-date=2020-06-01"); - myMdmProviderR4.clearMdmLinks(null, myRequestDetails); - - try { - myMdmProviderR4.mdmBatchPractitionerType(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"))); - } - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderClearLinkR4Test.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderClearLinkR4Test.java deleted file mode 100644 index 97b5d6ba866..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderClearLinkR4Test.java +++ /dev/null @@ -1,182 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.provider; - -import ca.uhn.fhir.mdm.api.MdmConstants; -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.jpa.entity.MdmLink; -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.param.TokenParam; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import org.hl7.fhir.instance.model.api.IAnyResource; -import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.Practitioner; -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 ca.uhn.fhir.mdm.api.MdmMatchOutcome.POSSIBLE_MATCH; -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 MdmProviderClearLinkR4Test extends BaseLinkR4Test { - - protected Practitioner myPractitioner; - protected StringType myPractitionerId; - protected IAnyResource myPractitionerGoldenResource; - protected StringType myPractitionerGoldenResourceId; - - @BeforeEach - public void before() { - super.before(); - myPractitioner = createPractitionerAndUpdateLinks(new Practitioner()); - myPractitionerId = new StringType(myPractitioner.getIdElement().getValue()); - myPractitionerGoldenResource = getGoldenResourceFromTargetResource(myPractitioner); - myPractitionerGoldenResourceId = new StringType(myPractitionerGoldenResource.getIdElement().getValue()); - } - - @Test - public void testClearAllLinks() { - assertLinkCount(2); - myMdmProviderR4.clearMdmLinks(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); - Patient read = myPatientDao.read(new IdDt(mySourcePatientId.getValueAsString()).toVersionless()); - assertThat(read, is(notNullValue())); - myMdmProviderR4.clearMdmLinks(new StringType("Patient"), myRequestDetails); - assertNoPatientLinksExist(); - try { - myPatientDao.read(new IdDt(mySourcePatientId.getValueAsString()).toVersionless()); - fail(); - } catch (ResourceNotFoundException e) {} - - } - @Test - public void testGoldenResourceWithMultipleHistoricalVersionsCanBeDeleted() { - createPatientAndUpdateLinks(buildJanePatient()); - createPatientAndUpdateLinks(buildJanePatient()); - createPatientAndUpdateLinks(buildJanePatient()); - createPatientAndUpdateLinks(buildJanePatient()); - Patient patientAndUpdateLinks = createPatientAndUpdateLinks(buildJanePatient()); - IAnyResource goldenResource = getGoldenResourceFromTargetResource(patientAndUpdateLinks); - assertThat(goldenResource, is(notNullValue())); - myMdmProviderR4.clearMdmLinks(null, myRequestDetails); - assertNoPatientLinksExist(); - goldenResource = getGoldenResourceFromTargetResource(patientAndUpdateLinks); - assertThat(goldenResource, is(nullValue())); - } - - @Test - public void testGoldenResourceWithLinksToOtherGoldenResourcesCanBeDeleted() { - createPatientAndUpdateLinks(buildJanePatient()); - Patient patientAndUpdateLinks1 = createPatientAndUpdateLinks(buildJanePatient()); - Patient patientAndUpdateLinks = createPatientAndUpdateLinks(buildPaulPatient()); - - IAnyResource goldenResourceFromTarget = getGoldenResourceFromTargetResource(patientAndUpdateLinks); - IAnyResource goldenResourceFromTarget2 = getGoldenResourceFromTargetResource(patientAndUpdateLinks1); - linkGoldenResources(goldenResourceFromTarget, goldenResourceFromTarget2); - - //SUT - myMdmProviderR4.clearMdmLinks(null, myRequestDetails); - - assertNoPatientLinksExist(); - IBundleProvider search = myPatientDao.search(buildGoldenResourceParameterMap()); - assertThat(search.size(), is(equalTo(0))); - } - - /** - * Build a SearchParameterMap which looks up Golden Records (Source resources). - * @return - */ - private SearchParameterMap buildGoldenResourceParameterMap() { - return new SearchParameterMap().setLoadSynchronous(true).add("_tag", new TokenParam(MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_HAPI_MDM_MANAGED)); - } - - @Test - public void testGoldenResourceWithCircularReferenceCanBeCleared() { - Patient patientAndUpdateLinks = createPatientAndUpdateLinks(buildPaulPatient()); - Patient patientAndUpdateLinks1 = createPatientAndUpdateLinks(buildJanePatient()); - Patient patientAndUpdateLinks2 = createPatientAndUpdateLinks(buildFrankPatient()); - - IAnyResource goldenResourceFromTarget = getGoldenResourceFromTargetResource(patientAndUpdateLinks); - IAnyResource goldenResourceFromTarget1 = getGoldenResourceFromTargetResource(patientAndUpdateLinks1); - IAnyResource goldenResourceFromTarget2 = getGoldenResourceFromTargetResource(patientAndUpdateLinks2); - - // A -> B -> C -> A linkages. - linkGoldenResources(goldenResourceFromTarget, goldenResourceFromTarget1); - linkGoldenResources(goldenResourceFromTarget1, goldenResourceFromTarget2); - linkGoldenResources(goldenResourceFromTarget2, goldenResourceFromTarget); - - //SUT - Parameters parameters = myMdmProviderR4.clearMdmLinks(null, myRequestDetails); - - printLinks(); - - assertNoPatientLinksExist(); - IBundleProvider search = myPatientDao.search(buildGoldenResourceParameterMap()); - assertThat(search.size(), is(equalTo(0))); - - } - - //TODO GGG unclear if we actually need to reimplement this. - private void linkGoldenResources(IAnyResource theGoldenResource, IAnyResource theTargetResource) { - // TODO NG - Should be ok to leave this - not really - // throw new UnsupportedOperationException("We need to fix this!"); - myMdmLinkDaoSvc.createOrUpdateLinkEntity(theGoldenResource, theTargetResource, POSSIBLE_MATCH, MdmLinkSourceEnum.AUTO, createContextForCreate("Patient")); - } - - @Test - public void testClearPractitionerLinks() { - assertLinkCount(2); - Practitioner read = myPractitionerDao.read(new IdDt(myPractitionerGoldenResourceId.getValueAsString()).toVersionless()); - assertThat(read, is(notNullValue())); - myMdmProviderR4.clearMdmLinks(new StringType("Practitioner"), myRequestDetails); - assertNoPractitionerLinksExist(); - try { - myPractitionerDao.read(new IdDt(myPractitionerGoldenResourceId.getValueAsString()).toVersionless()); - fail(); - } catch (ResourceNotFoundException e) {} - } - - @Test - public void testClearInvalidTargetType() { - try { - myMdmProviderR4.clearMdmLinks(new StringType("Observation"), myRequestDetails); - fail(); - } catch (InvalidRequestException e) { - assertThat(e.getMessage(), is(equalTo("$mdm-clear does not support resource type: Observation"))); - } - } - - @Nonnull - protected List getPractitionerLinks() { - return myMdmLinkDaoSvc.findMdmLinksBySourceResource(myPractitioner); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderMergeGoldenResourcesR4Test.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderMergeGoldenResourcesR4Test.java deleted file mode 100644 index 0e8e382a6db..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderMergeGoldenResourcesR4Test.java +++ /dev/null @@ -1,134 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.provider; - -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.util.MdmResourceUtil; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import org.hl7.fhir.r4.model.Patient; -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.hasSize; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertTrue; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.fail; - -public class MdmProviderMergeGoldenResourcesR4Test extends BaseProviderR4Test { - - private Patient myFromGoldenPatient; - private StringType myFromGoldenPatientId; - private Patient myToGoldenPatient; - private StringType myToGoldenPatientId; - - @Override - @BeforeEach - public void before() { - super.before(); - - myFromGoldenPatient = createGoldenPatient(); - myFromGoldenPatientId = new StringType(myFromGoldenPatient.getIdElement().getValue()); - myToGoldenPatient = createGoldenPatient(); - myToGoldenPatientId = new StringType(myToGoldenPatient.getIdElement().getValue()); - } - - @Test - public void testMerge() { - Patient mergedSourcePatient = (Patient) myMdmProviderR4.mergeGoldenResources(myFromGoldenPatientId, - myToGoldenPatientId, myRequestDetails); - - assertTrue(MdmResourceUtil.isGoldenRecord(myFromGoldenPatient)); - assertEquals(myToGoldenPatient.getIdElement(), mergedSourcePatient.getIdElement()); - assertThat(mergedSourcePatient, is(sameGoldenResourceAs(myToGoldenPatient))); - assertEquals(1, getAllRedirectedGoldenPatients().size()); - assertEquals(1, getAllGoldenPatients().size()); - - Patient fromSourcePatient = myPatientDao.read(myFromGoldenPatient.getIdElement().toUnqualifiedVersionless()); - assertThat(fromSourcePatient.getActive(), is(false)); - assertTrue(MdmResourceUtil.isGoldenRecordRedirected(fromSourcePatient)); - - //TODO GGG eventually this will need to check a redirect... this is a hack which doesnt work - // Optional redirect = fromSourcePatient.getIdentifier().stream().filter(theIdentifier -> theIdentifier.getSystem().equals("REDIRECT")).findFirst(); - // assertThat(redirect.get().getValue(), is(equalTo(myToSourcePatient.getIdElement().toUnqualified().getValue()))); - - List links = myMdmLinkDaoSvc.findMdmLinksBySourceResource(myFromGoldenPatient); - assertThat(links, hasSize(1)); - - MdmLink link = links.get(0); - assertEquals(link.getSourcePid(), myFromGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong()); - assertEquals(link.getGoldenResourcePid(), myToGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong()); - assertEquals(link.getMatchResult(), MdmMatchResultEnum.REDIRECT); - assertEquals(link.getLinkSource(), MdmLinkSourceEnum.MANUAL); - } - - @Test - public void testUnmanagedMerge() { - StringType fromGoldenResourceId = new StringType(createPatient().getIdElement().getValue()); - StringType toGoldenResourceId = new StringType(createPatient().getIdElement().getValue()); - try { - myMdmProviderR4.mergeGoldenResources(fromGoldenResourceId, toGoldenResourceId, myRequestDetails); - fail(); - } catch (InvalidRequestException e) { - assertEquals("Only MDM managed resources can be merged. MDM managed resources must have the HAPI-MDM tag.", e.getMessage()); - } - } - - @Test - public void testNullParams() { - try { - myMdmProviderR4.mergeGoldenResources(null, null, myRequestDetails); - fail(); - } catch (InvalidRequestException e) { - assertEquals("fromGoldenResourceId cannot be null", e.getMessage()); - } - try { - myMdmProviderR4.mergeGoldenResources(null, myToGoldenPatientId, myRequestDetails); - fail(); - } catch (InvalidRequestException e) { - assertEquals("fromGoldenResourceId cannot be null", e.getMessage()); - } - try { - myMdmProviderR4.mergeGoldenResources(myFromGoldenPatientId, null, myRequestDetails); - fail(); - } catch (InvalidRequestException e) { - assertEquals("toGoldenResourceId cannot be null", e.getMessage()); - } - } - - @Test - public void testBadParams() { - try { - myMdmProviderR4.mergeGoldenResources(new StringType("Patient/1"), new StringType("Patient/1"), myRequestDetails); - fail(); - } catch (InvalidRequestException e) { - assertEquals("fromGoldenResourceId must be different from toGoldenResourceId", e.getMessage()); - } - - try { - myMdmProviderR4.mergeGoldenResources(new StringType("Patient/abc"), myToGoldenPatientId, myRequestDetails); - fail(); - } catch (ResourceNotFoundException e) { - assertEquals("Resource Patient/abc is not known", e.getMessage()); - } - - try { - myMdmProviderR4.mergeGoldenResources(new StringType("Organization/abc"), myToGoldenPatientId, myRequestDetails); - fail(); - } catch (ResourceNotFoundException e) { - assertEquals("Resource Organization/abc is not known", e.getMessage()); - } - - try { - myMdmProviderR4.mergeGoldenResources(myFromGoldenPatientId, new StringType("Patient/abc"), myRequestDetails); - fail(); - } catch (ResourceNotFoundException e) { - assertEquals("Resource Patient/abc is not known", e.getMessage()); - } - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderUpdateLinkR4Test.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderUpdateLinkR4Test.java deleted file mode 100644 index 3cd70b77ec5..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderUpdateLinkR4Test.java +++ /dev/null @@ -1,166 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.provider; - -import ca.uhn.fhir.mdm.api.MdmConstants; -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.util.MessageHelper; -import ca.uhn.fhir.jpa.entity.MdmLink; -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.StringType; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.List; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; - -public class MdmProviderUpdateLinkR4Test extends BaseLinkR4Test { - - @Autowired - private MessageHelper myMessageHelper; - - @Test - public void testUpdateLinkNoMatch() { - assertLinkCount(1); - myMdmProviderR4.updateLink(mySourcePatientId, myPatientId, NO_MATCH_RESULT, myRequestDetails); - assertLinkCount(2); - - List links = getPatientLinks(); - assertEquals(MdmLinkSourceEnum.MANUAL, links.get(0).getLinkSource()); - assertEquals(MdmMatchResultEnum.NO_MATCH, links.get(0).getMatchResult()); - assertEquals(MdmLinkSourceEnum.AUTO, links.get(1).getLinkSource()); - assertEquals(MdmMatchResultEnum.MATCH, links.get(1).getMatchResult()); - assertNotEquals(links.get(0).getGoldenResourcePid(), links.get(1).getGoldenResourcePid()); - } - - @Test - public void testUpdateLinkMatch() { - assertLinkCount(1); - myMdmProviderR4.updateLink(mySourcePatientId, myPatientId, MATCH_RESULT, myRequestDetails); - assertLinkCount(1); - - List links = getPatientLinks(); - assertEquals(MdmLinkSourceEnum.MANUAL, links.get(0).getLinkSource()); - assertEquals(MdmMatchResultEnum.MATCH, links.get(0).getMatchResult()); - } - - @Test - public void testUpdateLinkTwiceFailsDueToWrongVersion() { - myMdmProviderR4.updateLink(mySourcePatientId, myPatientId, MATCH_RESULT, myRequestDetails); - - materiallyChangeGoldenPatient(); - - try { - myMdmProviderR4.updateLink(mySourcePatientId, myPatientId, NO_MATCH_RESULT, myRequestDetails); - fail(); - } catch (ResourceVersionConflictException e) { - assertThat(e.getMessage(), matchesPattern("Requested resource Patient/\\d+/_history/1 is not the latest version. Latest version is Patient/\\d+/_history/2")); - } - } - - private void materiallyChangeGoldenPatient() { - Patient materiallyChangedSourcePatientThatShouldTriggerVersionChange = (Patient) mySourcePatient; - materiallyChangedSourcePatientThatShouldTriggerVersionChange.getNameFirstRep().setFamily("NEW LAST NAME"); - myPatientDao.update(materiallyChangedSourcePatientThatShouldTriggerVersionChange); - } - - @Test - public void testUpdateLinkTwiceDoesNotThrowValidationErrorWhenNoVersionIsProvided() { - myMdmProviderR4.updateLink(mySourcePatientId, myPatientId, MATCH_RESULT, myRequestDetails); - Patient patient = (Patient) myMdmProviderR4.updateLink(myVersionlessGodlenResourceId, myPatientId, NO_MATCH_RESULT, myRequestDetails); - assertNotNull(patient); // if this wasn't allowed - a validation exception would be thrown - } - - @Test - public void testUnlinkLink() { - myMdmProviderR4.updateLink(mySourcePatientId, myPatientId, NO_MATCH_RESULT, myRequestDetails); - - materiallyChangeGoldenPatient(); - - try { - myMdmProviderR4.updateLink(mySourcePatientId, myPatientId, MATCH_RESULT, myRequestDetails); - fail(); - } catch (ResourceVersionConflictException e) { - assertThat(e.getMessage(), matchesPattern("Requested resource Patient/\\d+/_history/1 is not the latest version. Latest version is Patient/\\d+/_history/2")); - } - } - - @Test - public void testUpdateIllegalResultForPossibleMatch() { - try { - myMdmProviderR4.updateLink(mySourcePatientId, myPatientId, POSSIBLE_MATCH_RESULT, myRequestDetails); - fail(); - } catch (InvalidRequestException e) { - assertEquals("$mdm-update-link illegal matchResult value 'POSSIBLE_MATCH'. Must be NO_MATCH or MATCH", e.getMessage()); - } - } - - @Test - public void testUpdateIllegalResultPD() { - try { - myMdmProviderR4.updateLink(mySourcePatientId, myPatientId, POSSIBLE_DUPLICATE_RESULT, myRequestDetails); - fail(); - } catch (InvalidRequestException e) { - assertEquals("$mdm-update-link illegal matchResult value 'POSSIBLE_DUPLICATE'. Must be NO_MATCH or MATCH", e.getMessage()); - } - } - - @Test - public void testUpdateIllegalSecondArg() { - try { - myMdmProviderR4.updateLink(myPatientId, new StringType(""), NO_MATCH_RESULT, myRequestDetails); - fail(); - } catch (InvalidRequestException e) { - assertThat(e.getMessage(), endsWith(" must have form / where is the id of the resource and is the type of the resource")); - } - } - - @Test - public void testUpdateIllegalFirstArg() { - try { - myMdmProviderR4.updateLink(new StringType(""), myPatientId, NO_MATCH_RESULT, myRequestDetails); - fail(); - } catch (InvalidRequestException e) { - assertThat(e.getMessage(), endsWith(" must have form / where is the id of the resource")); - } - } - - @Test - public void testAttemptingToModifyANonExistentLinkFails() { - try { - myMdmProviderR4.updateLink(mySourcePatientId, mySourcePatientId, NO_MATCH_RESULT, myRequestDetails); - fail(); - } catch (InvalidRequestException e) { - assertThat(e.getMessage(), startsWith("No link")); - } - } - - @Test - public void testUpdateStrangePatient() { - Patient patient = createPatient(); - try { - myMdmProviderR4.updateLink(new StringType(patient.getIdElement().getValue()), myPatientId, NO_MATCH_RESULT, myRequestDetails); - fail(); - } catch (InvalidRequestException e) { - String expectedMessage = myMessageHelper.getMessageForUnmanagedResource(); - assertEquals(expectedMessage, e.getMessage()); - } - } - - @Test - public void testExcludedGoldenResource() { - Patient patient = new Patient(); - patient.getMeta().addTag().setSystem(MdmConstants.SYSTEM_MDM_MANAGED).setCode(MdmConstants.CODE_NO_MDM_MANAGED); - createPatient(patient); - try { - myMdmProviderR4.updateLink(mySourcePatientId, new StringType(patient.getIdElement().getValue()), NO_MATCH_RESULT, myRequestDetails); - fail(); - } catch (InvalidRequestException e) { - assertEquals(myMessageHelper.getMessageForUnsupportedSourceResource(), e.getMessage()); - } - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmBatchSvcImplTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmBatchSvcImplTest.java deleted file mode 100644 index b972988e423..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmBatchSvcImplTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc; - -import ca.uhn.fhir.mdm.api.IMdmSubmitSvc; -import ca.uhn.fhir.interceptor.api.IInterceptorService; -import ca.uhn.fhir.interceptor.api.Pointcut; -import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; -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 MdmBatchSvcImplTest extends BaseMdmR4Test { - - @Autowired - IMdmSubmitSvc myMdmSubmitSvc; - - @Autowired - IInterceptorService myInterceptorService; - - PointcutLatch afterMdmLatch = new PointcutLatch(Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED); - - @BeforeEach - public void before() { - myInterceptorService.registerAnonymousInterceptor(Pointcut.MDM_AFTER_PERSISTED_RESOURCE_CHECKED, afterMdmLatch); - } - @AfterEach - public void after() throws IOException { - myInterceptorService.unregisterInterceptor(afterMdmLatch); - afterMdmLatch.clear(); - super.after(); - } - - @Test - public void testMdmBatchRunWorksOverMultipleTargetTypes() throws InterruptedException { - - for (int i =0; i < 10; i++) { - createPatient(buildJanePatient()); - } - - for(int i = 0; i< 10; i++) { - createPractitioner(buildPractitionerWithNameAndId("test", "id")); - } - - createDummyOrganization(); - for(int i = 0; i< 10; i++) { - createMedication(buildMedicationWithDummyOrganization()); - } - - assertLinkCount(0); - - //SUT - afterMdmLatch.runWithExpectedCount(30, () -> myMdmSubmitSvc.submitAllSourceTypesToMdm(null)); - - assertLinkCount(30); - } - - @Test - public void testMdmBatchOnPatientType() throws Exception { - - for (int i =0; i < 10; i++) { - createPatient(buildPatientWithNameAndId("test", "id")); - } - - assertLinkCount(0); - - //SUT - afterMdmLatch.runWithExpectedCount(10, () -> myMdmSubmitSvc.submitSourceResourceTypeToMdm("Patient", null)); - - assertLinkCount(10); - } - - @Test - public void testMdmBatchOnMedicationType() throws Exception { - - createDummyOrganization(); - - - for(int i = 0; i< 10; i++) { - createMedication(buildMedicationWithDummyOrganization()); - } - assertLinkCount(0); - - //SUT - afterMdmLatch.runWithExpectedCount(10, () -> myMdmSubmitSvc.submitSourceResourceTypeToMdm("Medication", null)); - - assertLinkCount(10); - } - - @Test - public void testMdmBatchOnPractitionerType() throws Exception { - - for (int i =0; i < 10; i++) { - createPractitioner(buildPractitionerWithNameAndId("test", "id")); - } - - assertLinkCount(0); - - //SUT - afterMdmLatch.runWithExpectedCount(10, () -> myMdmSubmitSvc.submitAllSourceTypesToMdm(null)); - - assertLinkCount(10); - } - - @Test - public void testMdmOnTargetTypeWithCriteria() throws InterruptedException { - createPatient(buildPatientWithNameIdAndBirthday("gary", "gary_id", new Date())); - createPatient(buildPatientWithNameIdAndBirthday("john", "john_id", DateUtils.addDays(new Date(), -300))); - - assertLinkCount(0); - - //SUT - afterMdmLatch.runWithExpectedCount(1, () -> myMdmSubmitSvc.submitSourceResourceTypeToMdm("Patient", "Patient?name=gary")); - - assertLinkCount(1); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmGoldenResourceMergerSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmGoldenResourceMergerSvcTest.java deleted file mode 100644 index b7dd763953c..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmGoldenResourceMergerSvcTest.java +++ /dev/null @@ -1,449 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc; - -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchOutcome; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.interceptor.api.IInterceptorService; -import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; -import ca.uhn.fhir.jpa.mdm.helper.MdmLinkHelper; -import ca.uhn.fhir.jpa.mdm.interceptor.IMdmStorageInterceptor; -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.rest.server.TransactionLogMessages; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import org.hl7.fhir.instance.model.api.IBaseResource; -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.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 MdmGoldenResourceMergerSvcTest extends BaseMdmR4Test { - - 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 MdmMatchOutcome POSSIBLE_MATCH = new MdmMatchOutcome(null, null).setMatchResultEnum(MdmMatchResultEnum.POSSIBLE_MATCH); - - @Autowired - IGoldenResourceMergerSvc myGoldenResourceMergerSvc; - @Autowired - MdmLinkHelper myMdmLinkHelper; - @Autowired - IMdmStorageInterceptor myMdmStorageInterceptor; - @Autowired - IInterceptorService myInterceptorService; - - private Patient myFromGoldenPatient; - private Patient myToGoldenPatient; - private Long myFromGoldenPatientPid; - private Long myToGoldenPatientPid; - private Patient myTargetPatient1; - private Patient myTargetPatient2; - private Patient myTargetPatient3; - - @BeforeEach - public void before() { - myFromGoldenPatient = createGoldenPatient(); - IdType fromSourcePatientId = myFromGoldenPatient.getIdElement().toUnqualifiedVersionless(); - myFromGoldenPatientPid = myIdHelperService.getPidOrThrowException(fromSourcePatientId); - myToGoldenPatient = createGoldenPatient(); - IdType toGoldenPatientId = myToGoldenPatient.getIdElement().toUnqualifiedVersionless(); - myToGoldenPatientPid = myIdHelperService.getPidOrThrowException(toGoldenPatientId); - - myTargetPatient1 = createPatient(); - myTargetPatient2 = createPatient(); - myTargetPatient3 = createPatient(); - - // Register the mdm storage interceptor after the creates so the delete hook is fired when we merge - myInterceptorService.registerInterceptor(myMdmStorageInterceptor); - } - - @Override - @AfterEach - public void after() throws IOException { - myInterceptorService.unregisterInterceptor(myMdmStorageInterceptor); - super.after(); - } - - @Test - public void emptyMerge() { - assertEquals(2, getAllGoldenPatients().size()); - assertEquals(0, getAllRedirectedGoldenPatients().size()); - - Patient mergedGoldenPatient = mergeGoldenPatients(); - - assertEquals(myToGoldenPatient.getIdElement(), mergedGoldenPatient.getIdElement()); - assertThat(mergedGoldenPatient, is(sameGoldenResourceAs(mergedGoldenPatient))); - assertEquals(1, getAllGoldenPatients().size()); - assertEquals(1, getAllRedirectedGoldenPatients().size()); - } - - private Patient mergeGoldenPatients() { - assertEquals(0, redirectLinkCount()); - Patient retval = (Patient) myGoldenResourceMergerSvc.mergeGoldenResources(myFromGoldenPatient, myToGoldenPatient, createMdmContext()); - assertEquals(1, redirectLinkCount()); - return retval; - } - - private int redirectLinkCount() { - MdmLink mdmLink = new MdmLink().setMatchResult(MdmMatchResultEnum.REDIRECT); - Example example = Example.of(mdmLink); - return myMdmLinkDao.findAll(example).size(); - } - - private MdmTransactionContext createMdmContext() { - MdmTransactionContext mdmTransactionContext = new MdmTransactionContext(TransactionLogMessages.createFromTransactionGuid(UUID.randomUUID().toString()), MdmTransactionContext.OperationType.MERGE_GOLDEN_RESOURCES); - mdmTransactionContext.setResourceType("Patient"); - return mdmTransactionContext; - } - - @Test - public void mergeRemovesPossibleDuplicatesLink() { - MdmLink mdmLink = myMdmLinkDaoSvc.newMdmLink() - .setGoldenResourcePid(myToGoldenPatientPid) - .setSourcePid(myFromGoldenPatientPid) - .setMdmSourceType("Patient") - .setMatchResult(MdmMatchResultEnum.POSSIBLE_DUPLICATE) - .setLinkSource(MdmLinkSourceEnum.AUTO); - - saveLink(mdmLink); - - { - List foundLinks = myMdmLinkDao.findAll(); - assertEquals(1, foundLinks.size()); - assertEquals(MdmMatchResultEnum.POSSIBLE_DUPLICATE, foundLinks.get(0).getMatchResult()); - } - - myMdmLinkHelper.logMdmLinks(); - - mergeGoldenPatients(); - - { - List foundLinks = myMdmLinkDao.findAll(); - assertEquals(1, foundLinks.size()); - assertEquals(MdmMatchResultEnum.REDIRECT, foundLinks.get(0).getMatchResult()); - } - } - - @Test - public void fullFromEmptyTo() { - populatePatient(myFromGoldenPatient); - - Patient mergedSourcePatient = mergeGoldenPatients(); - // TODO NG - Revisit when rules are ready -// HumanName returnedName = mergedSourcePatient.getNameFirstRep(); -// assertEquals(GIVEN_NAME, returnedName.getGivenAsSingleString()); -// assertEquals(FAMILY_NAME, returnedName.getFamily()); -// assertEquals(POSTAL_CODE, mergedSourcePatient.getAddressFirstRep().getPostalCode()); - } - - @Test - public void emptyFromFullTo() { - myFromGoldenPatient.getName().add(new HumanName().addGiven(BAD_GIVEN_NAME)); - populatePatient(myToGoldenPatient); - - Patient mergedSourcePatient = mergeGoldenPatients(); - HumanName returnedName = mergedSourcePatient.getNameFirstRep(); - assertEquals(GIVEN_NAME, returnedName.getGivenAsSingleString()); - assertEquals(FAMILY_NAME, returnedName.getFamily()); - assertEquals(POSTAL_CODE, mergedSourcePatient.getAddressFirstRep().getPostalCode()); - } - - @Test - public void fromLinkToNoLink() { - createMdmLink(myFromGoldenPatient, myTargetPatient1); - - Patient mergedGoldenPatient = mergeGoldenPatients(); - List links = getNonRedirectLinksByGoldenResource(mergedGoldenPatient); - assertEquals(1, links.size()); - assertThat(mergedGoldenPatient, is(possibleLinkedTo(myTargetPatient1))); - } - - @Test - public void fromNoLinkToLink() { - createMdmLink(myToGoldenPatient, myTargetPatient1); - - Patient mergedSourcePatient = mergeGoldenPatients(); - List links = getNonRedirectLinksByGoldenResource(mergedSourcePatient); - assertEquals(1, links.size()); - assertThat(mergedSourcePatient, is(possibleLinkedTo(myTargetPatient1))); - } - - @Test - public void fromManualLinkOverridesAutoToLink() { - MdmLink fromLink = createMdmLink(myFromGoldenPatient, myTargetPatient1); - fromLink.setLinkSource(MdmLinkSourceEnum.MANUAL); - fromLink.setMatchResult(MdmMatchResultEnum.MATCH); - saveLink(fromLink); - - createMdmLink(myToGoldenPatient, myTargetPatient1); - - mergeGoldenPatients(); - List links = getNonRedirectLinksByGoldenResource(myToGoldenPatient); - assertEquals(1, links.size()); - assertEquals(MdmLinkSourceEnum.MANUAL, links.get(0).getLinkSource()); - } - - private List getNonRedirectLinksByGoldenResource(Patient theGoldenPatient) { - return myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theGoldenPatient).stream() - .filter(link -> !link.isRedirect()) - .collect(Collectors.toList()); - } - - @Test - public void fromManualNoMatchLinkOverridesAutoToLink() { - MdmLink fromLink = createMdmLink(myFromGoldenPatient, myTargetPatient1); - fromLink.setLinkSource(MdmLinkSourceEnum.MANUAL); - fromLink.setMatchResult(MdmMatchResultEnum.NO_MATCH); - - saveLink(fromLink); - - createMdmLink(myToGoldenPatient, myTargetPatient1); - - mergeGoldenPatients(); - List links = getNonRedirectLinksByGoldenResource(myToGoldenPatient); - assertEquals(1, links.size()); - assertEquals(MdmLinkSourceEnum.MANUAL, links.get(0).getLinkSource()); - assertEquals(MdmMatchResultEnum.NO_MATCH, links.get(0).getMatchResult()); - } - - @Test - public void fromManualAutoMatchLinkNoOverridesManualToLink() { - createMdmLink(myFromGoldenPatient, myTargetPatient1); - - MdmLink toLink = createMdmLink(myToGoldenPatient, myTargetPatient1); - toLink.setLinkSource(MdmLinkSourceEnum.MANUAL); - toLink.setMatchResult(MdmMatchResultEnum.NO_MATCH); - saveLink(toLink); - - mergeGoldenPatients(); - List links = getNonRedirectLinksByGoldenResource(myToGoldenPatient); - assertEquals(1, links.size()); - assertEquals(MdmLinkSourceEnum.MANUAL, links.get(0).getLinkSource()); - assertEquals(MdmMatchResultEnum.NO_MATCH, links.get(0).getMatchResult()); - } - - @Test - public void fromNoMatchMergeToManualMatchIsError() { - MdmLink fromLink = createMdmLink(myFromGoldenPatient, myTargetPatient1); - fromLink.setLinkSource(MdmLinkSourceEnum.MANUAL); - fromLink.setMatchResult(MdmMatchResultEnum.NO_MATCH); - saveLink(fromLink); - - MdmLink toLink = createMdmLink(myToGoldenPatient, myTargetPatient1); - toLink.setLinkSource(MdmLinkSourceEnum.MANUAL); - toLink.setMatchResult(MdmMatchResultEnum.MATCH); - saveLink(toLink); - - try { - mergeGoldenPatients(); - 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() { - MdmLink fromLink = createMdmLink(myFromGoldenPatient, myTargetPatient1); - fromLink.setLinkSource(MdmLinkSourceEnum.MANUAL); - fromLink.setMatchResult(MdmMatchResultEnum.MATCH); - saveLink(fromLink); - - MdmLink toLink = createMdmLink(myToGoldenPatient, myTargetPatient1); - toLink.setLinkSource(MdmLinkSourceEnum.MANUAL); - toLink.setMatchResult(MdmMatchResultEnum.NO_MATCH); - saveLink(toLink); - - try { - mergeGoldenPatients(); - 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() { - MdmLink fromLink = createMdmLink(myFromGoldenPatient, myTargetPatient1); - fromLink.setLinkSource(MdmLinkSourceEnum.MANUAL); - fromLink.setMatchResult(MdmMatchResultEnum.NO_MATCH); - saveLink(fromLink); - - MdmLink toLink = createMdmLink(myToGoldenPatient, myTargetPatient2); - toLink.setLinkSource(MdmLinkSourceEnum.MANUAL); - toLink.setMatchResult(MdmMatchResultEnum.MATCH); - saveLink(toLink); - - mergeGoldenPatients(); - - assertResourceHasLinkCount(myToGoldenPatient, 3); - assertResourceHasLinkCount(myFromGoldenPatient, 0); - // TODO ENSURE PROPER LINK TYPES - assertEquals(3, myMdmLinkDao.count()); - } - - @Test - public void from123To1() { - createMdmLink(myFromGoldenPatient, myTargetPatient1); - createMdmLink(myFromGoldenPatient, myTargetPatient2); - createMdmLink(myFromGoldenPatient, myTargetPatient3); - createMdmLink(myToGoldenPatient, myTargetPatient1); - - mergeGoldenPatients(); - myMdmLinkHelper.logMdmLinks(); - - assertThat(myToGoldenPatient, is(possibleLinkedTo(myTargetPatient1, myTargetPatient2, myTargetPatient3))); - assertResourceHasAutoLinkCount(myToGoldenPatient, 3); - } - - - private void assertResourceHasLinkCount(IBaseResource theResource, int theCount) { - List links = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theResource); - assertEquals(theCount, links.size()); - } - - @Test - public void from1To123() { - createMdmLink(myFromGoldenPatient, myTargetPatient1); - createMdmLink(myToGoldenPatient, myTargetPatient1); - createMdmLink(myToGoldenPatient, myTargetPatient2); - createMdmLink(myToGoldenPatient, myTargetPatient3); - - mergeGoldenPatients(); - myMdmLinkHelper.logMdmLinks(); - - assertThat(myToGoldenPatient, is(possibleLinkedTo(myTargetPatient1, myTargetPatient2, myTargetPatient3))); - assertResourceHasAutoLinkCount(myToGoldenPatient, 3); - } - - private void assertResourceHasAutoLinkCount(Patient myToGoldenPatient, int theCount) { - List links = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(myToGoldenPatient); - assertEquals(theCount, links.stream().filter(MdmLink::isAuto).count()); - } - - @Test - public void from123To123() { - createMdmLink(myFromGoldenPatient, myTargetPatient1); - createMdmLink(myFromGoldenPatient, myTargetPatient2); - createMdmLink(myFromGoldenPatient, myTargetPatient3); - createMdmLink(myToGoldenPatient, myTargetPatient1); - createMdmLink(myToGoldenPatient, myTargetPatient2); - createMdmLink(myToGoldenPatient, myTargetPatient3); - - mergeGoldenPatients(); - - assertThat(myToGoldenPatient, is(possibleLinkedTo(myTargetPatient1, myTargetPatient2, myTargetPatient3))); - - assertResourceHasAutoLinkCount(myToGoldenPatient, 3); - } - - @Test - public void from12To23() { - createMdmLink(myFromGoldenPatient, myTargetPatient1); - createMdmLink(myFromGoldenPatient, myTargetPatient2); - createMdmLink(myToGoldenPatient, myTargetPatient2); - createMdmLink(myToGoldenPatient, myTargetPatient3); - - mergeGoldenPatients(); - myMdmLinkHelper.logMdmLinks(); - - assertThat(myToGoldenPatient, is(possibleLinkedTo(myTargetPatient1, myTargetPatient2, myTargetPatient3))); - - assertResourceHasAutoLinkCount(myToGoldenPatient, 3); - } - - @Test - public void testMergeNames() { - // TODO NG - Revisit when rules are available -// myFromSourcePatient.addName().addGiven("Jim"); -// myFromSourcePatient.getNameFirstRep().addGiven("George"); -// assertThat(myFromSourcePatient.getName(), hasSize(1)); -// assertThat(myFromSourcePatient.getName().get(0).getGiven(), hasSize(2)); -// -// myToSourcePatient.addName().addGiven("Jeff"); -// myToSourcePatient.getNameFirstRep().addGiven("George"); -// assertThat(myToSourcePatient.getName(), hasSize(1)); -// assertThat(myToSourcePatient.getName().get(0).getGiven(), hasSize(2)); -// -// Patient mergedSourcePatient = mergeSourcePatients(); -// assertThat(mergedSourcePatient.getName(), hasSize(2)); -// assertThat(mergedSourcePatient.getName().get(0).getGiven(), hasSize(2)); -// assertThat(mergedSourcePatient.getName().get(1).getGiven(), hasSize(2)); - } - - @Test - public void testMergeNamesAllSame() { - // TODO NG - Revisit when rules are available -// myFromSourcePatient.addName().addGiven("Jim"); -// myFromSourcePatient.getNameFirstRep().addGiven("George"); -// assertThat(myFromSourcePatient.getName(), hasSize(1)); -// assertThat(myFromSourcePatient.getName().get(0).getGiven(), hasSize(2)); -// -// myToSourcePatient.addName().addGiven("Jim"); -// myToSourcePatient.getNameFirstRep().addGiven("George"); -// assertThat(myToSourcePatient.getName(), hasSize(1)); -// assertThat(myToSourcePatient.getName().get(0).getGiven(), hasSize(2)); -// -// mergeSourcePatients(); -// assertThat(myToSourcePatient.getName(), hasSize(1)); -// assertThat(myToSourcePatient.getName().get(0).getGiven(), hasSize(2)); - } - - @Test - public void testMergeIdentifiers() { - myFromGoldenPatient.addIdentifier().setValue("aaa").setSystem("SYSTEM1"); - myFromGoldenPatient.addIdentifier().setValue("bbb").setSystem("SYSTEM1"); - myFromGoldenPatient.addIdentifier().setValue("ccc").setSystem("SYSTEM2"); - assertThat(myFromGoldenPatient.getIdentifier(), hasSize(3)); - - myToGoldenPatient.addIdentifier().setValue("aaa").setSystem("SYSTEM1"); - myToGoldenPatient.addIdentifier().setValue("ccc").setSystem("SYSTEM1"); - assertThat(myToGoldenPatient.getIdentifier(), hasSize(2)); - - mergeGoldenPatients(); - - assertThat(myToGoldenPatient.getIdentifier(), hasSize(4)); - } - - private MdmLink createMdmLink(Patient theSourcePatient, Patient theTargetPatient) { - //TODO GGG Ensure theis comment can be safely removed - //theSourcePatient.addLink().setTarget(new Reference(theTargetPatient)); - return myMdmLinkDaoSvc.createOrUpdateLinkEntity(theSourcePatient, theTargetPatient, POSSIBLE_MATCH, MdmLinkSourceEnum.AUTO, createContextForCreate("Patient")); - } - - private void populatePatient(Patient theSourcePatient) { - theSourcePatient.addName(new HumanName().addGiven(GIVEN_NAME).setFamily(FAMILY_NAME)); - theSourcePatient.setGender(Enumerations.AdministrativeGender.FEMALE); - theSourcePatient.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); - theSourcePatient.setAddress(Collections.singletonList(address)); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkSvcTest.java deleted file mode 100644 index a65bfad0f67..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmLinkSvcTest.java +++ /dev/null @@ -1,192 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc; - -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; -import ca.uhn.fhir.mdm.api.IMdmLinkSvc; -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchOutcome; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import org.hamcrest.Matchers; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Patient; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -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.fail; - -public class MdmLinkSvcTest extends BaseMdmR4Test { - private static final MdmMatchOutcome POSSIBLE_MATCH = new MdmMatchOutcome(null, null).setMatchResultEnum(MdmMatchResultEnum.POSSIBLE_MATCH); - @Autowired - IMdmLinkSvc myMdmLinkSvc; - - @Override - @AfterEach - public void after() throws IOException { - myExpungeEverythingService.expungeEverythingByType(MdmLink.class); - super.after(); - } - - @Test - public void compareEmptyPatients() { - Patient patient = new Patient(); - patient.setId("Patient/1"); - MdmMatchResultEnum result = myMdmResourceMatcherSvc.getMatchResult(patient, patient).getMatchResultEnum(); - assertEquals(MdmMatchResultEnum.NO_MATCH, result); - } - - @Test - public void testCreateRemoveLink() { - assertLinkCount(0); - Patient goldenPatient = createGoldenPatient(); - IdType sourcePatientId = goldenPatient.getIdElement().toUnqualifiedVersionless(); - // TODO NG should be ok to remove - assertEquals(0, goldenPatient.getLink().size()); - Patient patient = createPatient(); - - { - myMdmLinkSvc.updateLink(goldenPatient, patient, POSSIBLE_MATCH, MdmLinkSourceEnum.AUTO, createContextForCreate("Patient")); - assertLinkCount(1); - // TODO NG should be ok to remove - // Patient newSourcePatient = myPatientDao.read(sourcePatientId); - // assertEquals(1, newSourcePatient.getLink().size()); - } - - { - myMdmLinkSvc.updateLink(goldenPatient, patient, MdmMatchOutcome.NO_MATCH, MdmLinkSourceEnum.MANUAL, createContextForCreate("Patient")); - assertLinkCount(1); - // TODO NG should be ok to remove - // Patient newSourcePatient = myPatientDao.read(sourcePatientId); - // assertEquals(0, newSourcePatient.getLink().size()); - } - } - - - @Test - public void testPossibleDuplicate() { - assertLinkCount(0); - Patient goldenPatient1 = createGoldenPatient(); - Patient goldenPatient2 = createGoldenPatient(); - // TODO GGG MDM NOT VALID - myMdmLinkSvc.updateLink(goldenPatient1, goldenPatient2, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, createContextForCreate("Patient")); - assertLinkCount(1); - } - - @Test - public void testNoMatchBlocksPossibleDuplicate() { - assertLinkCount(0); - Patient goldenPatient1 = createGoldenPatient(); - Patient goldenPatient2 = createGoldenPatient(); - - Long goldenPatient1Pid = myIdHelperService.getPidOrNull(goldenPatient1); - Long goldenPatient2Pid = myIdHelperService.getPidOrNull(goldenPatient2); - assertFalse(myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenPatient1Pid, goldenPatient2Pid).isPresent()); - assertFalse(myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenPatient2Pid, goldenPatient1Pid).isPresent()); - - saveNoMatchLink(goldenPatient1Pid, goldenPatient2Pid); - - myMdmLinkSvc.updateLink(goldenPatient1, goldenPatient2, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, createContextForCreate("Patient")); - assertFalse(myMdmLinkDaoSvc.getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(goldenPatient1Pid, goldenPatient2Pid, MdmMatchResultEnum.POSSIBLE_DUPLICATE).isPresent()); - assertLinkCount(1); - } - - @Test - public void testNoMatchBlocksPossibleDuplicateReversed() { - assertLinkCount(0); - Patient goldenPatient1 = createGoldenPatient(); - Patient goldenPatient2 = createGoldenPatient(); - - Long goldenPatient1Pid = myIdHelperService.getPidOrNull(goldenPatient1); - Long goldenPatient2Pid = myIdHelperService.getPidOrNull(goldenPatient2); - assertFalse(myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenPatient1Pid, goldenPatient2Pid).isPresent()); - assertFalse(myMdmLinkDaoSvc.getLinkByGoldenResourcePidAndSourceResourcePid(goldenPatient2Pid, goldenPatient1Pid).isPresent()); - - saveNoMatchLink(goldenPatient2Pid, goldenPatient1Pid); - - myMdmLinkSvc.updateLink(goldenPatient1, goldenPatient2, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, createContextForCreate("Patient")); - assertFalse(myMdmLinkDaoSvc.getMdmLinksByGoldenResourcePidSourcePidAndMatchResult(goldenPatient1Pid, goldenPatient2Pid, MdmMatchResultEnum.POSSIBLE_DUPLICATE).isPresent()); - assertLinkCount(1); - } - - private void saveNoMatchLink(Long theGoldenResourcePid, Long theTargetPid) { - MdmLink noMatchLink = myMdmLinkDaoSvc.newMdmLink() - .setGoldenResourcePid(theGoldenResourcePid) - .setSourcePid(theTargetPid) - .setLinkSource(MdmLinkSourceEnum.MANUAL) - .setMatchResult(MdmMatchResultEnum.NO_MATCH); - saveLink(noMatchLink); - } - - @Test - public void testManualMdmLinksCannotBeModifiedBySystem() { -// Patient goldenPatient = createGoldenPatient(buildJaneSourcePatient()); - Patient goldenPatient = createGoldenPatient(buildJanePatient()); - Patient patient = createPatient(buildJanePatient()); - - myMdmLinkSvc.updateLink(goldenPatient, patient, MdmMatchOutcome.NO_MATCH, MdmLinkSourceEnum.MANUAL, createContextForCreate("Patient")); - try { - myMdmLinkSvc.updateLink(goldenPatient, patient, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, null); - fail(); - } catch (InternalErrorException e) { - assertThat(e.getMessage(), is(equalTo("MDM system is not allowed to modify links on manually created links"))); - } - } - - @Test - public void testAutomaticallyAddedNO_MATCHMdmLinksAreNotAllowed() { -// Patient goldenPatient = createGoldenPatient(buildJaneSourcePatient()); - Patient goldenPatient = createGoldenPatient(buildJanePatient()); - 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 { - myMdmLinkSvc.updateLink(goldenPatient, patient, MdmMatchOutcome.NO_MATCH, MdmLinkSourceEnum.AUTO, createContextForUpdate("Patient")); - fail(); - } catch (InternalErrorException e) { - assertThat(e.getMessage(), is(equalTo("MDM system is not allowed to automatically NO_MATCH a resource"))); - } - } - - @Test - public void testSyncDoesNotSyncNoMatchLinks() { -// Patient sourcePatient = createGoldenPatient(buildJaneSourcePatient()); - Patient goldenPatient = createGoldenPatient(buildJanePatient()); - Patient patient1 = createPatient(buildJanePatient()); - Patient patient2 = createPatient(buildJanePatient()); - assertEquals(0, myMdmLinkDao.count()); - - myMdmLinkDaoSvc.createOrUpdateLinkEntity(goldenPatient, patient1, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.MANUAL, createContextForCreate("Patient")); - myMdmLinkDaoSvc.createOrUpdateLinkEntity(goldenPatient, patient2, MdmMatchOutcome.NO_MATCH, MdmLinkSourceEnum.MANUAL, createContextForCreate("Patient")); - - List targets = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(goldenPatient); - assertFalse(targets.isEmpty()); - assertEquals(2, targets.size()); - // TODO NG - OK? original assertTrue(goldenPatient.hasLink()); - - //TODO GGG update this test once we decide what has to happen here. There is no more "syncing links" - //assertEquals(patient1.getIdElement().toVersionless().getValue(), sourcePatient.getLinkFirstRep().getTarget().getReference()); - List actual = targets - .stream() - .map(link -> link.getSourcePid().toString()) - .collect(Collectors.toList()); - - List expected = Arrays.asList(patient1, patient2) - .stream().map(p -> p.getIdElement().toVersionless().getIdPart()) - .collect(Collectors.toList()); - - System.out.println(actual); - System.out.println(expected); - - assertThat(actual, Matchers.containsInAnyOrder(expected.toArray())); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java deleted file mode 100644 index 7a8975da186..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java +++ /dev/null @@ -1,617 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc; - -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.mdm.api.IMdmLinkSvc; -import ca.uhn.fhir.mdm.api.MdmConstants; -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchOutcome; -import ca.uhn.fhir.mdm.model.CanonicalEID; -import ca.uhn.fhir.mdm.util.EIDHelper; -import ca.uhn.fhir.mdm.util.GoldenResourceHelper; -import ca.uhn.fhir.mdm.util.MdmResourceUtil; -import ca.uhn.fhir.rest.api.server.IBundleProvider; -import ca.uhn.fhir.rest.param.TokenParam; -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.Practitioner; -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.mdm.api.MdmMatchResultEnum.MATCH; -import static ca.uhn.fhir.mdm.api.MdmMatchResultEnum.NO_MATCH; -import static ca.uhn.fhir.mdm.api.MdmMatchResultEnum.POSSIBLE_DUPLICATE; -import static ca.uhn.fhir.mdm.api.MdmMatchResultEnum.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.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.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.slf4j.LoggerFactory.getLogger; - -public class MdmMatchLinkSvcTest extends BaseMdmR4Test { - - private static final Logger ourLog = getLogger(MdmMatchLinkSvcTest.class); - - @Autowired - IMdmLinkSvc myMdmLinkSvc; - @Autowired - private EIDHelper myEidHelper; - @Autowired - private GoldenResourceHelper myGoldenResourceHelper; - - @Test - public void testAddPatientLinksToNewGoldenResourceIfNoneFound() { - createPatientAndUpdateLinks(buildJanePatient()); - assertLinkCount(1); - assertLinksMatchResult(MATCH); - assertLinksCreatedNewResource(true); - assertLinksMatchedByEid(false); - } - - @Test - public void testAddMedicationLinksToNewGoldenRecordMedicationIfNoneFound() { - createDummyOrganization(); - - createMedicationAndUpdateLinks(buildMedication("Organization/mfr")); - assertLinkCount(1); - assertLinksMatchResult(MATCH); - assertLinksCreatedNewResource(true); - assertLinksMatchedByEid(false); - } - - @Test - public void testAddPatientLinksToNewlyCreatedResourceIfNoMatch() { - Patient patient1 = createPatientAndUpdateLinks(buildJanePatient()); - Patient patient2 = createPatientAndUpdateLinks(buildPaulPatient()); - - assertLinkCount(2); - - assertThat(patient1, is(not(sameGoldenResourceAs(patient2)))); - - assertLinksMatchResult(MATCH, MATCH); - assertLinksCreatedNewResource(true, true); - assertLinksMatchedByEid(false, false); - } - - @Test - public void testAddPatientLinksToExistingGoldenResourceIfMatch() { - Patient patient1 = createPatientAndUpdateLinks(buildJanePatient()); - assertLinkCount(1); - - Patient patient2 = createPatientAndUpdateLinks(buildJanePatient()); - assertLinkCount(2); - - assertThat(patient1, is(sameGoldenResourceAs(patient2))); - assertLinksMatchResult(MATCH, MATCH); - assertLinksCreatedNewResource(true, false); - assertLinksMatchedByEid(false, false); - } - - @Test - public void testWhenMatchOccursOnGoldenResourceThatHasBeenManuallyNOMATCHedThatItIsBlocked() { - Patient originalJane = createPatientAndUpdateLinks(buildJanePatient()); - IAnyResource janeGoldenResource = getGoldenResourceFromTargetResource(originalJane); - - //Create a manual NO_MATCH between janeGoldenResource and unmatchedJane. - Patient unmatchedJane = createPatient(buildJanePatient()); - myMdmLinkSvc.updateLink(janeGoldenResource, unmatchedJane, MdmMatchOutcome.NO_MATCH, MdmLinkSourceEnum.MANUAL, createContextForCreate("Patient")); - - //rerun MDM rules against unmatchedJane. - myMdmMatchLinkSvc.updateMdmLinksForMdmSource(unmatchedJane, createContextForCreate("Patient")); - - assertThat(unmatchedJane, is(not(sameGoldenResourceAs(janeGoldenResource)))); - assertThat(unmatchedJane, is(not(linkedTo(originalJane)))); - - assertLinksMatchResult(MATCH, NO_MATCH, MATCH); - assertLinksCreatedNewResource(true, false, true); - assertLinksMatchedByEid(false, false, false); - } - - @Test - public void testWhenPOSSIBLE_MATCHOccursOnGoldenResourceThatHasBeenManuallyNOMATCHedThatItIsBlocked() { - Patient originalJane = createPatientAndUpdateLinks(buildJanePatient()); - - IBundleProvider search = myPatientDao.search(buildGoldenRecordSearchParameterMap()); - IAnyResource janeGoldenResource = (IAnyResource) search.getResources(0, 1).get(0); - - Patient unmatchedPatient = createPatient(buildJanePatient()); - - // This simulates an admin specifically saying that unmatchedPatient does NOT match janeGoldenResource. - myMdmLinkSvc.updateLink(janeGoldenResource, unmatchedPatient, MdmMatchOutcome.NO_MATCH, MdmLinkSourceEnum.MANUAL, createContextForCreate("Patient")); - // TODO change this so that it will only partially match. - - //Now normally, when we run update links, it should link to janeGoldenResource. However, this manual NO_MATCH link - //should cause a whole new GoldenResource to be created. - myMdmMatchLinkSvc.updateMdmLinksForMdmSource(unmatchedPatient, createContextForCreate("Patient")); - - assertThat(unmatchedPatient, is(not(sameGoldenResourceAs(janeGoldenResource)))); - assertThat(unmatchedPatient, is(not(linkedTo(originalJane)))); - - assertLinksMatchResult(MATCH, NO_MATCH, MATCH); - assertLinksCreatedNewResource(true, false, true); - assertLinksMatchedByEid(false, false, false); - } - - @Test - public void testWhenPatientIsCreatedWithEIDThatItPropagatesToNewGoldenResource() { - String sampleEID = "sample-eid"; - Patient janePatient = addExternalEID(buildJanePatient(), sampleEID); - janePatient = createPatientAndUpdateLinks(janePatient); - - Optional mdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(janePatient.getIdElement().getIdPartAsLong()); - assertThat(mdmLink.isPresent(), is(true)); - - Patient patient = getTargetResourceFromMdmLink(mdmLink.get(), "Patient"); - List externalEid = myEidHelper.getExternalEid(patient); - - assertThat(externalEid.get(0).getSystem(), is(equalTo(myMdmSettings.getMdmRules().getEnterpriseEIDSystem()))); - assertThat(externalEid.get(0).getValue(), is(equalTo(sampleEID))); - } - - @Test - public void testWhenPatientIsCreatedWithoutAnEIDTheGoldenResourceGetsAutomaticallyAssignedOne() { - Patient patient = createPatientAndUpdateLinks(buildJanePatient()); - MdmLink mdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(patient.getIdElement().getIdPartAsLong()).get(); - - Patient targetPatient = getTargetResourceFromMdmLink(mdmLink, "Patient"); - Identifier identifierFirstRep = targetPatient.getIdentifierFirstRep(); - assertThat(identifierFirstRep.getSystem(), is(equalTo(MdmConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM))); - assertThat(identifierFirstRep.getValue(), not(blankOrNullString())); - } - - @Test - public void testPatientAttributesAreCopiedOverWhenGoldenResourceIsCreatedFromPatient() { - Patient patient = createPatientAndUpdateLinks(buildPatientWithNameIdAndBirthday("Gary", "GARY_ID", new Date())); - - Optional mdmLink = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(patient.getIdElement().getIdPartAsLong()); - Patient read = getTargetResourceFromMdmLink(mdmLink.get(), "Patient"); - - // TODO NG - rules haven't been determined yet revisit once implemented... -// 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 testPatientMatchingAnotherPatientLinksToSameGoldenResource() { - Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); - Patient sameJanePatient = createPatientAndUpdateLinks(buildJanePatient()); - assertThat(janePatient, is(sameGoldenResourceAs(sameJanePatient))); - } - - @Test - public void testIncomingPatientWithEIDThatMatchesGoldenResourceWithHapiEidAddsExternalEidToGoldenResource() { - // Existing GoldenResource with system-assigned EID found linked from matched Patient. incoming Patient has EID. - // Replace GoldenResource system-assigned EID with Patient EID. - Patient patient = createPatientAndUpdateLinks(buildJanePatient()); - - IAnyResource janeGoldenResource = getGoldenResourceFromTargetResource(patient); - List hapiEid = myEidHelper.getHapiEid(janeGoldenResource); - 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 Golden Resource. - assertThat(patient, is(sameGoldenResourceAs(janePatient))); - - Patient sourcePatient = (Patient) getGoldenResourceFromTargetResource(patient); - - List identifier = sourcePatient.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.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.getValue(), is(equalTo("12345"))); - } - - @Test - public void testIncomingPatientWithEidMatchesAnotherPatientWithSameEIDAreLinked() { - // Create Use Case #3 - Patient patient1 = addExternalEID(buildJanePatient(), "uniqueid"); - createPatientAndUpdateLinks(patient1); - - Patient patient2 = buildPaulPatient(); - patient2 = addExternalEID(patient2, "uniqueid"); - createPatientAndUpdateLinks(patient2); - - assertThat(patient1, is(sameGoldenResourceAs(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(sameGoldenResourceAs(patient2))); - } - - @Test - public void testDuplicateGoldenResourceLinkIsCreatedWhenAnIncomingPatientArrivesWithEIDThatMatchesAnotherEIDPatient() { - - Patient patient1 = addExternalEID(buildJanePatient(), "eid-1"); - patient1 = createPatientAndUpdateLinks(patient1); - - Patient patient2 = addExternalEID(buildJanePatient(), "eid-2"); - patient2 = createPatientAndUpdateLinks(patient2); - - List possibleDuplicates = myMdmLinkDaoSvc.getPossibleDuplicates(); - assertThat(possibleDuplicates, hasSize(1)); - - - List duplicatePids = Stream.of(patient1, patient2) - .map(this::getGoldenResourceFromTargetResource) - .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))); - } - - @Test - public void testPatientWithNoMdmTagIsNotMatched() { - // Patient with "no-mdm" tag is not matched - Patient janePatient = buildJanePatient(); - janePatient.getMeta().addTag(MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_NO_MDM_MANAGED, "Don't MDM on me!"); - createPatientAndUpdateLinks(janePatient); - assertLinkCount(0); - } - - @Test - public void testPractitionersDoNotMatchToPatients() { - Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); - Practitioner janePractitioner = createPractitionerAndUpdateLinks(buildJanePractitioner()); - - assertLinkCount(2); - assertThat(janePatient, is(not(sameGoldenResourceAs(janePractitioner)))); - } - - @Test - public void testPractitionersThatMatchShouldLink() { - Practitioner janePractitioner = createPractitionerAndUpdateLinks(buildJanePractitioner()); - Practitioner anotherJanePractitioner = createPractitionerAndUpdateLinks(buildJanePractitioner()); - - assertLinkCount(2); - assertThat(anotherJanePractitioner, is(sameGoldenResourceAs(janePractitioner))); - } - - @Test - public void testWhenThereAreNoMATCHOrPOSSIBLE_MATCHOutcomesThatANewGoldenResourceIsCreated() { - /** - * CASE 1: No MATCHED and no PROBABLE_MATCHED outcomes -> a new GoldenResource resource - * is created and linked to that Pat/Prac. - */ - assertLinkCount(0); - Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); - assertLinkCount(1); - assertThat(janePatient, is(matchedToAGoldenResource())); - } - - @Test - public void testWhenAllMATCHResultsAreToSameGoldenResourceThatTheyAreLinked() { - /** - * CASE 2: All of the MATCHED Pat/Prac resources are already linked to the same GoldenResource -> - * a new Link is created between the new Pat/Prac and that GoldenResource and is set to MATCHED. - */ - Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); - Patient janePatient2 = createPatientAndUpdateLinks(buildJanePatient()); - - assertLinkCount(2); - assertThat(janePatient, is(sameGoldenResourceAs(janePatient2))); - - Patient incomingJanePatient = createPatientAndUpdateLinks(buildJanePatient()); - assertThat(incomingJanePatient, is(sameGoldenResourceAs(janePatient, janePatient2))); - assertThat(incomingJanePatient, is(linkedTo(janePatient, janePatient2))); - } - - @Test - public void testMATCHResultWithMultipleCandidatesCreatesPOSSIBLE_DUPLICATELinksAndNoGoldenResourceIsCreated() { - /** - * CASE 3: The MATCHED Pat/Prac resources link to more than one GoldenResource -> Mark all links as POSSIBLE_MATCH. - * All other GoldenResource resources are marked as POSSIBLE_DUPLICATE of this first GoldenResource. - */ - 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 GoldenResource for the purpose of this test. - IAnyResource goldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(janePatient2); - myMdmLinkSvc.updateLink(goldenResource, janePatient2, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, createContextForCreate("Patient")); - assertThat(janePatient, is(not(sameGoldenResourceAs(janePatient2)))); - - //In theory, this will match both GoldenResources! - 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 goldenResource. - assertThat(incomingJanePatient, is(possibleMatchWith(janePatient, janePatient2))); - - //Ensure there is no successful MATCH links for incomingJanePatient - Optional matchedLinkForTargetPid = myMdmLinkDaoSvc.getMatchedLinkForSourcePid(myIdHelperService.getPidOrNull(incomingJanePatient)); - assertThat(matchedLinkForTargetPid.isPresent(), is(false)); - - logAllLinks(); - assertLinksMatchResult(MATCH, MATCH, POSSIBLE_MATCH, POSSIBLE_MATCH, POSSIBLE_DUPLICATE); - assertLinksCreatedNewResource(true, true, false, false, false); - assertLinksMatchedByEid(false, false, false, false, false); - } - - @Test - public void testWhenAllMatchResultsArePOSSIBLE_MATCHThattheyAreLinkedAndNoGoldenResourceIsCreated() { - /** - * CASE 4: Only POSSIBLE_MATCH outcomes -> In this case, mdm-link records are created with POSSIBLE_MATCH - * outcome and await manual assignment to either NO_MATCH or MATCHED. GoldenResource link is added. - */ - Patient patient = buildJanePatient(); - patient.getNameFirstRep().setFamily("familyone"); - patient = createPatientAndUpdateLinks(patient); - assertThat(patient, is(sameGoldenResourceAs(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 = myPatientDao.search(buildGoldenRecordSearchParameterMap()); - assertEquals(1, bundle.size()); - Patient sourcePatient = (Patient) bundle.getResources(0, 1).get(0); - - //assertEquals(Person.IdentityAssuranceLevel.LEVEL2, sourcePatient.getLink().get(0).getAssurance()); - //assertEquals(Person.IdentityAssuranceLevel.LEVEL1, sourcePatient.getLink().get(1).getAssurance()); - //assertEquals(Person.IdentityAssuranceLevel.LEVEL1, sourcePatient.getLink().get(2).getAssurance()); - //TODO GGG MDM: Convert these asserts to checking the MPI_LINK table - - assertLinksMatchResult(MATCH, POSSIBLE_MATCH, POSSIBLE_MATCH); - assertLinksCreatedNewResource(true, false, false); - assertLinksMatchedByEid(false, false, false); - } - - private SearchParameterMap buildGoldenRecordSearchParameterMap() { - SearchParameterMap searchParameterMap = new SearchParameterMap(); - searchParameterMap.setLoadSynchronous(true); - searchParameterMap.add("_tag", new TokenParam(MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_HAPI_MDM_MANAGED)); - return searchParameterMap; - } - - @Test - public void testWhenAnIncomingResourceHasMatchesAndPossibleMatchesThatItLinksToMatch() { - Patient patient = buildJanePatient(); - patient.getNameFirstRep().setFamily("familyone"); - patient = createPatientAndUpdateLinks(patient); - assertThat(patient, is(sameGoldenResourceAs(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(sameGoldenResourceAs(patient)))); - assertThat(patient2, is(possibleMatchWith(patient))); - assertThat(patient3, is(sameGoldenResourceAs(patient))); - } - - @Test - public void testCreateGoldenResourceFromMdmTarget() { - // Create Use Case #2 - adding patient with no EID - Patient janePatient = buildJanePatient(); - Patient janeGoldenResourcePatient = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(janePatient); - - // golden record now contains HAPI-generated EID and HAPI tag - assertTrue(MdmResourceUtil.isMdmManaged(janeGoldenResourcePatient)); - assertFalse(myEidHelper.getHapiEid(janeGoldenResourcePatient).isEmpty()); - - // original checks - verifies that EIDs are assigned - assertThat("Resource must not be identical", janePatient != janeGoldenResourcePatient); - assertFalse(janePatient.getIdentifier().isEmpty()); - assertFalse(janeGoldenResourcePatient.getIdentifier().isEmpty()); - - CanonicalEID janeId = myEidHelper.getHapiEid(janePatient).get(0); - CanonicalEID janeGoldenResourceId = myEidHelper.getHapiEid(janeGoldenResourcePatient).get(0); - - // source and target EIDs must match, as target EID should be reset to the newly created EID - assertEquals(janeId.getValue(), janeGoldenResourceId.getValue()); - assertEquals(janeId.getSystem(), janeGoldenResourceId.getSystem()); - } - - //Case #1 - @Test - public void testPatientUpdateOverwritesGoldenResourceDataOnChanges() { - Patient janePatient = createPatientAndUpdateLinks(buildJanePatient()); - Patient janeSourcePatient = (Patient) getGoldenResourceFromTargetResource(janePatient); - - //Change Jane's name to paul. - Patient patient1 = buildPaulPatient(); - patient1.setId(janePatient.getId()); - Patient janePaulPatient = updatePatientAndUpdateLinks(patient1); - - assertThat(janeSourcePatient, is(sameGoldenResourceAs(janePaulPatient))); - - //Ensure the related GoldenResource was updated with new info. - Patient sourcePatientFromTarget = (Patient) getGoldenResourceFromTargetResource(janePaulPatient); - HumanName nameFirstRep = sourcePatientFromTarget.getNameFirstRep(); - - // TODO NG attribute propagation has been removed - revisit once source survivorship rules are defined - // assertThat(nameFirstRep.getGivenAsSingleString(), is(equalToIgnoringCase("paul"))); - } - - @Test - public void testPatientCreateDoesNotOverwriteGoldenResourceAttributesThatAreInvolvedInLinking() { - Patient paul = buildPaulPatient(); - paul.setGender(Enumerations.AdministrativeGender.MALE); - paul = createPatientAndUpdateLinks(paul); - - Patient sourcePatientFromTarget = (Patient) getGoldenResourceFromTargetResource(paul); - - // TODO NG - rules haven't been determined yet revisit once implemented... -// assertThat(sourcePatientFromTarget.getGender(), is(equalTo(Enumerations.AdministrativeGender.MALE))); - - Patient paul2 = buildPaulPatient(); - paul2.setGender(Enumerations.AdministrativeGender.FEMALE); - paul2 = createPatientAndUpdateLinks(paul2); - - assertThat(paul2, is(sameGoldenResourceAs(paul))); - - //Newly matched patients aren't allowed to overwrite GoldenResource Attributes unless they are empty, - // so gender should still be set to male. - Patient paul2GoldenResource = (Patient) getGoldenResourceFromTargetResource(paul2); -// assertThat(paul2GoldenResource.getGender(), is(equalTo(Enumerations.AdministrativeGender.MALE))); - } - - @Test - //Test Case #1 - public void testPatientUpdatesOverwriteGoldenResourceData() { - Patient paul = buildPaulPatient(); - String incorrectBirthdate = "1980-06-27"; - paul.getBirthDateElement().setValueAsString(incorrectBirthdate); - paul = createPatientAndUpdateLinks(paul); - - Patient sourcePatientFromTarget = (Patient) getGoldenResourceFromTargetResource(paul); - // TODO NG - rules haven't been determined yet revisit once implemented... -// assertThat(sourcePatientFromTarget.getBirthDateElement().getValueAsString(), is(incorrectBirthdate)); - - String correctBirthdate = "1990-06-28"; - paul.getBirthDateElement().setValueAsString(correctBirthdate); - - paul = updatePatientAndUpdateLinks(paul); - - sourcePatientFromTarget = (Patient) getGoldenResourceFromTargetResource(paul); - // TODO NG - rules haven't been determined yet revisit once implemented... -// assertThat(sourcePatientFromTarget.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)); - Patient originalPaulPatient = (Patient) getGoldenResourceFromTargetResource(paul); - - Patient jane = createPatientAndUpdateLinks(addExternalEID(buildJanePatient(), EID_2)); - Patient originalJanePatient = (Patient) getGoldenResourceFromTargetResource(jane); - - clearExternalEIDs(paul); - addExternalEID(paul, EID_2); - updatePatientAndUpdateLinks(paul); - - assertThat(originalJanePatient, is(possibleDuplicateOf(originalPaulPatient))); - assertThat(jane, is(sameGoldenResourceAs(paul))); - } - - @Test - public void testSinglyLinkedGoldenResourceThatGetsAnUpdatedEidSimplyUpdatesEID() { - //Use Case # 2 - String EID_1 = "123"; - String EID_2 = "456"; - - Patient paul = createPatientAndUpdateLinks(addExternalEID(buildPaulPatient(), EID_1)); - Patient originalPaulPatient = (Patient) getGoldenResourceFromTargetResource(paul); - - String oldEid = myEidHelper.getExternalEid(originalPaulPatient).get(0).getValue(); - assertThat(oldEid, is(equalTo(EID_1))); - - clearExternalEIDs(paul); - addExternalEID(paul, EID_2); - - paul = updatePatientAndUpdateLinks(paul); - assertNoDuplicates(); - - Patient newlyFoundPaulPatient = (Patient) getGoldenResourceFromTargetResource(paul); - assertThat(originalPaulPatient, is(sameGoldenResourceAs(newlyFoundPaulPatient))); - String newEid = myEidHelper.getExternalEid(newlyFoundPaulPatient).get(0).getValue(); - assertThat(newEid, is(equalTo(EID_2))); - } - - private void assertNoDuplicates() { - List possibleDuplicates = myMdmLinkDaoSvc.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 GoldenResource has 2 eids. - assertThat(patient2, is(sameGoldenResourceAs(patient3))); - assertNoDuplicates(); - // GoldenResource A -> {P1} - // GoldenResource B -> {P2, P3} - - patient2.getIdentifier().clear(); - addExternalEID(patient2, "eid-1"); - patient2 = updatePatientAndUpdateLinks(patient2); - - // GoldenResource A -> {P1, P2} - // GoldenResource B -> {P3} - // Possible duplicates A<->B - - assertThat(patient2, is(sameGoldenResourceAs(patient1))); - - List possibleDuplicates = myMdmLinkDaoSvc.getPossibleDuplicates(); - assertThat(possibleDuplicates, hasSize(1)); - assertThat(patient3, is(possibleDuplicateOf(patient1))); - - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceDaoSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceDaoSvcTest.java deleted file mode 100644 index 4dbac482c9b..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceDaoSvcTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package ca.uhn.fhir.jpa.mdm.svc; - -import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; -import ca.uhn.fhir.mdm.util.MdmResourceUtil; -import org.hl7.fhir.instance.model.api.IAnyResource; -import org.hl7.fhir.r4.model.Patient; -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 MdmResourceDaoSvcTest extends BaseMdmR4Test { - private static final String TEST_EID = "TEST_EID"; - @Autowired - MdmResourceDaoSvc myResourceDaoSvc; - - @Test - public void testSearchPatientByEidExcludesNonGoldenPatients() { - Patient goodSourcePatient = addExternalEID(createGoldenPatient(), TEST_EID); - - myPatientDao.update(goodSourcePatient); - - - Patient badSourcePatient = addExternalEID(createRedirectedGoldenPatient(new Patient()), TEST_EID); - MdmResourceUtil.setGoldenResourceRedirected(badSourcePatient); - myPatientDao.update(badSourcePatient); - - Optional foundGoldenResource = myResourceDaoSvc.searchGoldenResourceByEID(TEST_EID, "Patient"); - assertTrue(foundGoldenResource.isPresent()); - assertThat(foundGoldenResource.get().getIdElement().toUnqualifiedVersionless().getValue(), is(goodSourcePatient.getIdElement().toUnqualifiedVersionless().getValue())); - } - - @Test - public void testSearcGoldenResourceByEidExcludesNonMdmManaged() { - Patient goodSourcePatient = addExternalEID(createGoldenPatient(), TEST_EID); - myPatientDao.update(goodSourcePatient); - - Patient badSourcePatient = addExternalEID(createPatient(new Patient()), TEST_EID); - myPatientDao.update(badSourcePatient); - - Optional foundSourcePatient = myResourceDaoSvc.searchGoldenResourceByEID(TEST_EID, "Patient"); - assertTrue(foundSourcePatient.isPresent()); - assertThat(foundSourcePatient.get().getIdElement().toUnqualifiedVersionless().getValue(), is(goodSourcePatient.getIdElement().toUnqualifiedVersionless().getValue())); - } -} diff --git a/hapi-fhir-jpaserver-mdm/src/test/resources/mdm/mdm-rules.json b/hapi-fhir-jpaserver-mdm/src/test/resources/mdm/mdm-rules.json deleted file mode 100644 index 15ffb644d5e..00000000000 --- a/hapi-fhir-jpaserver-mdm/src/test/resources/mdm/mdm-rules.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "version": "1", - "mdmTypes": ["Patient", "Practitioner", "Medication"], - "candidateSearchParams": [ - { - "resourceType": "Patient", - "searchParams": [ - "birthdate" - ] - }, - { - "resourceType": "*", - "searchParams": [ - "identifier" - ] - }, - { - "resourceType": "Medication", - "searchParams": [ - "manufacturer" - ] - }, - { - "resourceType": "Patient", - "searchParams": [ - "general-practitioner" - ] - } - ], - "candidateFilterSearchParams": [ - { - "resourceType": "Practitioner", - "searchParam": "active", - "fixedValue": "true" - }, - { - "resourceType": "Patient", - "searchParam": "active", - "fixedValue": "true" - } - ], - "matchFields": [ - { - "name": "matched-medication-code", - "resourceType": "Medication", - "resourcePath": "code.coding.code", - "matcher": { - "algorithm": "STRING" - } - }, - { - "name": "cosine-given-name", - "resourceType": "Patient", - "resourcePath": "name.given", - "similarity": { - "algorithm": "COSINE", - "matchThreshold": 0.8, - "exact": true - } - }, - { - "name": "jaro-last-name", - "resourceType": "Patient", - "resourcePath": "name.family", - "similarity": { - "algorithm": "JARO_WINKLER", - "matchThreshold": 0.8, - "exact": true - } - }, - { - "name": "medicare-id", - "resourceType": "Patient", - "resourcePath": "identifier", - "matcher": { - "algorithm": "IDENTIFIER", - "identifierSystem": "http://hl7.org/fhir/sid/us-medicare" - } - }, - { - "name": "cosine-given-name-pract", - "resourceType": "Practitioner", - "resourcePath": "name.given", - "similarity": { - "algorithm": "COSINE", - "matchThreshold": 0.8, - "exact": true - } - }, - { - "name": "jaro-last-name-pract", - "resourceType": "Practitioner", - "resourcePath": "name.family", - "similarity": { - "algorithm": "JARO_WINKLER", - "matchThreshold": 0.8, - "exact": true - } - }, - { - "name": "medicare-id-pract", - "resourceType": "Practitioner", - "resourcePath": "identifier", - "matcher": { - "algorithm": "IDENTIFIER", - "identifierSystem": "http://hl7.org/fhir/sid/us-medicare" - } - } - ], - "matchResultMap": { - "cosine-given-name": "POSSIBLE_MATCH", - "cosine-given-name,jaro-last-name": "MATCH", - "cosine-given-name-pract": "POSSIBLE_MATCH", - "cosine-given-name-pract,jaro-last-name-pract": "MATCH", - "matched-medication-code": "MATCH" - }, - "eidSystem": "http://company.io/fhir/NamingSystem/custom-eid-system" -} diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index 56ae4bc7cbc..fa505b7f2bf 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -21,6 +21,11 @@ package ca.uhn.fhir.jpa.migrate.tasks; */ import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.entity.EmpiLink; +import ca.uhn.fhir.jpa.entity.Search; +import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; +import ca.uhn.fhir.jpa.entity.TermConceptMap; +import ca.uhn.fhir.jpa.entity.TermValueSet; import ca.uhn.fhir.jpa.migrate.DriverTypeEnum; import ca.uhn.fhir.jpa.migrate.taskdef.ArbitrarySqlTask; import ca.uhn.fhir.jpa.migrate.taskdef.CalculateHashesTask; @@ -69,20 +74,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { init430(); // Replaced by 5.0.0 init500(); // 20200218 - 20200513 init501(); // 20200514 - 20200515 - init510(); // 20200516 - 20201028 - init520(); // 20201029 - Present - } - - protected void init520() { - Builder version = forVersion(VersionEnum.V5_2_0); - - Builder.BuilderWithTableName mdmLink = version.onTable("MPI_LINK"); - mdmLink.addColumn("20201029.1", "GOLDEN_RESOURCE_PID").nonNullable().type(ColumnTypeEnum.LONG); - mdmLink.addColumn("20201029.2", "RULE_COUNT").nullable().type(ColumnTypeEnum.LONG); - mdmLink - .addForeignKey("20201029.3", "FK_EMPI_LINK_GOLDEN_RESOURCE") - .toColumn("GOLDEN_RESOURCE_PID") - .references("HFJ_RESOURCE", "RES_ID"); + init510(); // 20200516 - 20201112 } protected void init510() { @@ -150,7 +142,6 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { empiLink.addColumn("20200715.4", "VECTOR").nullable().type(ColumnTypeEnum.LONG); empiLink.addColumn("20200715.5", "SCORE").nullable().type(ColumnTypeEnum.FLOAT); - init510_20200725(); //EMPI Target Type diff --git a/hapi-fhir-server-mdm/pom.xml b/hapi-fhir-server-empi/pom.xml similarity index 97% rename from hapi-fhir-server-mdm/pom.xml rename to hapi-fhir-server-empi/pom.xml index 6d70462eda0..9e31608ba13 100644 --- a/hapi-fhir-server-mdm/pom.xml +++ b/hapi-fhir-server-empi/pom.xml @@ -11,10 +11,10 @@ ../hapi-deployable-pom/pom.xml - hapi-fhir-server-mdm + hapi-fhir-server-empi jar - HAPI FHIR - Master Data Management + HAPI FHIR - Enterprise Master Patient Index diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiConstants.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiConstants.java new file mode 100644 index 00000000000..1a1d53eabc0 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiConstants.java @@ -0,0 +1,37 @@ +package ca.uhn.fhir.empi.api; + +/*- + * #%L + * HAPI FHIR - 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% + */ + +public class EmpiConstants { + /** + * TAG system for Person resources which are managed by HAPI EMPI. + */ + + public static final String SYSTEM_EMPI_MANAGED = "https://hapifhir.org/NamingSystem/managing-empi-system"; + public static final String CODE_HAPI_EMPI_MANAGED = "HAPI-EMPI"; + public static final String DISPLAY_HAPI_EMPI_MANAGED = "This Person can only be modified by Smile CDR's EMPI system."; + public static final String CODE_NO_EMPI_MANAGED = "NO-EMPI"; + public static final String HAPI_ENTERPRISE_IDENTIFIER_SYSTEM = "http://hapifhir.io/fhir/NamingSystem/empi-person-enterprise-id"; + public static final String ALL_RESOURCE_SEARCH_PARAM_TYPE = "*"; + + public static final String FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE = "http://hl7.org/fhir/StructureDefinition/match-grade"; + +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmLinkJson.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiLinkJson.java similarity index 56% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmLinkJson.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiLinkJson.java index da3285ccab4..d115df20faa 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmLinkJson.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiLinkJson.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.api; +package ca.uhn.fhir.empi.api; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -25,19 +25,18 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Date; -public class MdmLinkJson implements IModelJson { +public class EmpiLinkJson implements IModelJson { + @JsonProperty("personId") + private String myPersonId; - @JsonProperty("goldenResourceId") - private String myGoldenResourceId; - - @JsonProperty("sourceId") - private String mySourceId; + @JsonProperty("targetId") + private String myTargetId; @JsonProperty("matchResult") - private MdmMatchResultEnum myMatchResult; + private EmpiMatchResultEnum myMatchResult; @JsonProperty("linkSource") - private MdmLinkSourceEnum myLinkSource; + private EmpiLinkSourceEnum myLinkSource; @JsonProperty("created") private Date myCreated; @@ -52,9 +51,9 @@ public class MdmLinkJson implements IModelJson { @JsonProperty("eidMatch") private Boolean myEidMatch; - /** This link created a new golden resource **/ - @JsonProperty("linkCreatedNewGoldenResource") - private Boolean myLinkCreatedNewResource; + /** This link created a new person **/ + @JsonProperty("newPerson") + private Boolean myNewPerson; @JsonProperty("vector") private Long myVector; @@ -62,38 +61,38 @@ public class MdmLinkJson implements IModelJson { @JsonProperty("score") private Double myScore; - public String getGoldenResourceId() { - return myGoldenResourceId; + public String getPersonId() { + return myPersonId; } - public MdmLinkJson setGoldenResourceId(String theGoldenResourceId) { - myGoldenResourceId = theGoldenResourceId; + public EmpiLinkJson setPersonId(String thePersonId) { + myPersonId = thePersonId; return this; } - public String getSourceId() { - return mySourceId; + public String getTargetId() { + return myTargetId; } - public MdmLinkJson setSourceId(String theSourceId) { - mySourceId = theSourceId; + public EmpiLinkJson setTargetId(String theTargetId) { + myTargetId = theTargetId; return this; } - public MdmMatchResultEnum getMatchResult() { + public EmpiMatchResultEnum getMatchResult() { return myMatchResult; } - public MdmLinkJson setMatchResult(MdmMatchResultEnum theMatchResult) { + public EmpiLinkJson setMatchResult(EmpiMatchResultEnum theMatchResult) { myMatchResult = theMatchResult; return this; } - public MdmLinkSourceEnum getLinkSource() { + public EmpiLinkSourceEnum getLinkSource() { return myLinkSource; } - public MdmLinkJson setLinkSource(MdmLinkSourceEnum theLinkSource) { + public EmpiLinkJson setLinkSource(EmpiLinkSourceEnum theLinkSource) { myLinkSource = theLinkSource; return this; } @@ -102,7 +101,7 @@ public class MdmLinkJson implements IModelJson { return myCreated; } - public MdmLinkJson setCreated(Date theCreated) { + public EmpiLinkJson setCreated(Date theCreated) { myCreated = theCreated; return this; } @@ -111,7 +110,7 @@ public class MdmLinkJson implements IModelJson { return myUpdated; } - public MdmLinkJson setUpdated(Date theUpdated) { + public EmpiLinkJson setUpdated(Date theUpdated) { myUpdated = theUpdated; return this; } @@ -120,7 +119,7 @@ public class MdmLinkJson implements IModelJson { return myVersion; } - public MdmLinkJson setVersion(String theVersion) { + public EmpiLinkJson setVersion(String theVersion) { myVersion = theVersion; return this; } @@ -129,17 +128,17 @@ public class MdmLinkJson implements IModelJson { return myEidMatch; } - public MdmLinkJson setEidMatch(Boolean theEidMatch) { + public EmpiLinkJson setEidMatch(Boolean theEidMatch) { myEidMatch = theEidMatch; return this; } - public Boolean getLinkCreatedNewResource() { - return myLinkCreatedNewResource; + public Boolean getNewPerson() { + return myNewPerson; } - public MdmLinkJson setLinkCreatedNewResource(Boolean theLinkCreatedNewResource) { - myLinkCreatedNewResource = theLinkCreatedNewResource; + public EmpiLinkJson setNewPerson(Boolean theNewPerson) { + myNewPerson = theNewPerson; return this; } @@ -147,7 +146,7 @@ public class MdmLinkJson implements IModelJson { return myVector; } - public MdmLinkJson setVector(Long theVector) { + public EmpiLinkJson setVector(Long theVector) { myVector = theVector; return this; } @@ -156,7 +155,7 @@ public class MdmLinkJson implements IModelJson { return myScore; } - public MdmLinkJson setScore(Double theScore) { + public EmpiLinkJson setScore(Double theScore) { myScore = theScore; return this; } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmLinkSourceEnum.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiLinkSourceEnum.java similarity index 83% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmLinkSourceEnum.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiLinkSourceEnum.java index 7655cb8b63c..f763896f0a5 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmLinkSourceEnum.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiLinkSourceEnum.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.api; +package ca.uhn.fhir.empi.api; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -20,16 +20,17 @@ package ca.uhn.fhir.mdm.api; * #L% */ -public enum MdmLinkSourceEnum { - +public enum EmpiLinkSourceEnum { /** * Link was created or last modified by an algorithm */ AUTO, /** - * Link was created or last modified manually by a system user + * Link was created or last modified by a person */ + MANUAL + // Stored in database as ORDINAL. Only add new values to bottom! } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmMatchEvaluation.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiMatchEvaluation.java similarity index 66% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmMatchEvaluation.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiMatchEvaluation.java index 4b82e925210..cfa988ba7bd 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmMatchEvaluation.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiMatchEvaluation.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.api; +package ca.uhn.fhir.empi.api; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -20,17 +20,16 @@ package ca.uhn.fhir.mdm.api; * #L% */ -public class MdmMatchEvaluation { - +public class EmpiMatchEvaluation { public final boolean match; public final double score; - public MdmMatchEvaluation(boolean theMatch, double theScore) { + public EmpiMatchEvaluation(boolean theMatch, double theScore) { match = theMatch; score = theScore; } - public static MdmMatchEvaluation max(MdmMatchEvaluation theLeft, MdmMatchEvaluation theRight) { - return new MdmMatchEvaluation(theLeft.match | theRight.match, Math.max(theLeft.score, theRight.score)); + public static EmpiMatchEvaluation max(EmpiMatchEvaluation theLeft, EmpiMatchEvaluation theRight) { + return new EmpiMatchEvaluation(theLeft.match | theRight.match, Math.max(theLeft.score, theRight.score)); } } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiMatchOutcome.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiMatchOutcome.java new file mode 100644 index 00000000000..4290773c7af --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiMatchOutcome.java @@ -0,0 +1,160 @@ +package ca.uhn.fhir.empi.api; + +/*- + * #%L + * HAPI FHIR - 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 org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * This data object captures the final outcome of an EMPI match + */ +public final class EmpiMatchOutcome { + + public static final EmpiMatchOutcome POSSIBLE_DUPLICATE = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.POSSIBLE_DUPLICATE); + public static final EmpiMatchOutcome NO_MATCH = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.NO_MATCH); + public static final EmpiMatchOutcome NEW_PERSON_MATCH = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.MATCH).setNewPerson(true); + public static final EmpiMatchOutcome EID_MATCH = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.MATCH).setEidMatch(true); + public static final EmpiMatchOutcome POSSIBLE_MATCH = new EmpiMatchOutcome(null, null).setMatchResultEnum(EmpiMatchResultEnum.POSSIBLE_MATCH); + + /** + * A bitmap that indicates which rules matched + */ + public final Long vector; + + /** + * The sum of all scores for all rules evaluated. Similarity rules add the similarity score (between 0.0 and 1.0) whereas + * matcher rules add either a 0.0 or 1.0. + */ + public final Double score; + + /** + * Did the EMPI match operation result in creating a new Person resource? + */ + private boolean myNewPerson; + + /** + * Did the EMPI match occur as a result of EIDs matching? + */ + private boolean myEidMatch; + + /** + * Based on the EMPI Rules, what was the final match result? + */ + private EmpiMatchResultEnum myMatchResultEnum; + + /** + * Total number of EMPI rules checked for this outcome + */ + private int myEmpiRuleCount; + + public EmpiMatchOutcome(Long theVector, Double theScore) { + vector = theVector; + score = theScore; + } + + public boolean isMatch() { + return myMatchResultEnum == EmpiMatchResultEnum.MATCH; + } + + public boolean isPossibleMatch() { + return myMatchResultEnum == EmpiMatchResultEnum.POSSIBLE_MATCH; + } + + + public boolean isPossibleDuplicate() { + return myMatchResultEnum == EmpiMatchResultEnum.POSSIBLE_DUPLICATE; + } + + public EmpiMatchResultEnum getMatchResultEnum() { + return myMatchResultEnum; + } + + public EmpiMatchOutcome setMatchResultEnum(EmpiMatchResultEnum theMatchResultEnum) { + myMatchResultEnum = theMatchResultEnum; + return this; + } + + public boolean isNewPerson() { + return myNewPerson; + } + + /** @param theNewPerson this match is creating a new person */ + public EmpiMatchOutcome setNewPerson(boolean theNewPerson) { + myNewPerson = theNewPerson; + return this; + } + + public boolean isEidMatch() { + return myEidMatch; + } + + /** + * Sets the number of EMPI rules checked for this match outcome + * + * @param theEmpiRuleCount + * Number of EMPI rules that were checked for this match outcome + * @return + * Returns this instance + */ + public EmpiMatchOutcome setEmpiRuleCount(int theEmpiRuleCount) { + myEmpiRuleCount = theEmpiRuleCount; + return this; + } + + /** + * Gets the number of EMPI rules checked for this match outcome + * + * @return + * Returns the number of rules + */ + public int getEmpiRuleCount() { + return myEmpiRuleCount; + } + + /** @param theEidMatch the link was established via a shared EID */ + public EmpiMatchOutcome setEidMatch(boolean theEidMatch) { + myEidMatch = theEidMatch; + return this; + } + + /** + * Gets normalized score that is in the range from zero to one + * + * @return + * Returns the normalized score + */ + public Double getNormalizedScore() { + if (myEmpiRuleCount == 0) { + return 0.0; + } + return score / myEmpiRuleCount; + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("vector", vector) + .append("score", score) + .append("myNewPerson", myNewPerson) + .append("myEidMatch", myEidMatch) + .append("myMatchResultEnum", myMatchResultEnum) + .toString(); + } +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmMatchResultEnum.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiMatchResultEnum.java similarity index 64% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmMatchResultEnum.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiMatchResultEnum.java index 278fac12a35..1626e780581 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmMatchResultEnum.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/EmpiMatchResultEnum.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.api; +package ca.uhn.fhir.empi.api; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -20,8 +20,7 @@ package ca.uhn.fhir.mdm.api; * #L% */ -public enum MdmMatchResultEnum { - +public enum EmpiMatchResultEnum { /** * Manually confirmed to not be a match. */ @@ -38,20 +37,21 @@ public enum MdmMatchResultEnum { MATCH, /** - * Link between two Golden Records resources indicating they may be duplicates. + * Link between two Person resources indicating they may be duplicates. */ POSSIBLE_DUPLICATE, /** - * Link between Golden Record and Source Resource pointing to the Golden Record for that Source Resource + * Link between Person and Target pointing to the Golden Record for that Person */ + GOLDEN_RECORD, /** - * Link between two Golden Resources resulting from a merge. One golden resource is deactivated. The inactive golden - * resource points to the active golden resource after the merge. The source resource points to the inactive golden - * resource after the merge. + * Link between two Person resources resulting from a merge. The Person points to the active person after the merge + * and the Target points to the inactive person after the merge. */ + REDIRECT // Stored in database as ORDINAL. Only add new values to bottom! } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmChannelSubmitterSvc.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiChannelSubmitterSvc.java similarity index 72% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmChannelSubmitterSvc.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiChannelSubmitterSvc.java index 356edb34201..188e814d9ae 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmChannelSubmitterSvc.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiChannelSubmitterSvc.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.api; +package ca.uhn.fhir.empi.api; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -22,12 +22,12 @@ package ca.uhn.fhir.mdm.api; import org.hl7.fhir.instance.model.api.IBaseResource; -public interface IMdmChannelSubmitterSvc { +public interface IEmpiChannelSubmitterSvc { /** - * Given an IBaseResource, submit it to the MDM channel for processing. + * Given an IBaseResource, submit it to the EMPI channel for processing. * - * @param theResource the {@link IBaseResource} that should have MDM processing applied to it. + * @param theResource the {@link IBaseResource} that should have EMPI processing applied to it. */ - void submitResourceToMdmChannel(IBaseResource theResource); + void submitResourceToEmpiChannel(IBaseResource theResource); } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiControllerSvc.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiControllerSvc.java new file mode 100644 index 00000000000..4895064452f --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiControllerSvc.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.empi.api; + +/*- + * #%L + * HAPI FHIR - 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.model.EmpiTransactionContext; +import org.hl7.fhir.instance.model.api.IAnyResource; + +import javax.annotation.Nullable; +import java.util.stream.Stream; + +public interface IEmpiControllerSvc { + Stream queryLinks(@Nullable String thePersonId, @Nullable String theTargetId, @Nullable String theMatchResult, @Nullable String theLinkSource, EmpiTransactionContext theEmpiContext); + Stream getDuplicatePersons(EmpiTransactionContext theEmpiContext); + void notDuplicatePerson(String thePersonId, String theTargetPersonId, EmpiTransactionContext theEmpiContext); + IAnyResource mergePersons(String theFromPersonId, String theToPersonId, EmpiTransactionContext theEmpiTransactionContext); + IAnyResource updateLink(String thePersonId, String theTargetId, String theMatchResult, EmpiTransactionContext theEmpiContext); +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmExpungeSvc.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiExpungeSvc.java similarity index 57% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmExpungeSvc.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiExpungeSvc.java index 9c97e06607b..bc5b0c45264 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmExpungeSvc.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiExpungeSvc.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.api; +package ca.uhn.fhir.empi.api; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -22,22 +22,24 @@ package ca.uhn.fhir.mdm.api; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -public interface IMdmExpungeSvc { +public interface IEmpiExpungeSvc { /** - * Given a resource type, delete the underlying MDM links, and their related golden resource objects. + * Given a resource type, delete the underlying EMPI links, and their related person objects. + * + * @param theResourceType The type of resources * - * @param theSourceResourceType The type of resources * @param theRequestDetails - * @return the count of deleted MDM links + * @return the count of deleted EMPI links */ - long expungeAllMdmLinksOfSourceType(String theSourceResourceType, ServletRequestDetails theRequestDetails); + long expungeAllEmpiLinksOfTargetType(String theResourceType, ServletRequestDetails theRequestDetails); /** - * Delete all MDM links, and their related golden resource objects. + * Delete all EMPI links, and their related Person objects. * - * @return the count of deleted MDM links + * + * @return the count of deleted EMPI links * @param theRequestDetails */ - long expungeAllMdmLinks(ServletRequestDetails theRequestDetails); + long expungeAllEmpiLinks(ServletRequestDetails theRequestDetails); } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmLinkQuerySvc.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiLinkQuerySvc.java similarity index 57% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmLinkQuerySvc.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiLinkQuerySvc.java index 35dc4846191..c4beb84f16d 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmLinkQuerySvc.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiLinkQuerySvc.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.api; +package ca.uhn.fhir.empi.api; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -20,15 +20,15 @@ package ca.uhn.fhir.mdm.api; * #L% */ -import ca.uhn.fhir.mdm.model.MdmTransactionContext; +import ca.uhn.fhir.empi.model.EmpiTransactionContext; import org.hl7.fhir.instance.model.api.IIdType; import java.util.stream.Stream; /** - * This service supports the MDM operation providers for those services that return multiple MDM links. + * This service supports the EMPI Operation providers for those services that return multiple empi links. */ -public interface IMdmLinkQuerySvc { - Stream queryLinks(IIdType theGoldenResourceId, IIdType theSourceResourceId, MdmMatchResultEnum theMatchResult, MdmLinkSourceEnum theLinkSource, MdmTransactionContext theMdmContext); - Stream getDuplicateGoldenResources(MdmTransactionContext theMdmContext); +public interface IEmpiLinkQuerySvc { + Stream queryLinks(IIdType thePersonId, IIdType theTargetId, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiContext); + Stream getDuplicatePersons(EmpiTransactionContext theEmpiContext); } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiLinkSvc.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiLinkSvc.java new file mode 100644 index 00000000000..15c1c621f22 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiLinkSvc.java @@ -0,0 +1,52 @@ +package ca.uhn.fhir.empi.api; + +/*- + * #%L + * HAPI FHIR - 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.model.EmpiTransactionContext; +import org.hl7.fhir.instance.model.api.IAnyResource; + +public interface IEmpiLinkSvc { + + /** + * Update a link between a Person record and its target Patient/Practitioner record. If a link does not exist between + * these two records, create it. + * @param thePerson the Person to link the target resource to. + * @param theTargetResource the target resource, which is a Patient or Practitioner + * @param theMatchResult the current status of the match to set the link to. + * @param theLinkSource MANUAL or AUTO: what caused the link. + * @param theEmpiTransactionContext + */ + void updateLink(IAnyResource thePerson, IAnyResource theTargetResource, EmpiMatchOutcome theMatchResult, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiTransactionContext); + + /** + * Replace Person.link values from what they should be based on EmpiLink values + * @param thePersonResource the person to correct the links on. + */ + void syncEmpiLinksToPersonLinks(IAnyResource thePersonResource, EmpiTransactionContext theEmpiTransactionContext); + + /** + * Delete a link between given Person and target patient/practitioner + * @param theExistingPerson + * @param theResource + */ + void deleteLink(IAnyResource theExistingPerson, IAnyResource theResource, EmpiTransactionContext theEmpiTransactionContext); + +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmLinkUpdaterSvc.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiLinkUpdaterSvc.java similarity index 60% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmLinkUpdaterSvc.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiLinkUpdaterSvc.java index f1fe0f7fc3e..72710e2e11e 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmLinkUpdaterSvc.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiLinkUpdaterSvc.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.api; +package ca.uhn.fhir.empi.api; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -20,10 +20,10 @@ package ca.uhn.fhir.mdm.api; * #L% */ -import ca.uhn.fhir.mdm.model.MdmTransactionContext; +import ca.uhn.fhir.empi.model.EmpiTransactionContext; import org.hl7.fhir.instance.model.api.IAnyResource; -public interface IMdmLinkUpdaterSvc { - IAnyResource updateLink(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmMatchResultEnum theMatchResult, MdmTransactionContext theMdmContext); - void notDuplicateGoldenResource(IAnyResource theGoldenResource, IAnyResource theTargetGoldenResource, MdmTransactionContext theMdmContext); +public interface IEmpiLinkUpdaterSvc { + IAnyResource updateLink(IAnyResource thePerson, IAnyResource theTarget, EmpiMatchResultEnum theMatchResult, EmpiTransactionContext theEmpiContext); + void notDuplicatePerson(IAnyResource thePerson, IAnyResource theTarget, EmpiTransactionContext theEmpiContext); } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmMatchFinderSvc.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiMatchFinderSvc.java similarity index 74% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmMatchFinderSvc.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiMatchFinderSvc.java index 8d18fa0b445..3304b8e9a23 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmMatchFinderSvc.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiMatchFinderSvc.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.api; +package ca.uhn.fhir.empi.api; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -21,15 +21,15 @@ package ca.uhn.fhir.mdm.api; */ import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseResource; import javax.annotation.Nonnull; import java.util.List; -public interface IMdmMatchFinderSvc { - +public interface IEmpiMatchFinderSvc { /** - * Retrieve a list of possible target candidates for matching, based on the given {@link IAnyResource} - * Internally, performs all MDM matching rules on the type of the resource. + * Retrieve a list of possible Patient/Practitioner candidates for matching, based on the given {@link IBaseResource} + * Internally, performs all EMPI matching rules on the type of the resource. * * @param theResourceType the type of the resource. * @param theResource the resource that we are attempting to find matches for. diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiPersonMergerSvc.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiPersonMergerSvc.java new file mode 100644 index 00000000000..1ebd9a54215 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiPersonMergerSvc.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.empi.api; + +/*- + * #%L + * HAPI FHIR - 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.model.EmpiTransactionContext; +import org.hl7.fhir.instance.model.api.IAnyResource; + +public interface IEmpiPersonMergerSvc { + /** + * Move all links from the theFromPerson to theToPerson and then set active=false on theFromPerson. Merge all Person + * fields. + * @param theFromPerson the person we are merging from + * @param theToPerson the person we are merging to + * @return updated theToPerson with the merged fields and links. + */ + IAnyResource mergePersons(IAnyResource theFromPerson, IAnyResource theToPerson, EmpiTransactionContext theEmpiTransactionContext); +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmRuleValidator.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiRuleValidator.java similarity index 75% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmRuleValidator.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiRuleValidator.java index d0bc5ed76d3..69a0c5095c8 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmRuleValidator.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiRuleValidator.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.api; +package ca.uhn.fhir.empi.api; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -20,8 +20,8 @@ package ca.uhn.fhir.mdm.api; * #L% */ -import ca.uhn.fhir.mdm.rules.json.MdmRulesJson; +import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; -public interface IMdmRuleValidator { - void validate(MdmRulesJson theMdmRules); +public interface IEmpiRuleValidator { + void validate(EmpiRulesJson theEmpiRules); } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiSettings.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiSettings.java new file mode 100644 index 00000000000..bbb4b0be85e --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiSettings.java @@ -0,0 +1,41 @@ +package ca.uhn.fhir.empi.api; + +/*- + * #%L + * HAPI FHIR - 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.rules.json.EmpiRulesJson; + +public interface IEmpiSettings { + String EMPI_CHANNEL_NAME = "empi"; + // Parallel processing of EMPI can result in missed matches. Best to single-thread. + int EMPI_DEFAULT_CONCURRENT_CONSUMERS = 1; + + boolean isEnabled(); + + int getConcurrentConsumers(); + + EmpiRulesJson getEmpiRules(); + + boolean isPreventEidUpdates(); + + boolean isPreventMultipleEids(); + + String getRuleVersion(); +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiSubmitSvc.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiSubmitSvc.java new file mode 100644 index 00000000000..6e849277aa7 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/IEmpiSubmitSvc.java @@ -0,0 +1,73 @@ +package ca.uhn.fhir.empi.api; + +/*- + * #%L + * HAPI FHIR - 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 org.hl7.fhir.instance.model.api.IIdType; + +import javax.annotation.Nullable; + +public interface IEmpiSubmitSvc { + + /** + * Submit all eligible resources for EMPI processing. + * @param theCriteria The FHIR search critieria for filtering the resources to be submitted for EMPI processing. + * NOTE: + * When using this function, the criteria supplied must be valid for all EMPI types. e.g. , if you + * run this with the criteria birthDate=1990-06-28, it will fail, as Practitioners do not have a birthday. + * Use with caution. + * + * @return + */ + long submitAllTargetTypesToEmpi(@Nullable String theCriteria); + + /** + * Given a type and a search criteria, submit all found resources for EMPI processing. + * + * @param theTargetType the resource type that you wish to execute a search over for submission to EMPI. + * @param theCriteria The FHIR search critieria for filtering the resources to be submitted for EMPI processing.. + * @return the number of resources submitted for EMPI processing. + */ + long submitTargetTypeToEmpi(String theTargetType, String theCriteria); + + /** + * Convenience method that calls {@link #submitTargetTypeToEmpi(String, String)} with the type pre-populated. + * + * @param theCriteria The FHIR search critieria for filtering the resources to be submitted for EMPI processing. + * @return the number of resources submitted for EMPI processing. + */ + long submitPractitionerTypeToEmpi(String theCriteria); + + /** + * Convenience method that calls {@link #submitTargetTypeToEmpi(String, String)} with the type pre-populated. + * + * @param theCriteria The FHIR search critieria for filtering the resources to be submitted for EMPI processing. + * @return the number of resources submitted for EMPI processing. + */ + long submitPatientTypeToEmpi(String theCriteria); + + /** + * Given an ID and a target type valid for EMPI, manually submit the given ID for EMPI processing. + * @param theId the ID of the resource to process for EMPI. + * @return the constant `1`, as if this function returns successfully, it will have processed one resource for EMPI. + */ + long submitTargetToEmpi(IIdType theId); + +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MatchedTarget.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/MatchedTarget.java similarity index 81% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MatchedTarget.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/MatchedTarget.java index 3c0be739d15..c709280f6f7 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MatchedTarget.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/api/MatchedTarget.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.api; +package ca.uhn.fhir.empi.api; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -25,9 +25,9 @@ import org.hl7.fhir.instance.model.api.IAnyResource; public class MatchedTarget { private final IAnyResource myTarget; - private final MdmMatchOutcome myMatchResult; + private final EmpiMatchOutcome myMatchResult; - public MatchedTarget(IAnyResource theTarget, MdmMatchOutcome theMatchResult) { + public MatchedTarget(IAnyResource theTarget, EmpiMatchOutcome theMatchResult) { myTarget = theTarget; myMatchResult = theMatchResult; } @@ -36,7 +36,7 @@ public class MatchedTarget { return myTarget; } - public MdmMatchOutcome getMatchResult() { + public EmpiMatchOutcome getMatchResult() { return myMatchResult; } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/log/Logs.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/log/Logs.java similarity index 71% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/log/Logs.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/log/Logs.java index cc6bb321c0e..77f4215b1ec 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/log/Logs.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/log/Logs.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.log; +package ca.uhn.fhir.empi.log; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -24,9 +24,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Logs { - private static final Logger ourMdmTroubleshootingLog = LoggerFactory.getLogger("ca.uhn.fhir.log.mdm_troubleshooting"); + private static final Logger ourEmpiTroubleshootingLog = LoggerFactory.getLogger("ca.uhn.fhir.log.empi_troubleshooting"); - public static Logger getMdmTroubleshootingLog() { - return ourMdmTroubleshootingLog; + public static Logger getEmpiTroubleshootingLog() { + return ourEmpiTroubleshootingLog; } } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/model/CanonicalEID.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/model/CanonicalEID.java similarity index 87% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/model/CanonicalEID.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/model/CanonicalEID.java index 6f1f65efba5..a9d1052f0d4 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/model/CanonicalEID.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/model/CanonicalEID.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.model; +package ca.uhn.fhir.empi.model; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -28,7 +28,6 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Identifier; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; public class CanonicalEID { @@ -59,7 +58,7 @@ public class CanonicalEID { /** * Get the appropriate FHIRPath expression to extract the EID identifier value, regardless of resource type. - * e.g. if theBaseResource is a patient, and the MDM EID system is test-system, this will return + * e.g. if theBaseResource is a patient, and the EMPI EID system is test-system, this will return * * Patient.identifier.where(system='test-system').value * @@ -137,20 +136,4 @@ public class CanonicalEID { .map(ibase -> new CanonicalEID(fhirPath, ibase)) .collect(Collectors.toList()); } - - @Override - public boolean equals(Object o) { - if (!(o instanceof CanonicalEID)) { - return false; - } - CanonicalEID otherEid = (CanonicalEID)o; - return Objects.equals(otherEid.getSystem(), this.getSystem()) - && Objects.equals(otherEid.getValue(), this.getValue()) - && Objects.equals(otherEid.getUse(), this.getUse()); - } - - @Override - public int hashCode() { - return Objects.hash(mySystem, myValue, myUse); - } } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/model/CanonicalIdentityAssuranceLevel.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/model/CanonicalIdentityAssuranceLevel.java new file mode 100644 index 00000000000..a619ff14c0d --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/model/CanonicalIdentityAssuranceLevel.java @@ -0,0 +1,44 @@ +package ca.uhn.fhir.empi.model; + +/*- + * #%L + * HAPI FHIR - 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 org.hl7.fhir.r4.model.Person; + +public enum CanonicalIdentityAssuranceLevel { + LEVEL1("level1"), + LEVEL2("level2"), + LEVEL3("level3"), + LEVEL4("level4"); + + private String myCanonicalLevel; + private CanonicalIdentityAssuranceLevel(String theCanonicalLevel) { + myCanonicalLevel = theCanonicalLevel; + } + + public Person.IdentityAssuranceLevel toR4() { + return Person.IdentityAssuranceLevel.fromCode(myCanonicalLevel); + } + + public org.hl7.fhir.dstu3.model.Person.IdentityAssuranceLevel toDstu3() { + return org.hl7.fhir.dstu3.model.Person.IdentityAssuranceLevel.fromCode(myCanonicalLevel); + } + +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/model/MdmTransactionContext.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/model/EmpiTransactionContext.java similarity index 67% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/model/MdmTransactionContext.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/model/EmpiTransactionContext.java index c03ae77f52a..6e659028105 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/model/MdmTransactionContext.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/model/EmpiTransactionContext.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.model; +package ca.uhn.fhir.empi.model; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -22,45 +22,38 @@ package ca.uhn.fhir.mdm.model; import ca.uhn.fhir.rest.server.TransactionLogMessages; -public class MdmTransactionContext { - - public enum OperationType { - CREATE_RESOURCE, - UPDATE_RESOURCE, - SUBMIT_RESOURCE_TO_MDM, - QUERY_LINKS, - UPDATE_LINK, - DUPLICATE_GOLDEN_RESOURCES, - NOT_DUPLICATE, - MERGE_GOLDEN_RESOURCES - } +public class EmpiTransactionContext { /** - * Any MDM methods may add transaction log messages. + * Any EMPI methods may add transaction log messages. */ private TransactionLogMessages myTransactionLogMessages; private OperationType myRestOperation; - private String myResourceType; + public enum OperationType { + CREATE_RESOURCE, + UPDATE_RESOURCE, + SUBMIT_RESOURCE_TO_EMPI, + QUERY_LINKS, + UPDATE_LINK, + DUPLICATE_PERSONS, + NOT_DUPLICATE, + MERGE_PERSONS + } public TransactionLogMessages getTransactionLogMessages() { return myTransactionLogMessages; } - public MdmTransactionContext() { + public EmpiTransactionContext() { } - public MdmTransactionContext(TransactionLogMessages theTransactionLogMessages, OperationType theRestOperation) { + public EmpiTransactionContext(TransactionLogMessages theTransactionLogMessages, OperationType theRestOperation) { myTransactionLogMessages = theTransactionLogMessages; myRestOperation = theRestOperation; } - public MdmTransactionContext(TransactionLogMessages theTransactionLogMessages, OperationType theRestOperation, String theResourceType) { - this(theTransactionLogMessages, theRestOperation); - setResourceType(theResourceType); - } - public void addTransactionLogMessage(String theMessage) { if (myTransactionLogMessages == null) { return; @@ -79,13 +72,4 @@ public class MdmTransactionContext { public void setRestOperation(OperationType theRestOperation) { myRestOperation = theRestOperation; } - - public String getResourceType() { - return myResourceType; - } - - public void setResourceType(String myResourceType) { - this.myResourceType = myResourceType; - } - } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/BaseEmpiProvider.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/BaseEmpiProvider.java new file mode 100644 index 00000000000..89c6d1281e8 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/BaseEmpiProvider.java @@ -0,0 +1,110 @@ +package ca.uhn.fhir.empi.provider; + +/*- + * #%L + * HAPI FHIR - 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.EmpiLinkJson; +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import ca.uhn.fhir.empi.model.EmpiTransactionContext; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.TransactionLogMessages; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.provider.ProviderConstants; +import ca.uhn.fhir.util.ParametersUtil; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IPrimitiveType; + +import java.util.stream.Stream; + +public abstract class BaseEmpiProvider { + + protected final FhirContext myFhirContext; + + public BaseEmpiProvider(FhirContext theFhirContext) { + myFhirContext = theFhirContext; + } + + protected void validateMergeParameters(IPrimitiveType theFromPersonId, IPrimitiveType theToPersonId) { + validateNotNull(ProviderConstants.EMPI_MERGE_PERSONS_FROM_PERSON_ID, theFromPersonId); + validateNotNull(ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, theToPersonId); + if (theFromPersonId.getValue().equals(theToPersonId.getValue())) { + throw new InvalidRequestException("fromPersonId must be different from toPersonId"); + } + } + + private void validateNotNull(String theName, IPrimitiveType theString) { + if (theString == null || theString.getValue() == null) { + throw new InvalidRequestException(theName + " cannot be null"); + } + } + + protected void validateUpdateLinkParameters(IPrimitiveType thePersonId, IPrimitiveType theTargetId, IPrimitiveType theMatchResult) { + validateNotNull(ProviderConstants.EMPI_UPDATE_LINK_PERSON_ID, thePersonId); + validateNotNull(ProviderConstants.EMPI_UPDATE_LINK_TARGET_ID, theTargetId); + validateNotNull(ProviderConstants.EMPI_UPDATE_LINK_MATCH_RESULT, theMatchResult); + EmpiMatchResultEnum matchResult = EmpiMatchResultEnum.valueOf(theMatchResult.getValue()); + switch (matchResult) { + case NO_MATCH: + case MATCH: + break; + default: + throw new InvalidRequestException(ProviderConstants.EMPI_UPDATE_LINK + " illegal " + ProviderConstants.EMPI_UPDATE_LINK_MATCH_RESULT + + " value '" + matchResult + "'. Must be " + EmpiMatchResultEnum.NO_MATCH + " or " + EmpiMatchResultEnum.MATCH); + } + } + + protected void validateNotDuplicateParameters(IPrimitiveType thePersonId, IPrimitiveType theTargetId) { + validateNotNull(ProviderConstants.EMPI_UPDATE_LINK_PERSON_ID, thePersonId); + validateNotNull(ProviderConstants.EMPI_UPDATE_LINK_TARGET_ID, theTargetId); + } + + protected EmpiTransactionContext createEmpiContext(RequestDetails theRequestDetails, EmpiTransactionContext.OperationType theOperationType) { + TransactionLogMessages transactionLogMessages = TransactionLogMessages.createFromTransactionGuid(theRequestDetails.getTransactionGuid()); + return new EmpiTransactionContext(transactionLogMessages, theOperationType); + } + + protected String extractStringOrNull(IPrimitiveType theString) { + if (theString == null) { + return null; + } + return theString.getValue(); + } + + protected IBaseParameters parametersFromEmpiLinks(Stream theEmpiLinkStream, boolean includeResultAndSource) { + IBaseParameters retval = ParametersUtil.newInstance(myFhirContext); + + theEmpiLinkStream.forEach(empiLink -> { + IBase resultPart = ParametersUtil.addParameterToParameters(myFhirContext, retval, "link"); + ParametersUtil.addPartString(myFhirContext, resultPart, "personId", empiLink.getPersonId()); + ParametersUtil.addPartString(myFhirContext, resultPart, "targetId", empiLink.getTargetId()); + + if (includeResultAndSource) { + ParametersUtil.addPartString(myFhirContext, resultPart, "matchResult", empiLink.getMatchResult().name()); + ParametersUtil.addPartString(myFhirContext, resultPart, "linkSource", empiLink.getLinkSource().name()); + ParametersUtil.addPartBoolean(myFhirContext, resultPart, "eidMatch", empiLink.getEidMatch()); + ParametersUtil.addPartBoolean(myFhirContext, resultPart, "newPerson", empiLink.getNewPerson()); + ParametersUtil.addPartDecimal(myFhirContext, resultPart, "score", empiLink.getScore()); + } + }); + return retval; + } +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmControllerHelper.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiControllerHelper.java similarity index 55% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmControllerHelper.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiControllerHelper.java index 12f1feda2a4..0cb9bd35955 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmControllerHelper.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiControllerHelper.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.provider; +package ca.uhn.fhir.empi.provider; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -21,9 +21,8 @@ package ca.uhn.fhir.mdm.provider; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.IMdmSettings; -import ca.uhn.fhir.mdm.util.MdmResourceUtil; -import ca.uhn.fhir.mdm.util.MessageHelper; +import ca.uhn.fhir.empi.api.EmpiConstants; +import ca.uhn.fhir.empi.util.EmpiUtil; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; @@ -36,18 +35,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service -public class MdmControllerHelper { +public class EmpiControllerHelper { private final FhirContext myFhirContext; private final IResourceLoader myResourceLoader; - private final IMdmSettings myMdmSettings; - private final MessageHelper myMessageHelper; @Autowired - public MdmControllerHelper(FhirContext theFhirContext, IResourceLoader theResourceLoader, IMdmSettings theMdmSettings, MessageHelper theMessageHelper) { + public EmpiControllerHelper(FhirContext theFhirContext, IResourceLoader theResourceLoader) { myFhirContext = theFhirContext; myResourceLoader = theResourceLoader; - myMdmSettings = theMdmSettings; - myMessageHelper = theMessageHelper; } public void validateSameVersion(IAnyResource theResource, String theResourceId) { @@ -61,15 +56,15 @@ public class MdmControllerHelper { return new IdDt(theId).hasVersionIdPart(); } - public IAnyResource getLatestGoldenResourceFromIdOrThrowException(String theParamName, String theGoldenResourceId) { - IdDt resourceId = MdmControllerUtil.getGoldenIdDtOrThrowException(theParamName, theGoldenResourceId); - return loadResource(resourceId.toUnqualifiedVersionless()); + public IAnyResource getLatestPersonFromIdOrThrowException(String theParamName, String theId) { + IdDt personId = EmpiControllerUtil.getPersonIdDtOrThrowException(theParamName, theId); + return loadResource(personId.toUnqualifiedVersionless()); } - public IAnyResource getLatestSourceFromIdOrThrowException(String theParamName, String theSourceId) { - IIdType sourceId = MdmControllerUtil.getSourceIdDtOrThrowException(theParamName, theSourceId); - return loadResource(sourceId.toUnqualifiedVersionless()); + public IAnyResource getLatestTargetFromIdOrThrowException(String theParamName, String theId) { + IIdType targetId = EmpiControllerUtil.getTargetIdDtOrThrowException(theParamName, theId); + return loadResource(targetId.toUnqualifiedVersionless()); } protected IAnyResource loadResource(IIdType theResourceId) { @@ -77,25 +72,21 @@ public class MdmControllerHelper { return (IAnyResource) myResourceLoader.load(resourceClass, theResourceId); } - public void validateMergeResources(IAnyResource theFromGoldenResource, IAnyResource theToGoldenResource) { - validateIsMdmManaged(ProviderConstants.MDM_MERGE_GR_FROM_GOLDEN_RESOURCE_ID, theFromGoldenResource); - validateIsMdmManaged(ProviderConstants.MDM_MERGE_GR_TO_GOLDEN_RESOURCE_ID, theToGoldenResource); + public void validateMergeResources(IAnyResource theFromPerson, IAnyResource theToPerson) { + validateIsEmpiManaged(ProviderConstants.EMPI_MERGE_PERSONS_FROM_PERSON_ID, theFromPerson); + validateIsEmpiManaged(ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, theToPerson); } public String toJson(IAnyResource theAnyResource) { return myFhirContext.newJsonParser().encodeResourceToString(theAnyResource); } - public void validateIsMdmManaged(String theName, IAnyResource theResource) { - String resourceType = myFhirContext.getResourceType(theResource); - if (!myMdmSettings.isSupportedMdmType(resourceType)) { - throw new InvalidRequestException( - myMessageHelper.getMessageForUnsupportedResource(theName, resourceType) - ); + private void validateIsEmpiManaged(String theName, IAnyResource thePerson) { + if (!"Person".equals(myFhirContext.getResourceType(thePerson))) { + throw new InvalidRequestException("Only Person resources can be merged. The " + theName + " points to a " + myFhirContext.getResourceType(thePerson)); } - - if (!MdmResourceUtil.isMdmManaged(theResource)) { - throw new InvalidRequestException(myMessageHelper.getMessageForUnmanagedResource()); + if (!EmpiUtil.isEmpiManaged(thePerson)) { + throw new InvalidRequestException("Only EMPI managed resources can be merged. Empi managed resource have the " + EmpiConstants.CODE_HAPI_EMPI_MANAGED + " tag."); } } } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiControllerUtil.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiControllerUtil.java new file mode 100644 index 00000000000..f1197931309 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiControllerUtil.java @@ -0,0 +1,77 @@ +package ca.uhn.fhir.empi.provider; + +/*- + * #%L + * HAPI FHIR - 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.util.EmpiUtil; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import org.hl7.fhir.instance.model.api.IIdType; + +public class EmpiControllerUtil { + public static EmpiMatchResultEnum extractMatchResultOrNull(String theMatchResult) { + if (theMatchResult == null) { + return null; + } + return EmpiMatchResultEnum.valueOf(theMatchResult); + } + + public static EmpiLinkSourceEnum extractLinkSourceOrNull(String theLinkSource) { + if (theLinkSource == null) { + return null; + } + return EmpiLinkSourceEnum.valueOf(theLinkSource); + } + + public static IIdType extractPersonIdDtOrNull(String theName, String thePersonId) { + if (thePersonId == null) { + return null; + } + return getPersonIdDtOrThrowException(theName, thePersonId); + } + + public static IIdType extractTargetIdDtOrNull(String theName, String theTargetId) { + if (theTargetId == null) { + return null; + } + return getTargetIdDtOrThrowException(theName, theTargetId); + } + + static IdDt getPersonIdDtOrThrowException(String theParamName, String theId) { + IdDt personId = new IdDt(theId); + if (!"Person".equals(personId.getResourceType()) || + personId.getIdPart() == null) { + throw new InvalidRequestException(theParamName + " is '" + theId + "'. must have form Person/ where is the id of the person"); + } + return personId; + } + + public static IIdType getTargetIdDtOrThrowException(String theParamName, String theId) { + IdDt targetId = new IdDt(theId); + String resourceType = targetId.getResourceType(); + if (!EmpiUtil.supportedTargetType(resourceType) || + targetId.getIdPart() == null) { + throw new InvalidRequestException(theParamName + " is '" + theId + "'. must have form Patient/ or Practitioner/ where is the id of the resource"); + } + return targetId; + } +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiProviderDstu3.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiProviderDstu3.java new file mode 100644 index 00000000000..5f85300d643 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiProviderDstu3.java @@ -0,0 +1,262 @@ +package ca.uhn.fhir.empi.provider; + +/*- + * #%L + * HAPI FHIR - 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.EmpiLinkJson; +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.api.MatchedTarget; +import ca.uhn.fhir.empi.model.EmpiTransactionContext; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.server.RequestDetails; +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 ca.uhn.fhir.util.ParametersUtil; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.CodeType; +import org.hl7.fhir.dstu3.model.DecimalType; +import org.hl7.fhir.dstu3.model.InstantType; +import org.hl7.fhir.dstu3.model.Parameters; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.Person; +import org.hl7.fhir.dstu3.model.Practitioner; +import org.hl7.fhir.dstu3.model.Resource; +import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.dstu3.model.codesystems.MatchGrade; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IIdType; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +public class EmpiProviderDstu3 extends BaseEmpiProvider { + private final IEmpiControllerSvc myEmpiControllerSvc; + private final IEmpiMatchFinderSvc myEmpiMatchFinderSvc; + private final IEmpiExpungeSvc myEmpiResetSvc; + private final IEmpiSubmitSvc myEmpiBatchSvc; + + /** + * Constructor + * + * Note that this is not a spring bean. Any necessary injections should + * happen in the constructor + */ + public EmpiProviderDstu3(FhirContext theFhirContext, IEmpiControllerSvc theEmpiControllerSvc, IEmpiMatchFinderSvc theEmpiMatchFinderSvc, IEmpiExpungeSvc theEmpiResetSvc, IEmpiSubmitSvc theEmpiBatchSvc) { + super(theFhirContext); + myEmpiControllerSvc = theEmpiControllerSvc; + myEmpiMatchFinderSvc = theEmpiMatchFinderSvc; + myEmpiResetSvc = theEmpiResetSvc; + myEmpiBatchSvc = theEmpiBatchSvc; + } + + @Operation(name = ProviderConstants.EMPI_MATCH, type = Patient.class) + public Bundle match(@OperationParam(name = ProviderConstants.EMPI_MATCH_RESOURCE, min = 1, max = 1) Patient thePatient) { + if (thePatient == null) { + throw new InvalidRequestException("resource may not be null"); + } + + List matches = myEmpiMatchFinderSvc.getMatchedTargets("Patient", thePatient); + matches.sort(Comparator.comparing((MatchedTarget m) -> m.getMatchResult().getNormalizedScore()).reversed()); + + Bundle retVal = new Bundle(); + retVal.setType(Bundle.BundleType.SEARCHSET); + retVal.setId(UUID.randomUUID().toString()); + retVal.getMeta().setLastUpdatedElement(InstantType.now()); + + for (MatchedTarget next : matches) { + boolean shouldKeepThisEntry = next.isMatch() || next.isPossibleMatch(); + if (!shouldKeepThisEntry) { + continue; + } + + Bundle.BundleEntryComponent entry = new Bundle.BundleEntryComponent(); + entry.setResource((Resource) next.getTarget()); + entry.setSearch(toBundleEntrySearchComponent(next)); + + retVal.addEntry(entry); + } + + return retVal; + } + + private Bundle.BundleEntrySearchComponent toBundleEntrySearchComponent(MatchedTarget theMatchedTarget) { + Bundle.BundleEntrySearchComponent searchComponent = new Bundle.BundleEntrySearchComponent(); + searchComponent.setMode(Bundle.SearchEntryMode.MATCH); + searchComponent.setScore(theMatchedTarget.getMatchResult().getNormalizedScore()); + + MatchGrade matchGrade = MatchGrade.PROBABLE; + if (theMatchedTarget.isMatch()) { + matchGrade = MatchGrade.CERTAIN; + } else if (theMatchedTarget.isPossibleMatch()) { + matchGrade = MatchGrade.POSSIBLE; + } + + searchComponent.addExtension(EmpiConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE, new CodeType(matchGrade.toCode())); + return searchComponent; + } + + @Operation(name = ProviderConstants.EMPI_MERGE_PERSONS, type = Person.class) + public Person mergePerson(@OperationParam(name=ProviderConstants.EMPI_MERGE_PERSONS_FROM_PERSON_ID, min = 1, max = 1) StringType theFromPersonId, + @OperationParam(name=ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, min = 1, max = 1) StringType theToPersonId, + RequestDetails theRequestDetails) { + validateMergeParameters(theFromPersonId, theToPersonId); + + return (Person) myEmpiControllerSvc.mergePersons(theFromPersonId.getValue(), theToPersonId.getValue(), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.MERGE_PERSONS)); + } + + @Operation(name = ProviderConstants.EMPI_UPDATE_LINK, type = Person.class) + public Person updateLink(@OperationParam(name=ProviderConstants.EMPI_UPDATE_LINK_PERSON_ID, min = 1, max = 1) StringType thePersonId, + @OperationParam(name=ProviderConstants.EMPI_UPDATE_LINK_TARGET_ID, min = 1, max = 1) StringType theTargetId, + @OperationParam(name=ProviderConstants.EMPI_UPDATE_LINK_MATCH_RESULT, min = 1, max = 1) StringType theMatchResult, + ServletRequestDetails theRequestDetails) { + + validateUpdateLinkParameters(thePersonId, theTargetId, theMatchResult); + + return (Person) myEmpiControllerSvc.updateLink(thePersonId.getValue(), theTargetId.getValue(), theMatchResult.getValue(), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.UPDATE_LINK)); + } + + @Operation(name = ProviderConstants.EMPI_QUERY_LINKS) + public Parameters queryLinks(@OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_PERSON_ID, min = 0, max = 1) StringType thePersonId, + @OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_TARGET_ID, min = 0, max = 1) StringType theTargetId, + @OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_MATCH_RESULT, min = 0, max = 1) StringType theMatchResult, + @OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_MATCH_RESULT, min = 0, max = 1) StringType theLinkSource, + ServletRequestDetails theRequestDetails) { + + Stream empiLinkJson = myEmpiControllerSvc.queryLinks(extractStringOrNull(thePersonId), extractStringOrNull(theTargetId), extractStringOrNull(theMatchResult), extractStringOrNull(theLinkSource), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.QUERY_LINKS)); + return (Parameters) parametersFromEmpiLinks(empiLinkJson, true); + } + + @Operation(name = ProviderConstants.EMPI_DUPLICATE_PERSONS) + public Parameters getDuplicatePersons(ServletRequestDetails theRequestDetails) { + Stream possibleDuplicates = myEmpiControllerSvc.getDuplicatePersons(createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.QUERY_LINKS)); + return (Parameters) parametersFromEmpiLinks(possibleDuplicates, false); + } + + @Operation(name = ProviderConstants.EMPI_NOT_DUPLICATE) + // TODO KHS can this return void? + public Parameters notDuplicate(@OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_PERSON_ID, min = 1, max = 1) StringType thePersonId, + @OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_TARGET_ID, min = 1, max = 1) StringType theTargetId, + ServletRequestDetails theRequestDetails) { + + validateNotDuplicateParameters(thePersonId, theTargetId); + myEmpiControllerSvc.notDuplicatePerson(thePersonId.getValue(), theTargetId.getValue(), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.NOT_DUPLICATE)); + + Parameters retval = (Parameters) ParametersUtil.newInstance(myFhirContext); + ParametersUtil.addParameterToParametersBoolean(myFhirContext, retval, "success", true); + return retval; + } + + @Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, returnParameters = { + @OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type= DecimalType.class) + }) + public Parameters empiBatchOnAllTargets( + @OperationParam(name= ProviderConstants.EMPI_BATCH_RUN_CRITERIA,min = 0 , max = 1) StringType theCriteria, + ServletRequestDetails theRequestDetails) { + String criteria = convertCriteriaToString(theCriteria); + long submittedCount = myEmpiBatchSvc.submitAllTargetTypesToEmpi(criteria); + return buildEmpiOutParametersWithCount(submittedCount); + } + + private String convertCriteriaToString(StringType theCriteria) { + return theCriteria == null ? null : theCriteria.getValueAsString(); + } + + @Operation(name = ProviderConstants.EMPI_CLEAR, returnParameters = { + @OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type= DecimalType.class) + }) + public Parameters clearEmpiLinks(@OperationParam(name=ProviderConstants.EMPI_CLEAR_TARGET_TYPE, min = 0, max = 1) StringType theTargetType, + ServletRequestDetails theRequestDetails) { + long resetCount; + if (theTargetType == null || StringUtils.isBlank(theTargetType.getValue())) { + resetCount = myEmpiResetSvc.expungeAllEmpiLinks(theRequestDetails); + } else { + resetCount = myEmpiResetSvc.expungeAllEmpiLinksOfTargetType(theTargetType.getValueNotNull(), theRequestDetails); + } + Parameters parameters = new Parameters(); + parameters.addParameter().setName(ProviderConstants.OPERATION_EMPI_CLEAR_OUT_PARAM_DELETED_COUNT) + .setValue(new DecimalType(resetCount)); + return parameters; + } + + @Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Patient.class, returnParameters = { + @OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = DecimalType.class) + }) + public Parameters empiBatchPatientInstance( + @IdParam IIdType theIdParam, + RequestDetails theRequest) { + long submittedCount = myEmpiBatchSvc.submitTargetToEmpi(theIdParam); + return buildEmpiOutParametersWithCount(submittedCount); + } + + @Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Patient.class, returnParameters = { + @OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = DecimalType.class) + }) + public Parameters empiBatchPatientType( + @OperationParam(name = ProviderConstants.EMPI_BATCH_RUN_CRITERIA) StringType theCriteria, + RequestDetails theRequest) { + String criteria = convertCriteriaToString(theCriteria); + long submittedCount = myEmpiBatchSvc.submitPatientTypeToEmpi(criteria); + return buildEmpiOutParametersWithCount(submittedCount); + } + + @Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Practitioner.class, returnParameters = { + @OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = DecimalType.class) + }) + public Parameters empiBatchPractitionerInstance( + @IdParam IIdType theIdParam, + RequestDetails theRequest) { + long submittedCount = myEmpiBatchSvc.submitTargetToEmpi(theIdParam); + return buildEmpiOutParametersWithCount(submittedCount); + } + + @Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Practitioner.class, returnParameters = { + @OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = DecimalType.class) + }) + public Parameters empiBatchPractitionerType( + @OperationParam(name = ProviderConstants.EMPI_BATCH_RUN_CRITERIA) StringType theCriteria, + RequestDetails theRequest) { + String criteria = convertCriteriaToString(theCriteria); + long submittedCount = myEmpiBatchSvc.submitPractitionerTypeToEmpi(criteria); + return buildEmpiOutParametersWithCount(submittedCount); + } + + /** + * Helper function to build the out-parameters for all batch EMPI operations. + */ + private Parameters buildEmpiOutParametersWithCount(long theCount) { + Parameters parameters = new Parameters(); + parameters.addParameter() + .setName(ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT) + .setValue(new DecimalType(theCount)); + return parameters; + } +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderLoader.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiProviderLoader.java similarity index 57% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderLoader.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiProviderLoader.java index 1868154f92a..04908eb6247 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderLoader.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiProviderLoader.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.provider; +package ca.uhn.fhir.empi.provider; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -22,39 +22,39 @@ package ca.uhn.fhir.mdm.provider; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.IMdmControllerSvc; -import ca.uhn.fhir.mdm.api.IMdmExpungeSvc; -import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc; -import ca.uhn.fhir.mdm.api.IMdmSubmitSvc; +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.rest.server.provider.ResourceProviderFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service -public class MdmProviderLoader { +public class EmpiProviderLoader { @Autowired private FhirContext myFhirContext; @Autowired private ResourceProviderFactory myResourceProviderFactory; @Autowired - private IMdmMatchFinderSvc myMdmMatchFinderSvc; + private IEmpiMatchFinderSvc myEmpiMatchFinderSvc; @Autowired - private IMdmControllerSvc myMdmControllerSvc; + private IEmpiControllerSvc myEmpiControllerSvc; @Autowired - private IMdmExpungeSvc myMdmExpungeSvc; + private IEmpiExpungeSvc myEmpiResetSvc; @Autowired - private IMdmSubmitSvc myMdmSubmitSvc; + private IEmpiSubmitSvc myEmpiBatchSvc; public void loadProvider() { switch (myFhirContext.getVersion().getVersion()) { case DSTU3: - myResourceProviderFactory.addSupplier(() -> new MdmProviderDstu3(myFhirContext, myMdmControllerSvc, myMdmMatchFinderSvc, myMdmExpungeSvc, myMdmSubmitSvc)); + myResourceProviderFactory.addSupplier(() -> new EmpiProviderDstu3(myFhirContext, myEmpiControllerSvc, myEmpiMatchFinderSvc, myEmpiResetSvc, myEmpiBatchSvc)); break; case R4: - myResourceProviderFactory.addSupplier(() -> new MdmProviderR4(myFhirContext, myMdmControllerSvc, myMdmMatchFinderSvc, myMdmExpungeSvc, myMdmSubmitSvc)); + myResourceProviderFactory.addSupplier(() -> new EmpiProviderR4(myFhirContext, myEmpiControllerSvc, myEmpiMatchFinderSvc, myEmpiResetSvc, myEmpiBatchSvc)); 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()); } } } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiProviderR4.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiProviderR4.java new file mode 100644 index 00000000000..d0fc3c2a277 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/provider/EmpiProviderR4.java @@ -0,0 +1,263 @@ +package ca.uhn.fhir.empi.provider; + +/*- + * #%L + * HAPI FHIR - 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.EmpiLinkJson; +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.api.MatchedTarget; +import ca.uhn.fhir.empi.model.EmpiTransactionContext; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.server.RequestDetails; +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 ca.uhn.fhir.util.ParametersUtil; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.InstantType; +import org.hl7.fhir.r4.model.IntegerType; +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.Resource; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.codesystems.MatchGrade; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +public class EmpiProviderR4 extends BaseEmpiProvider { + private final IEmpiControllerSvc myEmpiControllerSvc; + private final IEmpiMatchFinderSvc myEmpiMatchFinderSvc; + private final IEmpiExpungeSvc myEmpiExpungeSvc; + private final IEmpiSubmitSvc myEmpiSubmitSvc; + + /** + * Constructor + * + * Note that this is not a spring bean. Any necessary injections should + * happen in the constructor + */ + public EmpiProviderR4(FhirContext theFhirContext, IEmpiControllerSvc theEmpiControllerSvc, IEmpiMatchFinderSvc theEmpiMatchFinderSvc, IEmpiExpungeSvc theEmpiExpungeSvc, IEmpiSubmitSvc theEmpiSubmitSvc) { + super(theFhirContext); + myEmpiControllerSvc = theEmpiControllerSvc; + myEmpiMatchFinderSvc = theEmpiMatchFinderSvc; + myEmpiExpungeSvc = theEmpiExpungeSvc; + myEmpiSubmitSvc = theEmpiSubmitSvc; + } + + @Operation(name = ProviderConstants.EMPI_MATCH, type = Patient.class) + public Bundle match(@OperationParam(name = ProviderConstants.EMPI_MATCH_RESOURCE, min = 1, max = 1) Patient thePatient) { + if (thePatient == null) { + throw new InvalidRequestException("resource may not be null"); + } + + List matches = myEmpiMatchFinderSvc.getMatchedTargets("Patient", thePatient); + matches.sort(Comparator.comparing((MatchedTarget m) -> m.getMatchResult().getNormalizedScore()).reversed()); + + Bundle retVal = new Bundle(); + retVal.setType(Bundle.BundleType.SEARCHSET); + retVal.setId(UUID.randomUUID().toString()); + retVal.getMeta().setLastUpdatedElement(InstantType.now()); + + for (MatchedTarget next : matches) { + boolean shouldKeepThisEntry = next.isMatch() || next.isPossibleMatch(); + if (!shouldKeepThisEntry) { + continue; + } + + Bundle.BundleEntryComponent entry = new Bundle.BundleEntryComponent(); + entry.setResource((Resource) next.getTarget()); + entry.setSearch(toBundleEntrySearchComponent(next)); + + retVal.addEntry(entry); + } + + return retVal; + } + + private Bundle.BundleEntrySearchComponent toBundleEntrySearchComponent(MatchedTarget theMatchedTarget) { + Bundle.BundleEntrySearchComponent searchComponent = new Bundle.BundleEntrySearchComponent(); + searchComponent.setMode(Bundle.SearchEntryMode.MATCH); + searchComponent.setScore(theMatchedTarget.getMatchResult().getNormalizedScore()); + + MatchGrade matchGrade = MatchGrade.PROBABLE; + if (theMatchedTarget.isMatch()) { + matchGrade = MatchGrade.CERTAIN; + } else if (theMatchedTarget.isPossibleMatch()) { + matchGrade = MatchGrade.POSSIBLE; + } + + searchComponent.addExtension(EmpiConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE, new CodeType(matchGrade.toCode())); + return searchComponent; + } + + @Operation(name = ProviderConstants.EMPI_MERGE_PERSONS, type = Person.class) + public Person mergePersons(@OperationParam(name=ProviderConstants.EMPI_MERGE_PERSONS_FROM_PERSON_ID, min = 1, max = 1) StringType theFromPersonId, + @OperationParam(name=ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, min = 1, max = 1) StringType theToPersonId, + RequestDetails theRequestDetails) { + validateMergeParameters(theFromPersonId, theToPersonId); + + return (Person) myEmpiControllerSvc.mergePersons(theFromPersonId.getValue(), theToPersonId.getValue(), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.MERGE_PERSONS)); + } + + @Operation(name = ProviderConstants.EMPI_UPDATE_LINK, type = Person.class) + public Person updateLink(@OperationParam(name=ProviderConstants.EMPI_UPDATE_LINK_PERSON_ID, min = 1, max = 1) StringType thePersonId, + @OperationParam(name=ProviderConstants.EMPI_UPDATE_LINK_TARGET_ID, min = 1, max = 1) StringType theTargetId, + @OperationParam(name=ProviderConstants.EMPI_UPDATE_LINK_MATCH_RESULT, min = 1, max = 1) StringType theMatchResult, + ServletRequestDetails theRequestDetails) { + + validateUpdateLinkParameters(thePersonId, theTargetId, theMatchResult); + + return (Person) myEmpiControllerSvc.updateLink(thePersonId.getValueNotNull(), theTargetId.getValue(), theMatchResult.getValue(), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.UPDATE_LINK)); + } + + @Operation(name = ProviderConstants.EMPI_CLEAR, returnParameters = { + @OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type=DecimalType.class) + }) + public Parameters clearEmpiLinks(@OperationParam(name=ProviderConstants.EMPI_CLEAR_TARGET_TYPE, min = 0, max = 1) StringType theTargetType, + ServletRequestDetails theRequestDetails) { + long resetCount; + if (theTargetType == null || StringUtils.isBlank(theTargetType.getValue())) { + resetCount = myEmpiExpungeSvc.expungeAllEmpiLinks(theRequestDetails); + } else { + resetCount = myEmpiExpungeSvc.expungeAllEmpiLinksOfTargetType(theTargetType.getValueNotNull(), theRequestDetails); + } + Parameters parameters = new Parameters(); + parameters.addParameter().setName(ProviderConstants.OPERATION_EMPI_CLEAR_OUT_PARAM_DELETED_COUNT) + .setValue(new DecimalType(resetCount)); + return parameters; + } + + @Operation(name = ProviderConstants.EMPI_QUERY_LINKS, idempotent = true) + public Parameters queryLinks(@OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_PERSON_ID, min = 0, max = 1) StringType thePersonId, + @OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_TARGET_ID, min = 0, max = 1) StringType theTargetId, + @OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_MATCH_RESULT, min = 0, max = 1) StringType theMatchResult, + @OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_LINK_SOURCE, min = 0, max = 1) StringType theLinkSource, + ServletRequestDetails theRequestDetails) { + + Stream empiLinkJson = myEmpiControllerSvc.queryLinks(extractStringOrNull(thePersonId), extractStringOrNull(theTargetId), extractStringOrNull(theMatchResult), extractStringOrNull(theLinkSource), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.QUERY_LINKS)); + return (Parameters) parametersFromEmpiLinks(empiLinkJson, true); + } + + @Operation(name = ProviderConstants.EMPI_DUPLICATE_PERSONS, idempotent = true) + public Parameters getDuplicatePersons(ServletRequestDetails theRequestDetails) { + Stream possibleDuplicates = myEmpiControllerSvc.getDuplicatePersons(createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.DUPLICATE_PERSONS)); + return (Parameters) parametersFromEmpiLinks(possibleDuplicates, false); + } + + @Operation(name = ProviderConstants.EMPI_NOT_DUPLICATE) + public Parameters notDuplicate(@OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_PERSON_ID, min = 1, max = 1) StringType thePersonId, + @OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_TARGET_ID, min = 1, max = 1) StringType theTargetId, + ServletRequestDetails theRequestDetails) { + + validateNotDuplicateParameters(thePersonId, theTargetId); + myEmpiControllerSvc.notDuplicatePerson(thePersonId.getValue(), theTargetId.getValue(), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.NOT_DUPLICATE)); + + Parameters retval = (Parameters) ParametersUtil.newInstance(myFhirContext); + ParametersUtil.addParameterToParametersBoolean(myFhirContext, retval, "success", true); + return retval; + } + + @Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, returnParameters = { + @OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type= IntegerType.class) + }) + public Parameters empiBatchOnAllTargets( + @OperationParam(name= ProviderConstants.EMPI_BATCH_RUN_CRITERIA,min = 0 , max = 1) StringType theCriteria, + ServletRequestDetails theRequestDetails) { + String criteria = convertCriteriaToString(theCriteria); + long submittedCount = myEmpiSubmitSvc.submitAllTargetTypesToEmpi(criteria); + return buildEmpiOutParametersWithCount(submittedCount); + } + + private String convertCriteriaToString(StringType theCriteria) { + return theCriteria == null ? null : theCriteria.getValueAsString(); + } + + @Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Patient.class, returnParameters = { + @OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class) + }) + public Parameters empiBatchPatientInstance( + @IdParam IIdType theIdParam, + RequestDetails theRequest) { + long submittedCount = myEmpiSubmitSvc.submitTargetToEmpi(theIdParam); + return buildEmpiOutParametersWithCount(submittedCount); + } + + @Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Patient.class, returnParameters = { + @OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class) + }) + public Parameters empiBatchPatientType( + @OperationParam(name = ProviderConstants.EMPI_BATCH_RUN_CRITERIA) StringType theCriteria, + RequestDetails theRequest) { + String criteria = convertCriteriaToString(theCriteria); + long submittedCount = myEmpiSubmitSvc.submitPatientTypeToEmpi(criteria); + return buildEmpiOutParametersWithCount(submittedCount); + } + + @Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Practitioner.class, returnParameters = { + @OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class) + }) + public Parameters empiBatchPractitionerInstance( + @IdParam IIdType theIdParam, + RequestDetails theRequest) { + long submittedCount = myEmpiSubmitSvc.submitTargetToEmpi(theIdParam); + return buildEmpiOutParametersWithCount(submittedCount); + } + + @Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Practitioner.class, returnParameters = { + @OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class) + }) + public Parameters empiBatchPractitionerType( + @OperationParam(name = ProviderConstants.EMPI_BATCH_RUN_CRITERIA) StringType theCriteria, + RequestDetails theRequest) { + String criteria = convertCriteriaToString(theCriteria); + long submittedCount = myEmpiSubmitSvc.submitPractitionerTypeToEmpi(criteria); + return buildEmpiOutParametersWithCount(submittedCount); + } + + /** + * Helper function to build the out-parameters for all batch EMPI operations. + */ + private Parameters buildEmpiOutParametersWithCount(long theCount) { + Parameters parameters = new Parameters(); + parameters.addParameter() + .setName(ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT) + .setValue(new DecimalType(theCount)); + return parameters; + } +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/config/MdmRuleValidator.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/config/EmpiRuleValidator.java similarity index 50% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/config/MdmRuleValidator.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/config/EmpiRuleValidator.java index e6e69cf7903..91a00fd6eec 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/config/MdmRuleValidator.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/config/EmpiRuleValidator.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.config; +package ca.uhn.fhir.empi.rules.config; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -22,14 +22,13 @@ package ca.uhn.fhir.mdm.rules.config; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.mdm.api.MdmConstants; -import ca.uhn.fhir.mdm.api.IMdmRuleValidator; -import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson; -import ca.uhn.fhir.mdm.rules.json.MdmFilterSearchParamJson; -import ca.uhn.fhir.mdm.rules.json.MdmResourceSearchParamJson; -import ca.uhn.fhir.mdm.rules.json.MdmRulesJson; -import ca.uhn.fhir.mdm.rules.json.MdmSimilarityJson; +import ca.uhn.fhir.empi.api.EmpiConstants; +import ca.uhn.fhir.empi.api.IEmpiRuleValidator; +import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson; +import ca.uhn.fhir.empi.rules.json.EmpiFilterSearchParamJson; +import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson; +import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; +import ca.uhn.fhir.empi.rules.json.EmpiSimilarityJson; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.server.util.ISearchParamRetriever; import ca.uhn.fhir.util.FhirTerser; @@ -42,12 +41,11 @@ import org.springframework.stereotype.Service; import java.net.URI; import java.net.URISyntaxException; import java.util.HashSet; -import java.util.List; import java.util.Set; @Service -public class MdmRuleValidator implements IMdmRuleValidator { - private static final Logger ourLog = LoggerFactory.getLogger(MdmRuleValidator.class); +public class EmpiRuleValidator implements IEmpiRuleValidator { + private static final Logger ourLog = LoggerFactory.getLogger(EmpiRuleValidator.class); private final FhirContext myFhirContext; private final ISearchParamRetriever mySearchParamRetriever; @@ -56,7 +54,7 @@ public class MdmRuleValidator implements IMdmRuleValidator { private final FhirTerser myTerser; @Autowired - public MdmRuleValidator(FhirContext theFhirContext, ISearchParamRetriever theSearchParamRetriever) { + public EmpiRuleValidator(FhirContext theFhirContext, ISearchParamRetriever theSearchParamRetriever) { myFhirContext = theFhirContext; myPatientClass = theFhirContext.getResourceDefinition("Patient").getImplementingClass(); myPractitionerClass = theFhirContext.getResourceDefinition("Practitioner").getImplementingClass(); @@ -64,44 +62,24 @@ public class MdmRuleValidator implements IMdmRuleValidator { mySearchParamRetriever = theSearchParamRetriever; } - public void validate(MdmRulesJson theMdmRules) { - validateMdmTypes(theMdmRules); - validateSearchParams(theMdmRules); - validateMatchFields(theMdmRules); - validateSystemIsUri(theMdmRules); + public void validate(EmpiRulesJson theEmpiRulesJson) { + validateSearchParams(theEmpiRulesJson); + validateMatchFields(theEmpiRulesJson); + validateSystemIsUri(theEmpiRulesJson); } - public void validateMdmTypes(MdmRulesJson theMdmRulesJson) { - ourLog.info("Validating MDM types {}", theMdmRulesJson.getMdmTypes()); - - if (theMdmRulesJson.getMdmTypes() == null) { - throw new ConfigurationException("mdmTypes must be set to a list of resource types."); - } - for (String resourceType: theMdmRulesJson.getMdmTypes()) { - validateTypeHasIdentifier(resourceType); - } - } - - public void validateTypeHasIdentifier(String theResourceType) { - if (mySearchParamRetriever.getActiveSearchParam(theResourceType, "identifier") == null) { - throw new ConfigurationException("Resource Type " + theResourceType + " is not supported, as it does not have an 'identifier' field, which is necessary for MDM workflow."); - } - } - - private void validateSearchParams(MdmRulesJson theMdmRulesJson) { - ourLog.info("Validating search parameters {}", theMdmRulesJson.getCandidateSearchParams()); - - for (MdmResourceSearchParamJson searchParams : theMdmRulesJson.getCandidateSearchParams()) { + private void validateSearchParams(EmpiRulesJson theEmpiRulesJson) { + for (EmpiResourceSearchParamJson searchParams : theEmpiRulesJson.getCandidateSearchParams()) { searchParams.iterator().forEachRemaining( searchParam -> validateSearchParam("candidateSearchParams", searchParams.getResourceType(), searchParam)); } - for (MdmFilterSearchParamJson filter : theMdmRulesJson.getCandidateFilterSearchParams()) { + for (EmpiFilterSearchParamJson filter : theEmpiRulesJson.getCandidateFilterSearchParams()) { validateSearchParam("candidateFilterSearchParams", filter.getResourceType(), filter.getSearchParam()); } } private void validateSearchParam(String theFieldName, String theTheResourceType, String theTheSearchParam) { - if (MdmConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE.equals(theTheResourceType)) { + if (EmpiConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE.equals(theTheResourceType)) { validateResourceSearchParam(theFieldName, "Patient", theTheSearchParam); validateResourceSearchParam(theFieldName, "Practitioner", theTheSearchParam); } else { @@ -115,11 +93,9 @@ public class MdmRuleValidator implements IMdmRuleValidator { } } - private void validateMatchFields(MdmRulesJson theMdmRulesJson) { - ourLog.info("Validating match fields {}", theMdmRulesJson.getMatchFields()); - + private void validateMatchFields(EmpiRulesJson theEmpiRulesJson) { Set names = new HashSet<>(); - for (MdmFieldMatchJson fieldMatch : theMdmRulesJson.getMatchFields()) { + for (EmpiFieldMatchJson fieldMatch : theEmpiRulesJson.getMatchFields()) { if (names.contains(fieldMatch.getName())) { throw new ConfigurationException("Two MatchFields have the same name '" + fieldMatch.getName() + "'"); } @@ -129,43 +105,35 @@ public class MdmRuleValidator implements IMdmRuleValidator { } else if (fieldMatch.getMatcher() == null) { throw new ConfigurationException("MatchField " + fieldMatch.getName() + " has neither a similarity nor a matcher. At least one must be present."); } - validatePath(theMdmRulesJson.getMdmTypes(), fieldMatch); + validatePath(fieldMatch); } } - private void validateSimilarity(MdmFieldMatchJson theFieldMatch) { - MdmSimilarityJson similarity = theFieldMatch.getSimilarity(); + private void validateSimilarity(EmpiFieldMatchJson theFieldMatch) { + EmpiSimilarityJson similarity = theFieldMatch.getSimilarity(); if (similarity.getMatchThreshold() == null) { throw new ConfigurationException("MatchField " + theFieldMatch.getName() + " similarity " + similarity.getAlgorithm() + " requires a matchThreshold"); } } - private void validatePath(List theMdmTypes, MdmFieldMatchJson theFieldMatch) { + private void validatePath(EmpiFieldMatchJson theFieldMatch) { String resourceType = theFieldMatch.getResourceType(); - - if (MdmConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE.equals(resourceType)) { - validateFieldPathForAllTypes(theMdmTypes, theFieldMatch); + if (EmpiConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE.equals(resourceType)) { + validatePatientPath(theFieldMatch); + validatePractitionerPath(theFieldMatch); + } else if ("Patient".equals(resourceType)) { + validatePatientPath(theFieldMatch); + } else if ("Practitioner".equals(resourceType)) { + validatePractitionerPath(theFieldMatch); } else { - validateFieldPath(theFieldMatch); + throw new ConfigurationException("MatchField " + theFieldMatch.getName() + " has unknown resourceType " + resourceType); } } - private void validateFieldPathForAllTypes(List theMdmResourceTypes, MdmFieldMatchJson theFieldMatch) { - - for (String resourceType: theMdmResourceTypes) { - validateFieldPathForType(resourceType, theFieldMatch); - } - } - - private void validateFieldPathForType(String theResourceType, MdmFieldMatchJson theFieldMatch) { - ourLog.debug(" validating resource {} for {} ", theResourceType, theFieldMatch.getResourcePath()); - + private void validatePatientPath(EmpiFieldMatchJson theFieldMatch) { try { - RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theResourceType); - Class implementingClass = resourceDefinition.getImplementingClass(); - String path = theResourceType + "." + theFieldMatch.getResourcePath(); - myTerser.getDefinition(implementingClass, path); - } catch (DataFormatException | ConfigurationException | ClassCastException e) { + myTerser.getDefinition(myPatientClass, "Patient." + theFieldMatch.getResourcePath()); + } catch (DataFormatException | ConfigurationException e) { throw new ConfigurationException("MatchField " + theFieldMatch.getName() + " resourceType " + @@ -175,19 +143,26 @@ public class MdmRuleValidator implements IMdmRuleValidator { } } - private void validateFieldPath(MdmFieldMatchJson theFieldMatch) { - validateFieldPathForType(theFieldMatch.getResourceType(), theFieldMatch); + private void validatePractitionerPath(EmpiFieldMatchJson theFieldMatch) { + try { + myTerser.getDefinition(myPractitionerClass, "Practitioner." + theFieldMatch.getResourcePath()); + } catch (DataFormatException e) { + throw new ConfigurationException("MatchField " + + theFieldMatch.getName() + + " resourceType " + + theFieldMatch.getResourceType() + + " has invalid path '" + theFieldMatch.getResourcePath() + "'. " + + e.getMessage()); + } } - private void validateSystemIsUri(MdmRulesJson theMdmRulesJson) { - if (theMdmRulesJson.getEnterpriseEIDSystem() == null) { + private void validateSystemIsUri(EmpiRulesJson theEmpiRulesJson) { + if (theEmpiRulesJson.getEnterpriseEIDSystem() == null) { return; } - ourLog.info("Validating system URI {}", theMdmRulesJson.getEnterpriseEIDSystem()); - try { - new URI(theMdmRulesJson.getEnterpriseEIDSystem()); + new URI(theEmpiRulesJson.getEnterpriseEIDSystem()); } catch (URISyntaxException e) { throw new ConfigurationException("Enterprise Identifier System (eidSystem) must be a valid URI"); } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/config/MdmSettings.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/config/EmpiSettings.java similarity index 56% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/config/MdmSettings.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/config/EmpiSettings.java index 6bc7eecf4ee..db310c8933d 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/config/MdmSettings.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/config/EmpiSettings.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.config; +package ca.uhn.fhir.empi.rules.config; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -20,9 +20,9 @@ package ca.uhn.fhir.mdm.rules.config; * #L% */ -import ca.uhn.fhir.mdm.api.IMdmRuleValidator; -import ca.uhn.fhir.mdm.api.IMdmSettings; -import ca.uhn.fhir.mdm.rules.json.MdmRulesJson; +import ca.uhn.fhir.empi.api.IEmpiRuleValidator; +import ca.uhn.fhir.empi.api.IEmpiSettings; +import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; import ca.uhn.fhir.util.JsonUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -30,26 +30,27 @@ import org.springframework.stereotype.Component; import java.io.IOException; @Component -public class MdmSettings implements IMdmSettings { - private final IMdmRuleValidator myMdmRuleValidator; +public class EmpiSettings implements IEmpiSettings { + private final IEmpiRuleValidator myEmpiRuleValidator; private boolean myEnabled; - private int myConcurrentConsumers = MDM_DEFAULT_CONCURRENT_CONSUMERS; + private int myConcurrentConsumers = EMPI_DEFAULT_CONCURRENT_CONSUMERS; private String myScriptText; - private MdmRulesJson myMdmRules; + private EmpiRulesJson myEmpiRules; private boolean myPreventEidUpdates; /** - * If disabled, the underlying MDM system will operate under the following assumptions: + * If disabled, the underlying EMPI system will operate under the following assumptions: + * + * 1. Patients/Practitioners may have more than 1 EID of the same system simultaneously. + * 2. During linking, incoming patient EIDs will be merged with existing Person EIDs. * - * 1. Source resource may have more than 1 EID of the same system simultaneously. - * 2. During linking, incoming patient EIDs will be merged with existing Golden Resource EIDs. */ private boolean myPreventMultipleEids; @Autowired - public MdmSettings(IMdmRuleValidator theMdmRuleValidator) { - myMdmRuleValidator = theMdmRuleValidator; + public EmpiSettings(IEmpiRuleValidator theEmpiRuleValidator) { + myEmpiRuleValidator = theEmpiRuleValidator; } @Override @@ -57,7 +58,7 @@ public class MdmSettings implements IMdmSettings { return myEnabled; } - public MdmSettings setEnabled(boolean theEnabled) { + public EmpiSettings setEnabled(boolean theEnabled) { myEnabled = theEnabled; return this; } @@ -67,7 +68,7 @@ public class MdmSettings implements IMdmSettings { return myConcurrentConsumers; } - public MdmSettings setConcurrentConsumers(int theConcurrentConsumers) { + public EmpiSettings setConcurrentConsumers(int theConcurrentConsumers) { myConcurrentConsumers = theConcurrentConsumers; return this; } @@ -76,15 +77,15 @@ public class MdmSettings implements IMdmSettings { return myScriptText; } - public MdmSettings setScriptText(String theScriptText) throws IOException { + public EmpiSettings setScriptText(String theScriptText) throws IOException { myScriptText = theScriptText; - setMdmRules(JsonUtil.deserialize(theScriptText, MdmRulesJson.class)); + setEmpiRules(JsonUtil.deserialize(theScriptText, EmpiRulesJson.class)); return this; } @Override - public MdmRulesJson getMdmRules() { - return myMdmRules; + public EmpiRulesJson getEmpiRules() { + return myEmpiRules; } @Override @@ -92,14 +93,14 @@ public class MdmSettings implements IMdmSettings { return myPreventEidUpdates; } - public MdmSettings setPreventEidUpdates(boolean thePreventEidUpdates) { + public EmpiSettings setPreventEidUpdates(boolean thePreventEidUpdates) { myPreventEidUpdates = thePreventEidUpdates; return this; } - public MdmSettings setMdmRules(MdmRulesJson theMdmRules) { - myMdmRuleValidator.validate(theMdmRules); - myMdmRules = theMdmRules; + public EmpiSettings setEmpiRules(EmpiRulesJson theEmpiRules) { + myEmpiRuleValidator.validate(theEmpiRules); + myEmpiRules = theEmpiRules; return this; } @@ -109,10 +110,10 @@ public class MdmSettings implements IMdmSettings { @Override public String getRuleVersion() { - return myMdmRules.getVersion(); + return myEmpiRules.getVersion(); } - public MdmSettings setPreventMultipleEids(boolean thePreventMultipleEids) { + public EmpiSettings setPreventMultipleEids(boolean thePreventMultipleEids) { myPreventMultipleEids = thePreventMultipleEids; return this; } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmFieldMatchJson.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiFieldMatchJson.java similarity index 70% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmFieldMatchJson.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiFieldMatchJson.java index 5c3b3bfe994..27895def948 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmFieldMatchJson.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiFieldMatchJson.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.json; +package ca.uhn.fhir.empi.rules.json; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -21,8 +21,8 @@ package ca.uhn.fhir.mdm.rules.json; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.MdmMatchEvaluation; -import ca.uhn.fhir.mdm.rules.matcher.MdmMatcherEnum; +import ca.uhn.fhir.empi.api.EmpiMatchEvaluation; +import ca.uhn.fhir.empi.rules.matcher.EmpiMatcherEnum; import ca.uhn.fhir.model.api.IModelJson; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import com.fasterxml.jackson.annotation.JsonProperty; @@ -33,11 +33,11 @@ import javax.annotation.Nonnull; /** * Contains all business data for determining if a match exists on a particular field, given: *

- * 1. A {@link MdmMatcherEnum} which determines the actual similarity values. + * 1. A {@link EmpiMatcherEnum} which determines the actual similarity values. * 2. A given resource type (e.g. Patient) * 3. A given FHIRPath expression for finding the particular primitive to be used for comparison. (e.g. name.given) */ -public class MdmFieldMatchJson implements IModelJson { +public class EmpiFieldMatchJson implements IModelJson { @JsonProperty(value = "name", required = true) String myName; @@ -48,16 +48,16 @@ public class MdmFieldMatchJson implements IModelJson { String myResourcePath; @JsonProperty(value = "matcher", required = false) - MdmMatcherJson myMatcher; + EmpiMatcherJson myMatcher; @JsonProperty(value = "similarity", required = false) - MdmSimilarityJson mySimilarity; + EmpiSimilarityJson mySimilarity; public String getResourceType() { return myResourceType; } - public MdmFieldMatchJson setResourceType(String theResourceType) { + public EmpiFieldMatchJson setResourceType(String theResourceType) { myResourceType = theResourceType; return this; } @@ -66,7 +66,7 @@ public class MdmFieldMatchJson implements IModelJson { return myResourcePath; } - public MdmFieldMatchJson setResourcePath(String theResourcePath) { + public EmpiFieldMatchJson setResourcePath(String theResourcePath) { myResourcePath = theResourcePath; return this; } @@ -75,33 +75,33 @@ public class MdmFieldMatchJson implements IModelJson { return myName; } - public MdmFieldMatchJson setName(@Nonnull String theName) { + public EmpiFieldMatchJson setName(@Nonnull String theName) { myName = theName; return this; } - public MdmMatcherJson getMatcher() { + public EmpiMatcherJson getMatcher() { return myMatcher; } - public MdmFieldMatchJson setMatcher(MdmMatcherJson theMatcher) { + public EmpiFieldMatchJson setMatcher(EmpiMatcherJson theMatcher) { myMatcher = theMatcher; return this; } - public MdmSimilarityJson getSimilarity() { + public EmpiSimilarityJson getSimilarity() { return mySimilarity; } - public MdmFieldMatchJson setSimilarity(MdmSimilarityJson theSimilarity) { + public EmpiFieldMatchJson setSimilarity(EmpiSimilarityJson theSimilarity) { mySimilarity = theSimilarity; return this; } - public MdmMatchEvaluation match(FhirContext theFhirContext, IBase theLeftValue, IBase theRightValue) { + public EmpiMatchEvaluation match(FhirContext theFhirContext, IBase theLeftValue, IBase theRightValue) { if (myMatcher != null) { boolean result = myMatcher.match(theFhirContext, theLeftValue, theRightValue); - return new MdmMatchEvaluation(result, result ? 1.0 : 0.0); + return new EmpiMatchEvaluation(result, result ? 1.0 : 0.0); } if (mySimilarity != null) { return mySimilarity.match(theFhirContext, theLeftValue, theRightValue); diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmFilterSearchParamJson.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiFilterSearchParamJson.java similarity index 74% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmFilterSearchParamJson.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiFilterSearchParamJson.java index 1f8802797d7..1db1ffd18b3 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmFilterSearchParamJson.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiFilterSearchParamJson.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.json; +package ca.uhn.fhir.empi.rules.json; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -25,11 +25,10 @@ import ca.uhn.fhir.rest.param.TokenParamModifier; import com.fasterxml.jackson.annotation.JsonProperty; /** - * This class, unlike {@link MdmResourceSearchParamJson}, is responsible for doing inclusions during MDM - * candidate searching. e.g. When doing candidate matching, only consider candidates that match all - * MdmFilterSearchParams. + * This SearchParamJson, unlike EmpiREsourceSearchParamJson, is responsible for doing inclusions during empi + * candidate searching. e.g. When doing candidate matching, only consider candidates that match all EmpiFilterSearchParams. */ -public class MdmFilterSearchParamJson implements IModelJson { +public class EmpiFilterSearchParamJson implements IModelJson { @JsonProperty(value = "resourceType", required = true) String myResourceType; @JsonProperty(value = "searchParam", required = true) @@ -43,7 +42,7 @@ public class MdmFilterSearchParamJson implements IModelJson { return myResourceType; } - public MdmFilterSearchParamJson setResourceType(String theResourceType) { + public EmpiFilterSearchParamJson setResourceType(String theResourceType) { myResourceType = theResourceType; return this; } @@ -52,7 +51,7 @@ public class MdmFilterSearchParamJson implements IModelJson { return mySearchParam; } - public MdmFilterSearchParamJson setSearchParam(String theSearchParam) { + public EmpiFilterSearchParamJson setSearchParam(String theSearchParam) { mySearchParam = theSearchParam; return this; } @@ -62,7 +61,7 @@ public class MdmFilterSearchParamJson implements IModelJson { return myTokenParamModifier; } - public MdmFilterSearchParamJson setTokenParamModifier(TokenParamModifier theTokenParamModifier) { + public EmpiFilterSearchParamJson setTokenParamModifier(TokenParamModifier theTokenParamModifier) { myTokenParamModifier = theTokenParamModifier; return this; } @@ -71,7 +70,7 @@ public class MdmFilterSearchParamJson implements IModelJson { return myFixedValue; } - public MdmFilterSearchParamJson setFixedValue(String theFixedValue) { + public EmpiFilterSearchParamJson setFixedValue(String theFixedValue) { myFixedValue = theFixedValue; return this; } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmMatcherJson.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiMatcherJson.java similarity index 78% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmMatcherJson.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiMatcherJson.java index 094610c3dc7..33f5981c850 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmMatcherJson.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiMatcherJson.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.json; +package ca.uhn.fhir.empi.rules.json; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -21,14 +21,14 @@ package ca.uhn.fhir.mdm.rules.json; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.rules.matcher.MdmMatcherEnum; +import ca.uhn.fhir.empi.rules.matcher.EmpiMatcherEnum; import ca.uhn.fhir.model.api.IModelJson; import com.fasterxml.jackson.annotation.JsonProperty; import org.hl7.fhir.instance.model.api.IBase; -public class MdmMatcherJson implements IModelJson { +public class EmpiMatcherJson implements IModelJson { @JsonProperty(value = "algorithm", required = true) - MdmMatcherEnum myAlgorithm; + EmpiMatcherEnum myAlgorithm; @JsonProperty(value = "identifierSystem", required = false) String myIdentifierSystem; @@ -39,11 +39,11 @@ public class MdmMatcherJson implements IModelJson { @JsonProperty(value = "exact") boolean myExact; - public MdmMatcherEnum getAlgorithm() { + public EmpiMatcherEnum getAlgorithm() { return myAlgorithm; } - public MdmMatcherJson setAlgorithm(MdmMatcherEnum theAlgorithm) { + public EmpiMatcherJson setAlgorithm(EmpiMatcherEnum theAlgorithm) { myAlgorithm = theAlgorithm; return this; } @@ -52,7 +52,7 @@ public class MdmMatcherJson implements IModelJson { return myIdentifierSystem; } - public MdmMatcherJson setIdentifierSystem(String theIdentifierSystem) { + public EmpiMatcherJson setIdentifierSystem(String theIdentifierSystem) { myIdentifierSystem = theIdentifierSystem; return this; } @@ -61,7 +61,7 @@ public class MdmMatcherJson implements IModelJson { return myExact; } - public MdmMatcherJson setExact(boolean theExact) { + public EmpiMatcherJson setExact(boolean theExact) { myExact = theExact; return this; } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmResourceSearchParamJson.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiResourceSearchParamJson.java similarity index 81% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmResourceSearchParamJson.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiResourceSearchParamJson.java index 4f0b6fa108e..52c4c915310 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmResourceSearchParamJson.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiResourceSearchParamJson.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.json; +package ca.uhn.fhir.empi.rules.json; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -30,7 +30,7 @@ import java.util.List; /** * */ -public class MdmResourceSearchParamJson implements IModelJson, Iterable { +public class EmpiResourceSearchParamJson implements IModelJson, Iterable { @JsonProperty(value = "resourceType", required = true) String myResourceType; @JsonProperty(value = "searchParams", required = true) @@ -40,7 +40,7 @@ public class MdmResourceSearchParamJson implements IModelJson, Iterable return myResourceType; } - public MdmResourceSearchParamJson setResourceType(String theResourceType) { + public EmpiResourceSearchParamJson setResourceType(String theResourceType) { myResourceType = theResourceType; return this; } @@ -49,7 +49,7 @@ public class MdmResourceSearchParamJson implements IModelJson, Iterable return getSearchParams().iterator(); } - public MdmResourceSearchParamJson addSearchParam(String theSearchParam) { + public EmpiResourceSearchParamJson addSearchParam(String theSearchParam) { getSearchParams().add(theSearchParam); return this; } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmRulesJson.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiRulesJson.java similarity index 68% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmRulesJson.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiRulesJson.java index 696aa16022e..d3d459eb7d2 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmRulesJson.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiRulesJson.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.json; +package ca.uhn.fhir.empi.rules.json; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -20,7 +20,7 @@ package ca.uhn.fhir.mdm.rules.json; * #L% */ -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; import ca.uhn.fhir.model.api.IModelJson; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -34,37 +34,32 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -@JsonDeserialize(converter = MdmRulesJson.MdmRulesJsonConverter.class) -public class MdmRulesJson implements IModelJson { - +@JsonDeserialize(converter = EmpiRulesJson.EmpiRulesJsonConverter.class) +public class EmpiRulesJson implements IModelJson { @JsonProperty(value = "version", required = true) String myVersion; @JsonProperty(value = "candidateSearchParams", required = true) - List myCandidateSearchParams = new ArrayList<>(); + List myCandidateSearchParams = new ArrayList<>(); @JsonProperty(value = "candidateFilterSearchParams", required = true) - List myCandidateFilterSearchParams = new ArrayList<>(); + List myCandidateFilterSearchParams = new ArrayList<>(); @JsonProperty(value = "matchFields", required = true) - List myMatchFieldJsonList = new ArrayList<>(); + List myMatchFieldJsonList = new ArrayList<>(); @JsonProperty(value = "matchResultMap", required = true) - Map myMatchResultMap = new HashMap<>(); + Map myMatchResultMap = new HashMap<>(); @JsonProperty(value = "eidSystem") String myEnterpriseEIDSystem; - - @JsonProperty(value = "mdmTypes") - List myMdmTypes; - transient VectorMatchResultMap myVectorMatchResultMap; - public void addMatchField(MdmFieldMatchJson theMatchRuleName) { + public void addMatchField(EmpiFieldMatchJson theMatchRuleName) { myMatchFieldJsonList.add(theMatchRuleName); } - public void addResourceSearchParam(MdmResourceSearchParamJson theSearchParam) { + public void addResourceSearchParam(EmpiResourceSearchParamJson theSearchParam) { myCandidateSearchParams.add(theSearchParam); } - public void addFilterSearchParam(MdmFilterSearchParamJson theSearchParam) { + public void addFilterSearchParam(EmpiFilterSearchParamJson theSearchParam) { myCandidateFilterSearchParams.add(theSearchParam); } @@ -72,24 +67,24 @@ public class MdmRulesJson implements IModelJson { return myMatchFieldJsonList.size(); } - MdmFieldMatchJson get(int theIndex) { + EmpiFieldMatchJson get(int theIndex) { return myMatchFieldJsonList.get(theIndex); } - MdmMatchResultEnum getMatchResult(String theFieldMatchNames) { + EmpiMatchResultEnum getMatchResult(String theFieldMatchNames) { return myMatchResultMap.get(theFieldMatchNames); } - public MdmMatchResultEnum getMatchResult(Long theMatchVector) { + public EmpiMatchResultEnum getMatchResult(Long theMatchVector) { return myVectorMatchResultMap.get(theMatchVector); } - public void putMatchResult(String theFieldMatchNames, MdmMatchResultEnum theMatchResult) { + public void putMatchResult(String theFieldMatchNames, EmpiMatchResultEnum theMatchResult) { myMatchResultMap.put(theFieldMatchNames, theMatchResult); initialize(); } - Map getMatchResultMap() { + Map getMatchResultMap() { return Collections.unmodifiableMap(myMatchResultMap); } @@ -100,15 +95,15 @@ public class MdmRulesJson implements IModelJson { myVectorMatchResultMap = new VectorMatchResultMap(this); } - public List getMatchFields() { + public List getMatchFields() { return Collections.unmodifiableList(myMatchFieldJsonList); } - public List getCandidateSearchParams() { + public List getCandidateSearchParams() { return Collections.unmodifiableList(myCandidateSearchParams); } - public List getCandidateFilterSearchParams() { + public List getCandidateFilterSearchParams() { return Collections.unmodifiableList(myCandidateFilterSearchParams); } @@ -124,7 +119,7 @@ public class MdmRulesJson implements IModelJson { return myVersion; } - public MdmRulesJson setVersion(String theVersion) { + public EmpiRulesJson setVersion(String theVersion) { myVersion = theVersion; return this; } @@ -164,28 +159,19 @@ public class MdmRulesJson implements IModelJson { /** * Ensure the vector map is initialized after we deserialize */ - static class MdmRulesJsonConverter extends StdConverter { + static class EmpiRulesJsonConverter extends StdConverter { /** * This empty constructor is required by Jackson */ - public MdmRulesJsonConverter() { + public EmpiRulesJsonConverter() { } @Override - public MdmRulesJson convert(MdmRulesJson theMdmRulesJson) { - theMdmRulesJson.validate(); - theMdmRulesJson.initialize(); - return theMdmRulesJson; + public EmpiRulesJson convert(EmpiRulesJson theEmpiRulesJson) { + theEmpiRulesJson.validate(); + theEmpiRulesJson.initialize(); + return theEmpiRulesJson; } } - - public List getMdmTypes() { - return myMdmTypes; - } - - public void setMdmTypes(List theMdmTypes) { - myMdmTypes = theMdmTypes; - } - } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmSimilarityJson.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiSimilarityJson.java similarity index 71% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmSimilarityJson.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiSimilarityJson.java index 0676254ee55..e25613e0991 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/MdmSimilarityJson.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiSimilarityJson.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.json; +package ca.uhn.fhir.empi.rules.json; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -21,17 +21,17 @@ package ca.uhn.fhir.mdm.rules.json; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.MdmMatchEvaluation; -import ca.uhn.fhir.mdm.rules.similarity.MdmSimilarityEnum; +import ca.uhn.fhir.empi.api.EmpiMatchEvaluation; +import ca.uhn.fhir.empi.rules.similarity.EmpiSimilarityEnum; import ca.uhn.fhir.model.api.IModelJson; import com.fasterxml.jackson.annotation.JsonProperty; import org.hl7.fhir.instance.model.api.IBase; import javax.annotation.Nullable; -public class MdmSimilarityJson implements IModelJson { +public class EmpiSimilarityJson implements IModelJson { @JsonProperty(value = "algorithm", required = true) - MdmSimilarityEnum myAlgorithm; + EmpiSimilarityEnum myAlgorithm; @JsonProperty(value = "matchThreshold", required = true) Double myMatchThreshold; @@ -42,11 +42,11 @@ public class MdmSimilarityJson implements IModelJson { @JsonProperty(value = "exact") boolean myExact; - public MdmSimilarityEnum getAlgorithm() { + public EmpiSimilarityEnum getAlgorithm() { return myAlgorithm; } - public MdmSimilarityJson setAlgorithm(MdmSimilarityEnum theAlgorithm) { + public EmpiSimilarityJson setAlgorithm(EmpiSimilarityEnum theAlgorithm) { myAlgorithm = theAlgorithm; return this; } @@ -56,7 +56,7 @@ public class MdmSimilarityJson implements IModelJson { return myMatchThreshold; } - public MdmSimilarityJson setMatchThreshold(double theMatchThreshold) { + public EmpiSimilarityJson setMatchThreshold(double theMatchThreshold) { myMatchThreshold = theMatchThreshold; return this; } @@ -65,12 +65,12 @@ public class MdmSimilarityJson implements IModelJson { return myExact; } - public MdmSimilarityJson setExact(boolean theExact) { + public EmpiSimilarityJson setExact(boolean theExact) { myExact = theExact; return this; } - public MdmMatchEvaluation match(FhirContext theFhirContext, IBase theLeftValue, IBase theRightValue) { + public EmpiMatchEvaluation match(FhirContext theFhirContext, IBase theLeftValue, IBase theRightValue) { return myAlgorithm.match(theFhirContext, theLeftValue, theRightValue, myExact, myMatchThreshold); } } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/VectorMatchResultMap.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/VectorMatchResultMap.java similarity index 68% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/VectorMatchResultMap.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/VectorMatchResultMap.java index 628dcde3c95..b993832e095 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/json/VectorMatchResultMap.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/VectorMatchResultMap.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.json; +package ca.uhn.fhir.empi.rules.json; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -21,7 +21,7 @@ package ca.uhn.fhir.mdm.rules.json; */ import ca.uhn.fhir.context.ConfigurationException; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; import javax.annotation.Nonnull; import java.util.HashMap; @@ -30,46 +30,46 @@ import java.util.Map; import java.util.Set; public class VectorMatchResultMap { - private final MdmRulesJson myMdmRulesJson; - private Map myVectorToMatchResultMap = new HashMap<>(); + private final EmpiRulesJson myEmpiRulesJson; + private Map myVectorToMatchResultMap = new HashMap<>(); private Set myMatchVectors = new HashSet<>(); private Set myPossibleMatchVectors = new HashSet<>(); private Map myVectorToFieldMatchNamesMap = new HashMap<>(); - VectorMatchResultMap(MdmRulesJson theMdmRulesJson) { - myMdmRulesJson = theMdmRulesJson; - // no reason to hold the entire mdmRulesJson here + VectorMatchResultMap(EmpiRulesJson theEmpiRulesJson) { + myEmpiRulesJson = theEmpiRulesJson; + //no reason to hold the entire empirulesjson here initMap(); } private void initMap() { - for (Map.Entry entry : myMdmRulesJson.getMatchResultMap().entrySet()) { + for (Map.Entry entry : myEmpiRulesJson.getMatchResultMap().entrySet()) { put(entry.getKey(), entry.getValue()); } } @Nonnull - public MdmMatchResultEnum get(Long theMatchVector) { + public EmpiMatchResultEnum get(Long theMatchVector) { return myVectorToMatchResultMap.computeIfAbsent(theMatchVector, this::computeMatchResult); } - private MdmMatchResultEnum computeMatchResult(Long theVector) { + private EmpiMatchResultEnum computeMatchResult(Long theVector) { if (myMatchVectors.stream().anyMatch(v -> (v & theVector) == v)) { - return MdmMatchResultEnum.MATCH; + return EmpiMatchResultEnum.MATCH; } if (myPossibleMatchVectors.stream().anyMatch(v -> (v & theVector) == v)) { - return MdmMatchResultEnum.POSSIBLE_MATCH; + return EmpiMatchResultEnum.POSSIBLE_MATCH; } - return MdmMatchResultEnum.NO_MATCH; + return EmpiMatchResultEnum.NO_MATCH; } - private void put(String theFieldMatchNames, MdmMatchResultEnum theMatchResult) { + private void put(String theFieldMatchNames, EmpiMatchResultEnum theMatchResult) { long vector = getVector(theFieldMatchNames); myVectorToFieldMatchNamesMap.put(vector, theFieldMatchNames); myVectorToMatchResultMap.put(vector, theMatchResult); - if (theMatchResult == MdmMatchResultEnum.MATCH) { + if (theMatchResult == EmpiMatchResultEnum.MATCH) { myMatchVectors.add(vector); - } else if (theMatchResult == MdmMatchResultEnum.POSSIBLE_MATCH) { + } else if (theMatchResult == EmpiMatchResultEnum.POSSIBLE_MATCH) { myPossibleMatchVectors.add(vector); } } @@ -92,8 +92,8 @@ public class VectorMatchResultMap { } private int getFieldMatchIndex(final String theFieldMatchName) { - for (int i = 0; i < myMdmRulesJson.size(); ++i) { - if (myMdmRulesJson.get(i).getName().equals(theFieldMatchName)) { + for (int i = 0; i < myEmpiRulesJson.size(); ++i) { + if (myEmpiRulesJson.get(i).getName().equals(theFieldMatchName)) { return i; } } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/BaseHapiStringMetric.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/BaseHapiStringMetric.java similarity index 92% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/BaseHapiStringMetric.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/BaseHapiStringMetric.java index a13db17830e..b7e7fb4549d 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/BaseHapiStringMetric.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/BaseHapiStringMetric.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/MdmMatcherEnum.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/EmpiMatcherEnum.java similarity index 72% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/MdmMatcherEnum.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/EmpiMatcherEnum.java index a0ddbc59158..476179ecf79 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/MdmMatcherEnum.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/EmpiMatcherEnum.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -26,11 +26,9 @@ import org.hl7.fhir.instance.model.api.IBase; /** * Enum for holding all the known FHIR Element matchers that we support in HAPI. The string matchers first - * encode the string using an Apache Encoder before comparing them. - * https://commons.apache.org/proper/commons-codec/userguide.html + * encode the string using an Apache Encoder before comparing them. https://commons.apache.org/proper/commons-codec/userguide.html */ -public enum MdmMatcherEnum { - +public enum EmpiMatcherEnum { CAVERPHONE1(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.CAVERPHONE1))), CAVERPHONE2(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.CAVERPHONE2))), COLOGNE(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.COLOGNE))), @@ -45,28 +43,27 @@ public enum MdmMatcherEnum { SUBSTRING(new HapiStringMatcher(new SubstringStringMatcher())), DATE(new HapiDateMatcher()), - NAME_ANY_ORDER(new NameMatcher(MdmNameMatchModeEnum.ANY_ORDER)), - NAME_FIRST_AND_LAST(new NameMatcher(MdmNameMatchModeEnum.FIRST_AND_LAST)), + NAME_ANY_ORDER(new NameMatcher(EmpiPersonNameMatchModeEnum.ANY_ORDER)), + NAME_FIRST_AND_LAST(new NameMatcher(EmpiPersonNameMatchModeEnum.FIRST_AND_LAST)), IDENTIFIER(new IdentifierMatcher()); - private final IMdmFieldMatcher myMdmFieldMatcher; + private final IEmpiFieldMatcher myEmpiFieldMatcher; - MdmMatcherEnum(IMdmFieldMatcher theMdmFieldMatcher) { - myMdmFieldMatcher = theMdmFieldMatcher; + EmpiMatcherEnum(IEmpiFieldMatcher theEmpiFieldMatcher) { + myEmpiFieldMatcher = theEmpiFieldMatcher; } /** - * Determines whether two FHIR elements match according using the provided {@link IMdmFieldMatcher} - * + * Determines whether two FHIR elements match according using the provided IEmpiFieldMatcher * @param theFhirContext - * @param theLeftBase left FHIR element to compare - * @param theRightBase right FHIR element to compare - * @param theExact used by String matchers. If "false" then the string is normalized (case, accents) before comparing. If "true" then an exact string comparison is performed. + * @param theLeftBase left FHIR element to compare + * @param theRightBase right FHIR element to compare + * @param theExact used by String matchers. If "false" then the string is normalized (case, accents) before comparing. If "true" then an exact string comparison is performed. * @param theIdentifierSystem used optionally by the IDENTIFIER matcher, when present, only matches the identifiers if they belong to this system. * @return */ public boolean match(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem) { - return myMdmFieldMatcher.matches(theFhirContext, theLeftBase, theRightBase, theExact, theIdentifierSystem); + return myEmpiFieldMatcher.matches(theFhirContext, theLeftBase, theRightBase, theExact, theIdentifierSystem); } } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/MdmNameMatchModeEnum.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/EmpiPersonNameMatchModeEnum.java similarity index 83% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/MdmNameMatchModeEnum.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/EmpiPersonNameMatchModeEnum.java index 5d077185df1..0766d867d47 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/MdmNameMatchModeEnum.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/EmpiPersonNameMatchModeEnum.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -20,7 +20,7 @@ package ca.uhn.fhir.mdm.rules.matcher; * #L% */ -public enum MdmNameMatchModeEnum { +public enum EmpiPersonNameMatchModeEnum { ANY_ORDER, FIRST_AND_LAST } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/HapiDateMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/HapiDateMatcher.java similarity index 90% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/HapiDateMatcher.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/HapiDateMatcher.java index eba1ddaa007..10a0b5a33bc 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/HapiDateMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/HapiDateMatcher.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -23,7 +23,7 @@ package ca.uhn.fhir.mdm.rules.matcher; import ca.uhn.fhir.context.FhirContext; import org.hl7.fhir.instance.model.api.IBase; -public class HapiDateMatcher implements IMdmFieldMatcher { +public class HapiDateMatcher implements IEmpiFieldMatcher { private final HapiDateMatcherDstu3 myHapiDateMatcherDstu3 = new HapiDateMatcherDstu3(); private final HapiDateMatcherR4 myHapiDateMatcherR4 = new HapiDateMatcherR4(); diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/HapiDateMatcherDstu3.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/HapiDateMatcherDstu3.java similarity index 96% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/HapiDateMatcherDstu3.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/HapiDateMatcherDstu3.java index 35fd7377d5c..212da13d12a 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/HapiDateMatcherDstu3.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/HapiDateMatcherDstu3.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/HapiDateMatcherR4.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/HapiDateMatcherR4.java similarity index 96% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/HapiDateMatcherR4.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/HapiDateMatcherR4.java index 3262d51b0b3..1ada5926f8c 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/HapiDateMatcherR4.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/HapiDateMatcherR4.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/HapiStringMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/HapiStringMatcher.java similarity index 87% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/HapiStringMatcher.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/HapiStringMatcher.java index c6a26303421..6279bd08943 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/HapiStringMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/HapiStringMatcher.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -27,10 +27,10 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; /** * Similarity measure for two IBase fields whose similarity can be measured by their String representations. */ -public class HapiStringMatcher extends BaseHapiStringMetric implements IMdmFieldMatcher { - private final IMdmStringMatcher myStringMatcher; +public class HapiStringMatcher extends BaseHapiStringMetric implements IEmpiFieldMatcher { + private final IEmpiStringMatcher myStringMatcher; - public HapiStringMatcher(IMdmStringMatcher theStringMatcher) { + public HapiStringMatcher(IEmpiStringMatcher theStringMatcher) { myStringMatcher = theStringMatcher; } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/IMdmFieldMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/IEmpiFieldMatcher.java similarity index 88% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/IMdmFieldMatcher.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/IEmpiFieldMatcher.java index ca732d4f1f9..08061041bac 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/IMdmFieldMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/IEmpiFieldMatcher.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -26,6 +26,6 @@ import org.hl7.fhir.instance.model.api.IBase; /** * Measure how similar two IBase (resource fields) are to one another. 1.0 means identical. 0.0 means completely different. */ -public interface IMdmFieldMatcher { +public interface IEmpiFieldMatcher { boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem); } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/IMdmStringMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/IEmpiStringMatcher.java similarity index 85% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/IMdmStringMatcher.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/IEmpiStringMatcher.java index 06fd434b9fe..49665c78ad4 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/IMdmStringMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/IEmpiStringMatcher.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -20,6 +20,6 @@ package ca.uhn.fhir.mdm.rules.matcher; * #L% */ -public interface IMdmStringMatcher { +public interface IEmpiStringMatcher { boolean matches(String theLeftString, String theRightString); } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/IdentifierMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/IdentifierMatcher.java similarity index 86% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/IdentifierMatcher.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/IdentifierMatcher.java index c390aacc451..d79b4b73c8d 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/IdentifierMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/IdentifierMatcher.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -21,11 +21,11 @@ package ca.uhn.fhir.mdm.rules.matcher; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.util.CanonicalIdentifier; -import ca.uhn.fhir.mdm.util.IdentifierUtil; +import ca.uhn.fhir.empi.util.CanonicalIdentifier; +import ca.uhn.fhir.empi.util.IdentifierUtil; import org.hl7.fhir.instance.model.api.IBase; -public class IdentifierMatcher implements IMdmFieldMatcher { +public class IdentifierMatcher implements IEmpiFieldMatcher { /** * * @return true if the two fhir identifiers are the same. If @param theIdentifierSystem is not null, then the diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/NameMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/NameMatcher.java similarity index 86% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/NameMatcher.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/NameMatcher.java index c9c4f5e9530..1928c5a8a13 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/NameMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/NameMatcher.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -21,7 +21,7 @@ package ca.uhn.fhir.mdm.rules.matcher; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.util.NameUtil; +import ca.uhn.fhir.empi.util.NameUtil; import ca.uhn.fhir.util.StringUtil; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBase; @@ -32,11 +32,11 @@ import java.util.stream.Collectors; /** * Similarity measure for two IBase name fields */ -public class NameMatcher implements IMdmFieldMatcher { +public class NameMatcher implements IEmpiFieldMatcher { - private final MdmNameMatchModeEnum myMatchMode; + private final EmpiPersonNameMatchModeEnum myMatchMode; - public NameMatcher(MdmNameMatchModeEnum theMatchMode) { + public NameMatcher(EmpiPersonNameMatchModeEnum theMatchMode) { myMatchMode = theMatchMode; } @@ -63,7 +63,7 @@ public class NameMatcher implements IMdmFieldMatcher { for (String leftGivenName : leftGivenNames) { for (String rightGivenName : rightGivenNames) { match |= leftGivenName.equals(rightGivenName) && leftFamilyName.equals(rightFamilyName); - if (myMatchMode == MdmNameMatchModeEnum.ANY_ORDER) { + if (myMatchMode == EmpiPersonNameMatchModeEnum.ANY_ORDER) { match |= leftGivenName.equals(rightFamilyName) && leftFamilyName.equals(rightGivenName); } } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/PhoneticEncoderMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/PhoneticEncoderMatcher.java similarity index 89% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/PhoneticEncoderMatcher.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/PhoneticEncoderMatcher.java index 72b46cb8f14..318c5174bde 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/PhoneticEncoderMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/PhoneticEncoderMatcher.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -25,7 +25,7 @@ import ca.uhn.fhir.context.phonetic.PhoneticEncoderEnum; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class PhoneticEncoderMatcher implements IMdmStringMatcher { +public class PhoneticEncoderMatcher implements IEmpiStringMatcher { private static final Logger ourLog = LoggerFactory.getLogger(PhoneticEncoderMatcher.class); private final IPhoneticEncoder myStringEncoder; diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/SubstringStringMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/SubstringStringMatcher.java similarity index 84% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/SubstringStringMatcher.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/SubstringStringMatcher.java index 4bc630e0a66..92baab6720e 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/SubstringStringMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/matcher/SubstringStringMatcher.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -20,7 +20,7 @@ package ca.uhn.fhir.mdm.rules.matcher; * #L% */ -public class SubstringStringMatcher implements IMdmStringMatcher { +public class SubstringStringMatcher implements IEmpiStringMatcher { @Override public boolean matches(String theLeftString, String theRightString) { return theLeftString.startsWith(theRightString) || theRightString.startsWith(theLeftString); diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/similarity/MdmSimilarityEnum.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/EmpiSimilarityEnum.java similarity index 61% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/similarity/MdmSimilarityEnum.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/EmpiSimilarityEnum.java index 5dde32a66fb..d064a5fb369 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/similarity/MdmSimilarityEnum.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/EmpiSimilarityEnum.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.similarity; +package ca.uhn.fhir.empi.rules.similarity; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -21,7 +21,7 @@ package ca.uhn.fhir.mdm.rules.similarity; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.MdmMatchEvaluation; +import ca.uhn.fhir.empi.api.EmpiMatchEvaluation; import info.debatty.java.stringsimilarity.Cosine; import info.debatty.java.stringsimilarity.Jaccard; import info.debatty.java.stringsimilarity.JaroWinkler; @@ -31,26 +31,25 @@ import org.hl7.fhir.instance.model.api.IBase; import javax.annotation.Nullable; -public enum MdmSimilarityEnum { - +public enum EmpiSimilarityEnum { JARO_WINKLER(new HapiStringSimilarity(new JaroWinkler())), COSINE(new HapiStringSimilarity(new Cosine())), JACCARD(new HapiStringSimilarity(new Jaccard())), LEVENSCHTEIN(new HapiStringSimilarity(new NormalizedLevenshtein())), SORENSEN_DICE(new HapiStringSimilarity(new SorensenDice())); - private final IMdmFieldSimilarity myMdmFieldSimilarity; + private final IEmpiFieldSimilarity myEmpiFieldSimilarity; - MdmSimilarityEnum(IMdmFieldSimilarity theMdmFieldSimilarity) { - myMdmFieldSimilarity = theMdmFieldSimilarity; + EmpiSimilarityEnum(IEmpiFieldSimilarity theEmpiFieldSimilarity) { + myEmpiFieldSimilarity = theEmpiFieldSimilarity; } - public MdmMatchEvaluation match(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, @Nullable Double theThreshold) { - return matchBySimilarity(myMdmFieldSimilarity, theFhirContext, theLeftBase, theRightBase, theExact, theThreshold); + public EmpiMatchEvaluation match(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, @Nullable Double theThreshold) { + return matchBySimilarity(myEmpiFieldSimilarity, theFhirContext, theLeftBase, theRightBase, theExact, theThreshold); } - private MdmMatchEvaluation matchBySimilarity(IMdmFieldSimilarity theSimilarity, FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, Double theThreshold) { + private EmpiMatchEvaluation matchBySimilarity(IEmpiFieldSimilarity theSimilarity, FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, Double theThreshold) { double similarityResult = theSimilarity.similarity(theFhirContext, theLeftBase, theRightBase, theExact); - return new MdmMatchEvaluation(similarityResult >= theThreshold, similarityResult); + return new EmpiMatchEvaluation(similarityResult >= theThreshold, similarityResult); } } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/similarity/HapiStringSimilarity.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/HapiStringSimilarity.java similarity index 90% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/similarity/HapiStringSimilarity.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/HapiStringSimilarity.java index fb222127117..aea535228a5 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/similarity/HapiStringSimilarity.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/HapiStringSimilarity.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.similarity; +package ca.uhn.fhir.empi.rules.similarity; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -21,7 +21,7 @@ package ca.uhn.fhir.mdm.rules.similarity; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.rules.matcher.BaseHapiStringMetric; +import ca.uhn.fhir.empi.rules.matcher.BaseHapiStringMetric; import info.debatty.java.stringsimilarity.interfaces.NormalizedStringSimilarity; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IPrimitiveType; @@ -29,7 +29,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; /** * Similarity measure for two IBase fields whose similarity can be measured by their String representations. */ -public class HapiStringSimilarity extends BaseHapiStringMetric implements IMdmFieldSimilarity { +public class HapiStringSimilarity extends BaseHapiStringMetric implements IEmpiFieldSimilarity { private final NormalizedStringSimilarity myStringSimilarity; public HapiStringSimilarity(NormalizedStringSimilarity theStringSimilarity) { diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/similarity/IMdmFieldSimilarity.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/IEmpiFieldSimilarity.java similarity index 88% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/similarity/IMdmFieldSimilarity.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/IEmpiFieldSimilarity.java index 2f92998871b..0ef6c4bc4ea 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/similarity/IMdmFieldSimilarity.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/IEmpiFieldSimilarity.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.similarity; +package ca.uhn.fhir.empi.rules.similarity; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -26,6 +26,6 @@ import org.hl7.fhir.instance.model.api.IBase; /** * Measure how similar two IBase (resource fields) are to one another. 1.0 means identical. 0.0 means completely different. */ -public interface IMdmFieldSimilarity { +public interface IEmpiFieldSimilarity { double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact); } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/svc/MdmResourceFieldMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldMatcher.java similarity index 57% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/svc/MdmResourceFieldMatcher.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldMatcher.java index 8579c5da547..cf1751ed819 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/svc/MdmResourceFieldMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldMatcher.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.svc; +package ca.uhn.fhir.empi.rules.svc; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -21,43 +21,35 @@ package ca.uhn.fhir.mdm.rules.svc; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.MdmMatchEvaluation; -import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson; -import ca.uhn.fhir.mdm.rules.json.MdmRulesJson; +import ca.uhn.fhir.empi.api.EmpiMatchEvaluation; +import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson; import ca.uhn.fhir.util.FhirTerser; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import java.util.List; -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; /** * This class is responsible for performing matching between raw-typed values of a left record and a right record. */ -public class MdmResourceFieldMatcher { - +public class EmpiResourceFieldMatcher { private final FhirContext myFhirContext; - private final MdmFieldMatchJson myMdmFieldMatchJson; + private final EmpiFieldMatchJson myEmpiFieldMatchJson; private final String myResourceType; private final String myResourcePath; - private final MdmRulesJson myMdmRulesJson; - private final String myName; - public MdmResourceFieldMatcher(FhirContext theFhirContext, MdmFieldMatchJson theMdmFieldMatchJson, MdmRulesJson theMdmRulesJson) { + public EmpiResourceFieldMatcher(FhirContext theFhirContext, EmpiFieldMatchJson theEmpiFieldMatchJson) { myFhirContext = theFhirContext; - myMdmFieldMatchJson = theMdmFieldMatchJson; - myResourceType = theMdmFieldMatchJson.getResourceType(); - myResourcePath = theMdmFieldMatchJson.getResourcePath(); - myName = theMdmFieldMatchJson.getName(); - myMdmRulesJson = theMdmRulesJson; + myEmpiFieldMatchJson = theEmpiFieldMatchJson; + myResourceType = theEmpiFieldMatchJson.getResourceType(); + myResourcePath = theEmpiFieldMatchJson.getResourcePath(); } /** - * Compares two {@link IBaseResource}s and determines if they match, using the algorithm defined in this object's - * {@link MdmFieldMatchJson}. + * Compares two {@link IBaseResource}s and determines if they match, using the algorithm defined in this object's EmpiFieldMatchJson. * * In this implementation, it determines whether a given field matches between two resources. Internally this is evaluated using FhirPath. If any of the elements of theLeftResource * match any of the elements of theRightResource, will return true. Otherwise, false. @@ -67,7 +59,7 @@ public class MdmResourceFieldMatcher { * @return A boolean indicating whether they match. */ @SuppressWarnings("rawtypes") - public MdmMatchEvaluation match(IBaseResource theLeftResource, IBaseResource theRightResource) { + public EmpiMatchEvaluation match(IBaseResource theLeftResource, IBaseResource theRightResource) { validate(theLeftResource); validate(theRightResource); @@ -78,42 +70,29 @@ public class MdmResourceFieldMatcher { } @SuppressWarnings("rawtypes") - private MdmMatchEvaluation match(List theLeftValues, List theRightValues) { - MdmMatchEvaluation retval = new MdmMatchEvaluation(false, 0.0); + private EmpiMatchEvaluation match(List theLeftValues, List theRightValues) { + EmpiMatchEvaluation retval = new EmpiMatchEvaluation(false, 0.0); for (IBase leftValue : theLeftValues) { for (IBase rightValue : theRightValues) { - MdmMatchEvaluation nextMatch = match(leftValue, rightValue); - retval = MdmMatchEvaluation.max(retval, nextMatch); + EmpiMatchEvaluation nextMatch = match(leftValue, rightValue); + retval = EmpiMatchEvaluation.max(retval, nextMatch); } } return retval; } - private MdmMatchEvaluation match(IBase theLeftValue, IBase theRightValue) { - return myMdmFieldMatchJson.match(myFhirContext, theLeftValue, theRightValue); + private EmpiMatchEvaluation match(IBase theLeftValue, IBase theRightValue) { + return myEmpiFieldMatchJson.match(myFhirContext, theLeftValue, theRightValue); } private void validate(IBaseResource theResource) { String resourceType = myFhirContext.getResourceType(theResource); Validate.notNull(resourceType, "Resource type may not be null"); - if (ALL_RESOURCE_SEARCH_PARAM_TYPE.equals(myResourceType)) { - boolean isMdmType = myMdmRulesJson.getMdmTypes().stream().anyMatch(mdmType -> mdmType.equalsIgnoreCase(resourceType)); - Validate.isTrue(isMdmType, "Expecting resource type %s, got resource type %s", myMdmRulesJson.getMdmTypes().stream().collect(Collectors.joining(",")), resourceType); + Validate.isTrue("Patient".equalsIgnoreCase(resourceType) || "Practitioner".equalsIgnoreCase(resourceType), + "Expecting resource type Patient/Practitioner got resource type %s", resourceType); } else { Validate.isTrue(myResourceType.equals(resourceType), "Expecting resource type %s got resource type %s", myResourceType, resourceType); } } - - public String getResourceType() { - return myResourceType; - } - - public String getResourcePath() { - return myResourcePath; - } - - public String getName() { - return myName; - } } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceMatcherSvc.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceMatcherSvc.java new file mode 100644 index 00000000000..d7ee12ee301 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceMatcherSvc.java @@ -0,0 +1,133 @@ +package ca.uhn.fhir.empi.rules.svc; + +/*- + * #%L + * HAPI FHIR - 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.EmpiMatchEvaluation; +import ca.uhn.fhir.empi.api.EmpiMatchOutcome; +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import ca.uhn.fhir.empi.api.IEmpiSettings; +import ca.uhn.fhir.empi.log.Logs; +import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson; +import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.List; + +/** + * The EmpiResourceComparator is in charge of performing actual comparisons between left and right records. + * It does so by calling individual comparators, and returning a vector based on the combination of + * field comparators that matched. + */ + +@Service +public class EmpiResourceMatcherSvc { + private static final Logger ourLog = Logs.getEmpiTroubleshootingLog(); + + private final FhirContext myFhirContext; + private final IEmpiSettings myEmpiSettings; + private EmpiRulesJson myEmpiRulesJson; + private final List myFieldMatchers = new ArrayList<>(); + + @Autowired + public EmpiResourceMatcherSvc(FhirContext theFhirContext, IEmpiSettings theEmpiSettings) { + myFhirContext = theFhirContext; + myEmpiSettings = theEmpiSettings; + } + + @PostConstruct + public void init() { + myEmpiRulesJson = myEmpiSettings.getEmpiRules(); + if (myEmpiRulesJson == null) { + throw new ConfigurationException("Failed to load EMPI Rules. If EMPI is enabled, then EMPI rules must be available in context."); + } + for (EmpiFieldMatchJson matchFieldJson : myEmpiRulesJson.getMatchFields()) { + myFieldMatchers.add(new EmpiResourceFieldMatcher(myFhirContext, matchFieldJson)); + } + + } + + /** + * Given two {@link IBaseResource}s, perform all comparisons on them to determine an {@link EmpiMatchResultEnum}, indicating + * to what level the two resources are considered to be matching. + * + * @param theLeftResource The first {@link IBaseResource}. + * @param theRightResource The second {@link IBaseResource} + * + * @return an {@link EmpiMatchResultEnum} indicating the result of the comparison. + */ + public EmpiMatchOutcome getMatchResult(IBaseResource theLeftResource, IBaseResource theRightResource) { + return match(theLeftResource, theRightResource); + } + + EmpiMatchOutcome match(IBaseResource theLeftResource, IBaseResource theRightResource) { + EmpiMatchOutcome matchResult = getMatchOutcome(theLeftResource, theRightResource); + EmpiMatchResultEnum matchResultEnum = myEmpiRulesJson.getMatchResult(matchResult.vector); + matchResult.setMatchResultEnum(matchResultEnum); + if (ourLog.isDebugEnabled()) { + if (matchResult.isMatch() || matchResult.isPossibleMatch()) { + ourLog.debug("{} {} with field matchers {}", matchResult, theRightResource.getIdElement().toUnqualifiedVersionless(), myEmpiRulesJson.getFieldMatchNamesForVector(matchResult.vector)); + } else if (ourLog.isTraceEnabled()) { + ourLog.trace("{} {}. Field matcher results: {}", matchResult, theRightResource.getIdElement().toUnqualifiedVersionless(), myEmpiRulesJson.getDetailedFieldMatchResultForUnmatchedVector(matchResult.vector)); + } + } + return matchResult; + } + + /** + * This function generates a `match vector`, which is a long representation of a binary string + * generated by the results of each of the given comparator matches. For example. + * start with a binary representation of the value 0 for long: 0000 + * first_name matches, so the value `1` is bitwise-ORed to the current value (0) in right-most position. + * `0001` + * + * Next, we look at the second field comparator, and see if it matches. If it does, we left-shift 1 by the index + * of the comparator, in this case also 1. + * `0010` + * + * Then, we bitwise-or it with the current retval: + * 0001|0010 = 0011 + * The binary string is now `0011`, which when you return it as a long becomes `3`. + */ + private EmpiMatchOutcome getMatchOutcome(IBaseResource theLeftResource, IBaseResource theRightResource) { + long vector = 0; + double score = 0.0; + for (int i = 0; i < myFieldMatchers.size(); ++i) { + //any that are not for the resourceType in question. + EmpiResourceFieldMatcher fieldComparator = myFieldMatchers.get(i); + EmpiMatchEvaluation matchEvaluation = fieldComparator.match(theLeftResource, theRightResource); + if (matchEvaluation.match) { + vector |= (1 << i); + } + score += matchEvaluation.score; + } + + EmpiMatchOutcome retVal = new EmpiMatchOutcome(vector, score); + retVal.setEmpiRuleCount(myFieldMatchers.size()); + return retVal; + } +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/AssuranceLevelUtil.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/AssuranceLevelUtil.java new file mode 100644 index 00000000000..7e6bb7ef851 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/AssuranceLevelUtil.java @@ -0,0 +1,72 @@ +package ca.uhn.fhir.empi.util; + +/*- + * #%L + * HAPI FHIR - 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.model.CanonicalIdentityAssuranceLevel; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +/** + * Helper class to determine assurance level based on Link Source and Match Result. + * This is strictly for use in populating Person links. + */ +public final class AssuranceLevelUtil { + + private AssuranceLevelUtil() { + } + + public static CanonicalIdentityAssuranceLevel getAssuranceLevel(EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theSource) { + switch (theSource) { + case MANUAL: + return getAssuranceFromManualResult(theMatchResult); + case AUTO: + return getAssuranceFromAutoResult(theMatchResult); + } + throw new InvalidRequestException("Couldn't figure out an assurance level for result: " + theMatchResult + " and source " + theSource); + } + + private static CanonicalIdentityAssuranceLevel getAssuranceFromAutoResult(EmpiMatchResultEnum theMatchResult) { + switch (theMatchResult) { + case MATCH: + return CanonicalIdentityAssuranceLevel.LEVEL2; + case POSSIBLE_MATCH: + return CanonicalIdentityAssuranceLevel.LEVEL1; + case POSSIBLE_DUPLICATE: + case NO_MATCH: + default: + throw new InvalidRequestException("An AUTO EMPI Link may not have a match result of " + theMatchResult); + } + } + + private static CanonicalIdentityAssuranceLevel getAssuranceFromManualResult(EmpiMatchResultEnum theMatchResult) { + switch (theMatchResult) { + case MATCH: + case REDIRECT: + return CanonicalIdentityAssuranceLevel.LEVEL3; + case NO_MATCH: + case POSSIBLE_DUPLICATE: + case POSSIBLE_MATCH: + default: + throw new InvalidRequestException("A MANUAL EMPI Link may not have a match result of " + theMatchResult); + } + } +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/CanonicalIdentifier.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/CanonicalIdentifier.java similarity index 96% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/CanonicalIdentifier.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/CanonicalIdentifier.java index c35891557b6..960b4be9998 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/CanonicalIdentifier.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/CanonicalIdentifier.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.util; +package ca.uhn.fhir.empi.util; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/EIDHelper.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/EIDHelper.java similarity index 71% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/EIDHelper.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/EIDHelper.java index 16c0b7d02f7..ff6141f1fb8 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/EIDHelper.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/EIDHelper.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.util; +package ca.uhn.fhir.empi.util; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -21,9 +21,9 @@ package ca.uhn.fhir.mdm.util; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.MdmConstants; -import ca.uhn.fhir.mdm.api.IMdmSettings; -import ca.uhn.fhir.mdm.model.CanonicalEID; +import ca.uhn.fhir.empi.api.EmpiConstants; +import ca.uhn.fhir.empi.api.IEmpiSettings; +import ca.uhn.fhir.empi.model.CanonicalEID; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; @@ -36,26 +36,25 @@ import java.util.stream.Collectors; @Service public final class EIDHelper { - private final FhirContext myFhirContext; - private final IMdmSettings myMdmSettings; + private final IEmpiSettings myEmpiConfig; @Autowired - public EIDHelper(FhirContext theFhirContext, IMdmSettings theMdmSettings) { + public EIDHelper(FhirContext theFhirContext, IEmpiSettings theEmpiConfig) { myFhirContext = theFhirContext; - myMdmSettings = theMdmSettings; + myEmpiConfig = theEmpiConfig; } public CanonicalEID createHapiEid() { return new CanonicalEID( - MdmConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM, + EmpiConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM, UUID.randomUUID().toString(), null ); } /** - * Given an {@link IAnyResource} representing a type supported by MDM, retrieve their externally-assigned EID, + * Given an {@link IAnyResource} representing a patient/practitioner/person, retrieve their externally-assigned EID, * represented as a {@link CanonicalEID} * * @param theResource the resource to extract the EID from. @@ -63,11 +62,11 @@ public final class EIDHelper { * @return An optional {@link CanonicalEID} representing the external EID. Absent if the EID is not present. */ public List getExternalEid(IBaseResource theResource) { - return CanonicalEID.extractFromResource(myFhirContext, myMdmSettings.getMdmRules().getEnterpriseEIDSystem(), theResource); + return CanonicalEID.extractFromResource(myFhirContext, myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem(), theResource); } /** - * Given an {@link IAnyResource} representing a type supported by MDM, retrieve their internally-assigned EID, + * Given an {@link IAnyResource} representing a patient/practitioner/person, retrieve their internally-assigned EID, * represented as a {@link CanonicalEID} * * @param theResource the resource to extract the EID from. @@ -75,7 +74,7 @@ public final class EIDHelper { * @return An optional {@link CanonicalEID} representing the internal EID. Absent if the EID is not present. */ public List getHapiEid(IAnyResource theResource) { - return CanonicalEID.extractFromResource(myFhirContext, MdmConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM, theResource); + return CanonicalEID.extractFromResource(myFhirContext, EmpiConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM, theResource); } /** @@ -97,12 +96,16 @@ public final class EIDHelper { } /** - * An incoming resource is a potential duplicate if it matches a source resource that has a golden resource with an - * official EID, but the incoming resource also has an EID that does not match. + * An incoming resource is a potential duplicate if it matches a Patient that has a Person with an official EID, but + * the incoming resource also has an EID that does not match. + * + * @param theExistingPerson + * @param theComparingPerson + * @return */ - public boolean hasEidOverlap(IAnyResource theExistingGoldenResource, IAnyResource theComparingGoldenResource) { - List firstEids = this.getExternalEid(theExistingGoldenResource); - List secondEids = this.getExternalEid(theComparingGoldenResource); + public boolean hasEidOverlap(IAnyResource theExistingPerson, IAnyResource theComparingPerson) { + List firstEids = this.getExternalEid(theExistingPerson); + List secondEids = this.getExternalEid(theComparingPerson); if (firstEids.isEmpty() || secondEids.isEmpty()) { return false; } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/EmpiUtil.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/EmpiUtil.java new file mode 100644 index 00000000000..4bc05798f76 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/EmpiUtil.java @@ -0,0 +1,66 @@ +package ca.uhn.fhir.empi.util; + +/*- + * #%L + * HAPI FHIR - 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 org.hl7.fhir.instance.model.api.IBaseResource; + +public final class EmpiUtil { + private EmpiUtil() {} + + public static boolean supportedTargetType(String theResourceType) { + return ("Patient".equals(theResourceType) || "Practitioner".equals(theResourceType)); + } + + public static boolean isEmpiResourceType(FhirContext theFhirContext, IBaseResource theResource) { + String resourceType = theFhirContext.getResourceType(theResource); + return ("Patient".equals(resourceType) || + "Practitioner".equals(resourceType)) || + "Person".equals(resourceType); + } + + /** + * If the resource is tagged as not managed by empi, return false. Otherwise true. + * @param theBaseResource The Patient/Practitioner that is potentially managed by EMPI. + * @return A boolean indicating whether EMPI should manage this resource. + */ + public static boolean isEmpiAccessible(IBaseResource theBaseResource) { + return theBaseResource.getMeta().getTag(EmpiConstants.SYSTEM_EMPI_MANAGED, EmpiConstants.CODE_NO_EMPI_MANAGED) == null; + } + + /** + * Checks for the presence of the EMPI-managed tag, indicating the EMPI system has ownership + * of this Person's links. + * + * @param theBaseResource the resource to check. + * @return a boolean indicating whether or not EMPI manages this Person. + */ + public static boolean isEmpiManaged(IBaseResource theBaseResource) { + return theBaseResource.getMeta().getTag(EmpiConstants.SYSTEM_EMPI_MANAGED, EmpiConstants.CODE_HAPI_EMPI_MANAGED) != null; + } + + public static boolean isEmpiManagedPerson(FhirContext theFhirContext, IBaseResource theResource) { + String resourceType = theFhirContext.getResourceType(theResource); + + return "Person".equals(resourceType) && isEmpiManaged(theResource); + } +} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/IdentifierUtil.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/IdentifierUtil.java similarity index 66% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/IdentifierUtil.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/IdentifierUtil.java index a0422edfa03..fbc39f13249 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/IdentifierUtil.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/IdentifierUtil.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.util; +package ca.uhn.fhir.empi.util; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -20,16 +20,10 @@ package ca.uhn.fhir.mdm.util; * #L% */ -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.model.CanonicalEID; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import org.hl7.fhir.instance.model.api.IBase; -public final class IdentifierUtil { - - private IdentifierUtil() { - } - +public class IdentifierUtil { public static CanonicalIdentifier identifierDtFromIdentifier(IBase theIdentifier) { CanonicalIdentifier retval = new CanonicalIdentifier(); @@ -48,22 +42,4 @@ public final class IdentifierUtil { } return retval; } - - /** - * Retrieves appropriate FHIR Identifier model instance based on the context version - * - * @param theFhirContext FHIR context to use for determining the identifier version - * @param eid EID to get equivalent FHIR Identifier from - * @param Generic Identifier base interface - * @return Returns appropriate R4 or DSTU3 Identifier instance - */ - public static T toId(FhirContext theFhirContext, CanonicalEID eid) { - switch (theFhirContext.getVersion().getVersion()) { - case R4: - return (T) eid.toR4(); - case DSTU3: - return (T) eid.toDSTU3(); - } - throw new IllegalStateException("Unsupported FHIR version " + theFhirContext.getVersion().getVersion()); - } } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/NameUtil.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/NameUtil.java similarity index 94% rename from hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/NameUtil.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/NameUtil.java index 420d8d47d7c..5a340ff471c 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/NameUtil.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/NameUtil.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.util; +package ca.uhn.fhir.empi.util; /*- * #%L - * HAPI FHIR - Master Data Management + * HAPI FHIR - Enterprise Master Patient Index * %% * Copyright (C) 2014 - 2020 University Health Network * %% @@ -29,11 +29,7 @@ import org.hl7.fhir.r4.model.PrimitiveType; import java.util.List; import java.util.stream.Collectors; -public final class NameUtil { - - private NameUtil() { - } - +public class NameUtil { public static List extractGivenNames(FhirContext theFhirContext, IBase theBase) { switch(theFhirContext.getVersion().getVersion()) { case R4: diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/PersonHelper.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/PersonHelper.java new file mode 100644 index 00000000000..675dc6c4cff --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/util/PersonHelper.java @@ -0,0 +1,660 @@ +package ca.uhn.fhir.empi.util; + +/*- + * #%L + * HAPI FHIR - 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.log.Logs; +import ca.uhn.fhir.empi.model.CanonicalEID; +import ca.uhn.fhir.empi.model.CanonicalIdentityAssuranceLevel; +import ca.uhn.fhir.empi.model.EmpiTransactionContext; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseBackboneElement; +import org.hl7.fhir.instance.model.api.IBaseCoding; +import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Address; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.ContactPoint; +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.hl7.fhir.r4.model.Reference; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Service +public class PersonHelper { + private static final Logger ourLog = Logs.getEmpiTroubleshootingLog(); + + @Autowired + private IEmpiSettings myEmpiConfig; + @Autowired + private EIDHelper myEIDHelper; + + private final FhirContext myFhirContext; + + @Autowired + public PersonHelper(FhirContext theFhirContext) { + myFhirContext = theFhirContext; + } + + /** + * Given a Person, extract all {@link IIdType}s for the linked targets. + * + * @param thePerson the Person to extract link IDs from. + * @return a Stream of {@link IIdType}. + */ + public Stream getLinkIds(IBaseResource thePerson) { + switch (myFhirContext.getVersion().getVersion()) { + case R4: + Person personR4 = (Person) thePerson; + return personR4.getLink().stream() + .map(Person.PersonLinkComponent::getTarget) + .map(IBaseReference::getReferenceElement) + .map(IIdType::toUnqualifiedVersionless); + case DSTU3: + org.hl7.fhir.dstu3.model.Person personStu3 = (org.hl7.fhir.dstu3.model.Person) thePerson; + return personStu3.getLink().stream() + .map(org.hl7.fhir.dstu3.model.Person.PersonLinkComponent::getTarget) + .map(IBaseReference::getReferenceElement) + .map(IIdType::toUnqualifiedVersionless); + default: + throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); + } + } + + /** + * Determine whether or not the given {@link IBaseResource} person contains a link to a particular {@link IIdType} + * + * @param thePerson The person to check + * @param theResourceId The ID to check. + * @return A boolean indicating whether or not there was a contained link. + */ + public boolean containsLinkTo(IBaseResource thePerson, IIdType theResourceId) { + Stream links = getLinkIds(thePerson); + return links.anyMatch(link -> link.getValue().equals(theResourceId.getValue())); + } + + /** + * Create or update a link from source {@link IBaseResource} to the target {@link IIdType}, with the given {@link CanonicalIdentityAssuranceLevel}. + * @param thePerson The person who's link needs to be updated. + * @param theResourceId The target of the link + * @param canonicalAssuranceLevel The level of certainty of this link. + * @param theEmpiTransactionContext + */ + public void addOrUpdateLink(IBaseResource thePerson, IIdType theResourceId, @Nonnull CanonicalIdentityAssuranceLevel canonicalAssuranceLevel, EmpiTransactionContext theEmpiTransactionContext) { + switch (myFhirContext.getVersion().getVersion()) { + case R4: + handleLinkUpdateR4(thePerson, theResourceId, canonicalAssuranceLevel, theEmpiTransactionContext); + break; + case DSTU3: + handleLinkUpdateDSTU3(thePerson, theResourceId, canonicalAssuranceLevel, theEmpiTransactionContext); + break; + default: + throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); + } + } + + private void handleLinkUpdateDSTU3(IBaseResource thePerson, IIdType theResourceId, CanonicalIdentityAssuranceLevel theCanonicalAssuranceLevel, EmpiTransactionContext theTransactionLogMessages) { + if (theCanonicalAssuranceLevel == null) { + ourLog.warn("Refusing to update or add a link without an Assurance Level."); + return; + } + + org.hl7.fhir.dstu3.model.Person person = (org.hl7.fhir.dstu3.model.Person) thePerson; + if (!containsLinkTo(thePerson, theResourceId)) { + person.addLink().setTarget(new org.hl7.fhir.dstu3.model.Reference(theResourceId)).setAssurance(theCanonicalAssuranceLevel.toDstu3()); + logLinkAddMessage(thePerson, theResourceId, theCanonicalAssuranceLevel, theTransactionLogMessages); + } else { + person.getLink().stream() + .filter(link -> link.getTarget().getReference().equalsIgnoreCase(theResourceId.getValue())) + .findFirst() + .ifPresent(link -> { + logLinkUpdateMessage(thePerson, theResourceId, theCanonicalAssuranceLevel, theTransactionLogMessages, link.getAssurance().toCode()); + link.setAssurance(theCanonicalAssuranceLevel.toDstu3()); + }); + } + } + + private void logLinkAddMessage(IBaseResource thePerson, IIdType theResourceId, CanonicalIdentityAssuranceLevel theCanonicalAssuranceLevel, EmpiTransactionContext theEmpiTransactionContext) { + theEmpiTransactionContext.addTransactionLogMessage("Creating new link from " + (StringUtils.isBlank(thePerson.getIdElement().toUnqualifiedVersionless().getValue()) ? "new Person" : thePerson.getIdElement().toUnqualifiedVersionless()) + " -> " + theResourceId.toUnqualifiedVersionless() + " with IdentityAssuranceLevel: " + theCanonicalAssuranceLevel.name()); + } + + private void logLinkUpdateMessage(IBaseResource thePerson, IIdType theResourceId, CanonicalIdentityAssuranceLevel canonicalAssuranceLevel, EmpiTransactionContext theEmpiTransactionContext, String theOriginalAssuranceLevel) { + theEmpiTransactionContext.addTransactionLogMessage("Updating link from " + thePerson.getIdElement().toUnqualifiedVersionless() + " -> " + theResourceId.toUnqualifiedVersionless() + ". Changing IdentityAssuranceLevel: " + theOriginalAssuranceLevel + " -> " + canonicalAssuranceLevel.name()); + } + + private void handleLinkUpdateR4(IBaseResource thePerson, IIdType theResourceId, CanonicalIdentityAssuranceLevel canonicalAssuranceLevel, EmpiTransactionContext theEmpiTransactionContext) { + if (canonicalAssuranceLevel == null) { + ourLog.warn("Refusing to update or add a link without an Assurance Level."); + return; + } + + Person person = (Person) thePerson; + if (!containsLinkTo(thePerson, theResourceId)) { + person.addLink().setTarget(new Reference(theResourceId)).setAssurance(canonicalAssuranceLevel.toR4()); + logLinkAddMessage(thePerson, theResourceId, canonicalAssuranceLevel, theEmpiTransactionContext); + } else { + person.getLink().stream() + .filter(link -> link.getTarget().getReference().equalsIgnoreCase(theResourceId.getValue())) + .findFirst() + .ifPresent(link -> { + logLinkUpdateMessage(thePerson, theResourceId, canonicalAssuranceLevel, theEmpiTransactionContext, link.getAssurance().toCode()); + link.setAssurance(canonicalAssuranceLevel.toR4()); + }); + } + } + + + /** + * Removes a link from the given {@link IBaseResource} to the target {@link IIdType}. + * @param thePerson The person to remove the link from. + * @param theResourceId The target ID to remove. + * @param theEmpiTransactionContext + */ + public void removeLink(IBaseResource thePerson, IIdType theResourceId, EmpiTransactionContext theEmpiTransactionContext) { + if (!containsLinkTo(thePerson, theResourceId)) { + return; + } + theEmpiTransactionContext.addTransactionLogMessage("Removing PersonLinkComponent from " + thePerson.getIdElement().toUnqualifiedVersionless() + " -> " + theResourceId.toUnqualifiedVersionless()); + switch (myFhirContext.getVersion().getVersion()) { + case R4: + Person person = (Person) thePerson; + List links = person.getLink(); + links.removeIf(component -> component.hasTarget() && component.getTarget().getReference().equals(theResourceId.getValue())); + break; + case DSTU3: + org.hl7.fhir.dstu3.model.Person personDstu3 = (org.hl7.fhir.dstu3.model.Person) thePerson; + personDstu3.getLink().removeIf(component -> component.hasTarget() && component.getTarget().getReference().equalsIgnoreCase(theResourceId.getValue())); + break; + default: + throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); + } + } + + /** + * Create a Person from a given patient. This will carry over the Patient's EID if it exists. If it does not exist, + * a randomly generated UUID EID will be created. + * + * @param theSourceResource The Patient that will be used as the starting point for the person. + * @return the Person that is created. + */ + public IAnyResource createPersonFromEmpiTarget(IAnyResource theSourceResource) { + String eidSystem = myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem(); + List eidsToApply = myEIDHelper.getExternalEid(theSourceResource); + if (eidsToApply.isEmpty()) { + eidsToApply.add(myEIDHelper.createHapiEid()); + } + switch (myFhirContext.getVersion().getVersion()) { + case R4: + Person personR4 = new Person(); + personR4.setActive(true); + eidsToApply.forEach(eid -> personR4.addIdentifier(eid.toR4())); + personR4.getMeta().addTag((Coding) buildEmpiManagedTag()); + copyEmpiTargetDataIntoPerson(theSourceResource, personR4, true); + return personR4; + case DSTU3: + org.hl7.fhir.dstu3.model.Person personDstu3 = new org.hl7.fhir.dstu3.model.Person(); + personDstu3.setActive(true); + eidsToApply.forEach(eid -> personDstu3.addIdentifier(eid.toDSTU3())); + personDstu3.getMeta().addTag((org.hl7.fhir.dstu3.model.Coding) buildEmpiManagedTag()); + copyEmpiTargetDataIntoPerson(theSourceResource, personDstu3, true); + return personDstu3; + default: + throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); + } + } + + /** + * This will copy over all attributes that are copiable from Patient/Practitioner to Person. + * + * @param theBaseResource The incoming {@link Patient} or {@link Practitioner} who's data we want to copy into Person. + * @param thePerson The incoming {@link Person} who needs to have their data updated. + * @param theAllowOverwriting If enabled, will overwrite existing values on the person. Otherwise, will set them only if they are currently empty/null. + * + */ + private void copyEmpiTargetDataIntoPerson(IBaseResource theBaseResource, IBaseResource thePerson, Boolean theAllowOverwriting) { + switch (myFhirContext.getVersion().getVersion()) { + case R4: + copyR4TargetInformation(theBaseResource, thePerson, theAllowOverwriting); + break; + case DSTU3: + copyDSTU3TargetInformation(theBaseResource, thePerson, theAllowOverwriting); + break; + default: + throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); + } + } + + private void copyR4TargetInformation(IBaseResource theBaseResource, IBaseResource thePerson, boolean theAllowOverwriting) { + Person person = (Person) thePerson; + switch (myFhirContext.getResourceType(theBaseResource)) { + case "Patient": + Patient patient = (Patient) theBaseResource; + if (theAllowOverwriting || person.getName().isEmpty()) { + person.setName(patient.getName()); + } + if (theAllowOverwriting || person.getAddress().isEmpty()) { + person.setAddress(patient.getAddress()); + } + if (theAllowOverwriting || person.getTelecom().isEmpty()) { + person.setTelecom(patient.getTelecom()); + } + if (theAllowOverwriting || person.getBirthDate() == null) { + person.setBirthDate(patient.getBirthDate()); + } + if (theAllowOverwriting || person.getGender() == null) { + person.setGender(patient.getGender()); + } + if (theAllowOverwriting || person.getPhoto().isEmpty()) { + person.setPhoto(patient.getPhotoFirstRep()); + } + break; + case "Practitioner": + Practitioner practitioner = (Practitioner) theBaseResource; + if (theAllowOverwriting || person.getName().isEmpty()) { + person.setName(practitioner.getName()); + } + if (theAllowOverwriting || person.getAddress().isEmpty()) { + person.setAddress(practitioner.getAddress()); + } + if (theAllowOverwriting || person.getTelecom().isEmpty()) { + person.setTelecom(practitioner.getTelecom()); + } + if (theAllowOverwriting || person.getBirthDate() == null) { + person.setBirthDate(practitioner.getBirthDate()); + } + if (theAllowOverwriting || person.getGender() == null) { + person.setGender(practitioner.getGender()); + } + if (theAllowOverwriting || person.getPhoto().isEmpty()) { + person.setPhoto(practitioner.getPhotoFirstRep()); + } + break; + default: + throw new UnsupportedOperationException("EMPI targets are limited to Practitioner/Patient. This is a : " + myFhirContext.getResourceType(theBaseResource)); + } + } + + private void copyDSTU3TargetInformation(IBaseResource theBaseResource, IBaseResource thePerson, boolean theAllowOverwriting) { + org.hl7.fhir.dstu3.model.Person person = (org.hl7.fhir.dstu3.model.Person) thePerson; + switch (myFhirContext.getResourceType(theBaseResource)) { + case "Patient": + org.hl7.fhir.dstu3.model.Patient patient = (org.hl7.fhir.dstu3.model.Patient) theBaseResource; + + if (theAllowOverwriting || person.getName().isEmpty()) { + person.setName(patient.getName()); + } + if (theAllowOverwriting || person.getAddress().isEmpty()) { + person.setAddress(patient.getAddress()); + } + if (theAllowOverwriting || person.getTelecom().isEmpty()) { + person.setTelecom(patient.getTelecom()); + } + if (theAllowOverwriting || person.getBirthDate() == null ) { + person.setBirthDate(patient.getBirthDate()); + } + if (theAllowOverwriting || person.getGender() == null ) { + person.setGender(patient.getGender()); + } + if (theAllowOverwriting || person.getPhoto().isEmpty()) { + person.setPhoto(patient.getPhotoFirstRep()); + } + break; + case "Practitioner": + org.hl7.fhir.dstu3.model.Practitioner practitioner = (org.hl7.fhir.dstu3.model.Practitioner) theBaseResource; + if (theAllowOverwriting || person.getName().isEmpty()) { + person.setName(practitioner.getName()); + } + if (theAllowOverwriting || person.getAddress().isEmpty()) { + person.setAddress(practitioner.getAddress()); + } + if (theAllowOverwriting || person.getTelecom().isEmpty()) { + person.setTelecom(practitioner.getTelecom()); + } + if (theAllowOverwriting || person.getBirthDate() == null) { + person.setBirthDate(practitioner.getBirthDate()); + } + if (theAllowOverwriting || person.getGender() == null) { + person.setGender(practitioner.getGender()); + } + if (theAllowOverwriting || person.getPhoto().isEmpty()) { + person.setPhoto(practitioner.getPhotoFirstRep()); + } + break; + default: + throw new UnsupportedOperationException("EMPI targets are limited to Practitioner/Patient. This is a : " + myFhirContext.getResourceType(theBaseResource)); + } + } + + private IBaseCoding buildEmpiManagedTag() { + switch (myFhirContext.getVersion().getVersion()) { + case R4: + Coding empiManagedCoding = new Coding(); + empiManagedCoding.setSystem(EmpiConstants.SYSTEM_EMPI_MANAGED); + empiManagedCoding.setCode(EmpiConstants.CODE_HAPI_EMPI_MANAGED); + empiManagedCoding.setDisplay(EmpiConstants.DISPLAY_HAPI_EMPI_MANAGED); + return empiManagedCoding; + case DSTU3: + org.hl7.fhir.dstu3.model.Coding empiManagedCodingDstu3 = new org.hl7.fhir.dstu3.model.Coding(); + empiManagedCodingDstu3.setSystem(EmpiConstants.SYSTEM_EMPI_MANAGED); + empiManagedCodingDstu3.setCode(EmpiConstants.CODE_HAPI_EMPI_MANAGED); + empiManagedCodingDstu3.setDisplay(EmpiConstants.DISPLAY_HAPI_EMPI_MANAGED); + return empiManagedCodingDstu3; + default: + throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); + + } + } + + /** + * Update a Person's EID based on the incoming target resource. If the incoming resource has an external EID, it is applied + * to the Person, unless that person already has an external EID which does not match, in which case throw {@link IllegalArgumentException} + * + * If running in multiple EID mode, then incoming EIDs are simply added to the Person without checking for matches. + * + * @param thePerson The person to update the external EID on. + * @param theEmpiTarget The target we will retrieve the external EID from. + * @return the modified {@link IBaseResource} representing the person. + */ + public IAnyResource updatePersonExternalEidFromEmpiTarget(IAnyResource thePerson, IAnyResource theEmpiTarget, EmpiTransactionContext theEmpiTransactionContext) { + //This handles overwriting an automatically assigned EID if a patient that links is coming in with an official EID. + List incomingTargetEid = myEIDHelper.getExternalEid(theEmpiTarget); + List personOfficialEid = myEIDHelper.getExternalEid(thePerson); + + + if (!incomingTargetEid.isEmpty()) { + if (personOfficialEid.isEmpty() || !myEmpiConfig.isPreventMultipleEids()) { + log(theEmpiTransactionContext, "Incoming resource:" + theEmpiTarget.getIdElement().toUnqualifiedVersionless() + " + with EID " + incomingTargetEid.stream().map(CanonicalEID::toString).collect(Collectors.joining(",")) + " is applying this EIDs to its related Person, as this person does not yet have an external EID"); + addCanonicalEidsToPersonIfAbsent(thePerson, incomingTargetEid); + } else if (!personOfficialEid.isEmpty() && myEIDHelper.eidMatchExists(personOfficialEid, incomingTargetEid)) { + log(theEmpiTransactionContext, "incoming resource:" + theEmpiTarget.getIdElement().toVersionless() + " with EIDs "+incomingTargetEid.stream().map(CanonicalEID::toString).collect(Collectors.joining(",")) +" does not need to overwrite person, as this EID is already present"); + } else { + throw new IllegalArgumentException("This would create a duplicate person!"); + } + } + return thePerson; + } + + public IBaseResource overwriteExternalEids(IBaseResource thePerson, List theNewEid) { + clearExternalEids(thePerson); + addCanonicalEidsToPersonIfAbsent(thePerson, theNewEid); + return thePerson; + } + + private void clearExternalEids(IBaseResource thePerson) { + switch (myFhirContext.getVersion().getVersion()) { + case R4: + Person personR4 = (Person) thePerson; + personR4.getIdentifier().removeIf(theIdentifier -> theIdentifier.getSystem().equalsIgnoreCase(myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem())); + break; + case DSTU3: + org.hl7.fhir.dstu3.model.Person personDstu3 = (org.hl7.fhir.dstu3.model.Person) thePerson; + personDstu3.getIdentifier().removeIf(theIdentifier -> theIdentifier.getSystem().equalsIgnoreCase(myEmpiConfig.getEmpiRules().getEnterpriseEIDSystem())); + break; + default: + throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); + } + } + + private void addCanonicalEidsToPersonIfAbsent(IBaseResource thePerson, List theIncomingTargetEid) { + switch (myFhirContext.getVersion().getVersion()) { + case R4: + theIncomingTargetEid.forEach(eid -> addIdentifierIfAbsent((Person) thePerson, eid.toR4())); + break; + case DSTU3: + theIncomingTargetEid.forEach(eid -> addIdentifierIfAbsent((org.hl7.fhir.dstu3.model.Person) thePerson, eid.toDSTU3())); + break; + default: + throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); + } + } + + /** + * To avoid adding duplicate + * + * @param thePerson + * @param theIdentifier + */ + private void addIdentifierIfAbsent(org.hl7.fhir.dstu3.model.Person thePerson, org.hl7.fhir.dstu3.model.Identifier theIdentifier) { + Optional first = thePerson.getIdentifier().stream().filter(identifier -> identifier.getSystem().equals(theIdentifier.getSystem())).filter(identifier -> identifier.getValue().equals(theIdentifier.getValue())).findFirst(); + if (first.isPresent()) { + return; + } else { + thePerson.addIdentifier(theIdentifier); + } + } + + private void addIdentifierIfAbsent(Person thePerson, Identifier theIdentifier) { + Optional first = thePerson.getIdentifier().stream().filter(identifier -> identifier.getSystem().equals(theIdentifier.getSystem())).filter(identifier -> identifier.getValue().equals(theIdentifier.getValue())).findFirst(); + if (first.isPresent()) { + return; + } else { + thePerson.addIdentifier(theIdentifier); + } + } + + public void mergePersonFields(IBaseResource theFromPerson, IBaseResource theToPerson) { + switch (myFhirContext.getVersion().getVersion()) { + case R4: + mergeR4PersonFields(theFromPerson, theToPerson); + break; + case DSTU3: + mergeDstu3PersonFields(theFromPerson, theToPerson); + break; + default: + throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); + } + } + + private void mergeR4PersonFields(IBaseResource theFromPerson, IBaseResource theToPerson) { + Person fromPerson = (Person) theFromPerson; + Person toPerson = (Person) theToPerson; + + mergeElementList(fromPerson, toPerson, HumanName.class, Person::getName, HumanName::equalsDeep); + mergeElementList(fromPerson, toPerson, Identifier.class, Person::getIdentifier, Identifier::equalsDeep); + mergeElementList(fromPerson, toPerson, Address.class, Person::getAddress, Address::equalsDeep); + mergeElementList(fromPerson, toPerson, ContactPoint.class, Person::getTelecom, ContactPoint::equalsDeep); + if (!toPerson.hasBirthDate()) { + toPerson.setBirthDate(fromPerson.getBirthDate()); + } + if (!toPerson.hasGender()) { + toPerson.setGender(fromPerson.getGender()); + } + if (!toPerson.hasPhoto()) { + toPerson.setPhoto(fromPerson.getPhoto()); + } + } + + private void mergeElementList(P fromPerson, P toPerson, Class theBase, Function> theGetList, BiPredicate theEquals) { + List fromList = theGetList.apply(fromPerson); + List toList = theGetList.apply(toPerson); + List itemsToAdd = new ArrayList<>(); + + for (T fromItem : fromList) { + if (toList.stream().noneMatch(t -> theEquals.test(fromItem, t))) { + itemsToAdd.add(fromItem); + } + } + toList.addAll(itemsToAdd); + } + + private void mergeDstu3PersonFields(IBaseResource theFromPerson, IBaseResource theToPerson) { + org.hl7.fhir.dstu3.model.Person fromPerson = (org.hl7.fhir.dstu3.model.Person) theFromPerson; + org.hl7.fhir.dstu3.model.Person toPerson = (org.hl7.fhir.dstu3.model.Person) theToPerson; + + mergeElementList(fromPerson, toPerson, org.hl7.fhir.dstu3.model.HumanName.class, org.hl7.fhir.dstu3.model.Person::getName, org.hl7.fhir.dstu3.model.HumanName::equalsDeep); + mergeElementList(fromPerson, toPerson, org.hl7.fhir.dstu3.model.Identifier.class, org.hl7.fhir.dstu3.model.Person::getIdentifier, org.hl7.fhir.dstu3.model.Identifier::equalsDeep); + mergeElementList(fromPerson, toPerson, org.hl7.fhir.dstu3.model.Address.class, org.hl7.fhir.dstu3.model.Person::getAddress, org.hl7.fhir.dstu3.model.Address::equalsDeep); + mergeElementList(fromPerson, toPerson, org.hl7.fhir.dstu3.model.ContactPoint.class, org.hl7.fhir.dstu3.model.Person::getTelecom, org.hl7.fhir.dstu3.model.ContactPoint::equalsDeep); + + if (!toPerson.hasBirthDate()) { + toPerson.setBirthDate(fromPerson.getBirthDate()); + } + if (!toPerson.hasGender()) { + toPerson.setGender(fromPerson.getGender()); + } + if (!toPerson.hasPhoto()) { + toPerson.setPhoto(fromPerson.getPhoto()); + } + } + + /** + * An incoming resource is a potential duplicate if it matches a Patient that has a Person with an official EID, but + * the incoming resource also has an EID that does not match. + * + * @param theExistingPerson + * @param theComparingPerson + * @return + */ + public boolean isPotentialDuplicate(IAnyResource theExistingPerson, IAnyResource theComparingPerson) { + List externalEidsPerson = myEIDHelper.getExternalEid(theExistingPerson); + List externalEidsResource = myEIDHelper.getExternalEid(theComparingPerson); + return !externalEidsPerson.isEmpty() && !externalEidsResource.isEmpty() && !myEIDHelper.eidMatchExists(externalEidsResource, externalEidsPerson); + } + + public IBaseBackboneElement newPersonLink(IIdType theTargetId, CanonicalIdentityAssuranceLevel theAssuranceLevel) { + switch (myFhirContext.getVersion().getVersion()) { + case R4: + return newR4PersonLink(theTargetId, theAssuranceLevel); + case DSTU3: + return newDstu3PersonLink(theTargetId, theAssuranceLevel); + default: + throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); + } + } + + private IBaseBackboneElement newR4PersonLink(IIdType theTargetId, CanonicalIdentityAssuranceLevel theAssuranceLevel) { + Person.PersonLinkComponent retval = new Person.PersonLinkComponent(); + retval.setTarget(new Reference(theTargetId)); + retval.setAssurance(theAssuranceLevel.toR4()); + return retval; + } + + private IBaseBackboneElement newDstu3PersonLink(IIdType theTargetId, CanonicalIdentityAssuranceLevel theAssuranceLevel) { + org.hl7.fhir.dstu3.model.Person.PersonLinkComponent retval = new org.hl7.fhir.dstu3.model.Person.PersonLinkComponent(); + retval.setTarget(new org.hl7.fhir.dstu3.model.Reference(theTargetId)); + retval.setAssurance(theAssuranceLevel.toDstu3()); + return retval; + } + + public void setLinks(IAnyResource thePersonResource, List theNewLinks) { + switch (myFhirContext.getVersion().getVersion()) { + case R4: + setLinksR4(thePersonResource, theNewLinks); + break; + case DSTU3: + setLinksDstu3(thePersonResource, theNewLinks); + break; + default: + throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); + } + } + + private void setLinksDstu3(IAnyResource thePersonResource, List theLinks) { + org.hl7.fhir.dstu3.model.Person person = (org.hl7.fhir.dstu3.model.Person)thePersonResource; + List links = (List)(List)theLinks; + person.setLink(links); + } + + private void setLinksR4(IAnyResource thePersonResource, List theLinks) { + Person person = (Person)thePersonResource; + List links = (List)(List)theLinks; + person.setLink(links); + } + + public void updatePersonFromNewlyCreatedEmpiTarget(IBaseResource thePerson, IBaseResource theResource, EmpiTransactionContext theEmpiTransactionContext) { + copyEmpiTargetDataIntoPerson(theResource, thePerson, false); + } + + public void updatePersonFromUpdatedEmpiTarget(IBaseResource thePerson, IBaseResource theResource, EmpiTransactionContext theEmpiTransactionContext) { + copyEmpiTargetDataIntoPerson(theResource, thePerson, true); + } + + public int getLinkCount(IAnyResource thePerson) { + switch (myFhirContext.getVersion().getVersion()) { + case R4: + Person personR4 = (Person) thePerson; + return personR4.getLink().size(); + case DSTU3: + org.hl7.fhir.dstu3.model.Person personStu3 = (org.hl7.fhir.dstu3.model.Person) thePerson; + return personStu3.getLink().size(); + default: + throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); + } + } + + private void log(EmpiTransactionContext theEmpiTransactionContext, String theMessage) { + theEmpiTransactionContext.addTransactionLogMessage(theMessage); + ourLog.debug(theMessage); + } + + public void handleExternalEidAddition(IAnyResource thePerson, IAnyResource theResource, EmpiTransactionContext theEmpiTransactionContext) { + List eidFromResource = myEIDHelper.getExternalEid(theResource); + if (!eidFromResource.isEmpty()) { + updatePersonExternalEidFromEmpiTarget(thePerson, theResource, theEmpiTransactionContext); + } + } + + public void deactivatePerson(IAnyResource thePerson) { + switch (myFhirContext.getVersion().getVersion()) { + case R4: + Person personR4 = (Person) thePerson; + personR4.setActive(false); + break; + case DSTU3: + org.hl7.fhir.dstu3.model.Person personStu3 = (org.hl7.fhir.dstu3.model.Person) thePerson; + personStu3.setActive(false); + break; + default: + throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); + } + } + + public boolean isDeactivated(IBaseResource thePerson) { + switch (myFhirContext.getVersion().getVersion()) { + case R4: + Person personR4 = (Person) thePerson; + return !personR4.getActive(); + case DSTU3: + org.hl7.fhir.dstu3.model.Person personStu3 = (org.hl7.fhir.dstu3.model.Person) thePerson; + return !personStu3.getActive(); + default: + throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); + } + } +} diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/BaseR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/BaseR4Test.java similarity index 56% rename from hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/BaseR4Test.java rename to hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/BaseR4Test.java index 4c006cd3647..092225abfe2 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/BaseR4Test.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/BaseR4Test.java @@ -1,12 +1,12 @@ -package ca.uhn.fhir.mdm; +package ca.uhn.fhir.empi; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.MdmMatchOutcome; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.rules.config.MdmRuleValidator; -import ca.uhn.fhir.mdm.rules.config.MdmSettings; -import ca.uhn.fhir.mdm.rules.json.MdmRulesJson; -import ca.uhn.fhir.mdm.rules.svc.MdmResourceMatcherSvc; +import ca.uhn.fhir.empi.api.EmpiMatchOutcome; +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator; +import ca.uhn.fhir.empi.rules.config.EmpiSettings; +import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; +import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc; import ca.uhn.fhir.rest.server.util.ISearchParamRetriever; import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.extension.ExtendWith; @@ -34,21 +34,21 @@ public abstract class BaseR4Test { return patient; } - protected MdmResourceMatcherSvc buildMatcher(MdmRulesJson theMdmRulesJson) { - MdmResourceMatcherSvc retval = new MdmResourceMatcherSvc(ourFhirContext, new MdmSettings(new MdmRuleValidator(ourFhirContext, mySearchParamRetriever)).setMdmRules(theMdmRulesJson)); + protected EmpiResourceMatcherSvc buildMatcher(EmpiRulesJson theEmpiRulesJson) { + EmpiResourceMatcherSvc retval = new EmpiResourceMatcherSvc(ourFhirContext, new EmpiSettings(new EmpiRuleValidator(ourFhirContext, mySearchParamRetriever)).setEmpiRules(theEmpiRulesJson)); retval.init(); return retval; } - protected void assertMatch(MdmMatchResultEnum theExpectedMatchEnum, MdmMatchOutcome theMatchResult) { + protected void assertMatch(EmpiMatchResultEnum theExpectedMatchEnum, EmpiMatchOutcome theMatchResult) { assertEquals(theExpectedMatchEnum, theMatchResult.getMatchResultEnum()); } - protected void assertMatchResult(MdmMatchResultEnum theExpectedMatchEnum, long theExpectedVector, double theExpectedScore, boolean theExpectedNewGoldenResource, boolean theExpectedEidMatch, MdmMatchOutcome theMatchResult) { + protected void assertMatchResult(EmpiMatchResultEnum theExpectedMatchEnum, long theExpectedVector, double theExpectedScore, boolean theExpectedNewPerson, boolean theExpectedEidMatch, EmpiMatchOutcome theMatchResult) { assertEquals(theExpectedScore, theMatchResult.score, 0.001); assertEquals(theExpectedVector, theMatchResult.vector); assertEquals(theExpectedEidMatch, theMatchResult.isEidMatch()); - assertEquals(theExpectedNewGoldenResource, theMatchResult.isCreatedNewResource()); + assertEquals(theExpectedNewPerson, theMatchResult.isNewPerson()); assertEquals(theExpectedMatchEnum, theMatchResult.getMatchResultEnum()); } } diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/api/MdmMatchOutcomeTest.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/api/EmpiMatchOutcomeTest.java similarity index 56% rename from hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/api/MdmMatchOutcomeTest.java rename to hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/api/EmpiMatchOutcomeTest.java index 32a6feee981..f8c5c34865a 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/api/MdmMatchOutcomeTest.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/api/EmpiMatchOutcomeTest.java @@ -1,24 +1,24 @@ -package ca.uhn.fhir.mdm.api; +package ca.uhn.fhir.empi.api; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; -class MdmMatchOutcomeTest { +class EmpiMatchOutcomeTest { public static final double DELTA = 0.0001; @Test void testNormalizedScore() { - MdmMatchOutcome outcome = new MdmMatchOutcome(0l, 0.0); + EmpiMatchOutcome outcome = new EmpiMatchOutcome(0l, 0.0); assertEquals(0.0, outcome.getNormalizedScore()); - outcome = new MdmMatchOutcome(null, 10.0); - outcome.setMdmRuleCount(10); + outcome = new EmpiMatchOutcome(null, 10.0); + outcome.setEmpiRuleCount(10); assertEquals(1.0, outcome.getNormalizedScore(), DELTA); - outcome = new MdmMatchOutcome(null, 2.0); - outcome.setMdmRuleCount(3); + outcome = new EmpiMatchOutcome(null, 2.0); + outcome.setEmpiRuleCount(3); assertEquals(2.0 / 3.0, outcome.getNormalizedScore(), DELTA); } diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/config/MdmRuleValidatorTest.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/config/EmpiRuleValidatorTest.java similarity index 52% rename from hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/config/MdmRuleValidatorTest.java rename to hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/config/EmpiRuleValidatorTest.java index f12ba8c35ae..9c5ab997cdd 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/config/MdmRuleValidatorTest.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/config/EmpiRuleValidatorTest.java @@ -1,11 +1,9 @@ -package ca.uhn.fhir.mdm.rules.config; +package ca.uhn.fhir.empi.rules.config; import ca.uhn.fhir.context.ConfigurationException; -import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.mdm.BaseR4Test; +import ca.uhn.fhir.empi.BaseR4Test; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; @@ -16,25 +14,12 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class MdmRuleValidatorTest extends BaseR4Test { - - @BeforeEach - public void before() { - when(mySearchParamRetriever.getActiveSearchParam("Patient", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); - when(mySearchParamRetriever.getActiveSearchParam("Practitioner", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); - when(mySearchParamRetriever.getActiveSearchParam("Medication", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); - when(mySearchParamRetriever.getActiveSearchParam("AllergyIntolerance", "identifier")).thenReturn(null); - when(mySearchParamRetriever.getActiveSearchParam("Organization", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); - when(mySearchParamRetriever.getActiveSearchParam("Organization", "active")).thenReturn(mock(RuntimeSearchParam.class)); - } +public class EmpiRuleValidatorTest extends BaseR4Test { @Test public void testValidate() throws IOException { try { - setMdmRuleJson("bad-rules-bad-url.json"); + setEmpiRuleJson("bad-rules-bad-url.json"); fail(); } catch (ConfigurationException e){ assertThat(e.getMessage(), is("Enterprise Identifier System (eidSystem) must be a valid URI")); @@ -44,7 +29,7 @@ public class MdmRuleValidatorTest extends BaseR4Test { @Test public void testNonExistentMatchField() throws IOException { try { - setMdmRuleJson("bad-rules-missing-name.json"); + setEmpiRuleJson("bad-rules-missing-name.json"); fail(); } catch (ConfigurationException e) { assertThat(e.getMessage(), is("There is no matchField with name foo")); @@ -54,7 +39,7 @@ public class MdmRuleValidatorTest extends BaseR4Test { @Test public void testSimilarityHasThreshold() throws IOException { try { - setMdmRuleJson("bad-rules-missing-threshold.json"); + setEmpiRuleJson("bad-rules-missing-threshold.json"); fail(); } catch (ConfigurationException e) { assertThat(e.getMessage(), is("MatchField given-name similarity COSINE requires a matchThreshold")); @@ -64,7 +49,7 @@ public class MdmRuleValidatorTest extends BaseR4Test { @Test public void testMatcherBadPath() throws IOException { try { - setMdmRuleJson("bad-rules-bad-path.json"); + setEmpiRuleJson("bad-rules-bad-path.json"); fail(); } catch (ConfigurationException e) { assertThat(e.getMessage(), startsWith("MatchField given-name resourceType Patient has invalid path 'name.first'. Unknown child name 'first' in element HumanName")); @@ -74,7 +59,7 @@ public class MdmRuleValidatorTest extends BaseR4Test { @Test public void testMatcherBadSearchParam() throws IOException { try { - setMdmRuleJson("bad-rules-bad-searchparam.json"); + setEmpiRuleJson("bad-rules-bad-searchparam.json"); fail(); } catch (ConfigurationException e) { assertThat(e.getMessage(), startsWith("Error in candidateSearchParams: Patient does not have a search parameter called 'foo'")); @@ -84,50 +69,31 @@ public class MdmRuleValidatorTest extends BaseR4Test { @Test public void testMatcherBadFilter() throws IOException { try { - setMdmRuleJson("bad-rules-bad-filter.json"); + setEmpiRuleJson("bad-rules-bad-filter.json"); fail(); } catch (ConfigurationException e) { assertThat(e.getMessage(), startsWith("Error in candidateFilterSearchParams: Patient does not have a search parameter called 'foo'")); } } - @Test - public void testInvalidMdmType() throws IOException { - try { - setMdmRuleJson("bad-rules-missing-mdm-types.json"); - fail(); - } catch (ConfigurationException e) { - assertThat(e.getMessage(), startsWith("mdmTypes must be set to a list of resource types.")); - } - } - @Test public void testMatcherduplicateName() throws IOException { try { - setMdmRuleJson("bad-rules-duplicate-name.json"); + setEmpiRuleJson("bad-rules-duplicate-name.json"); fail(); } catch (ConfigurationException e) { assertThat(e.getMessage(), startsWith("Two MatchFields have the same name 'foo'")); } } - @Test - public void testInvalidPath() throws IOException { - try { - setMdmRuleJson("bad-rules-invalid-path.json"); - fail(); - } catch (ConfigurationException e) { - assertThat(e.getMessage(), startsWith("MatchField name-prefix resourceType Organization has invalid path")); - } - } - private void setMdmRuleJson(String theTheS) throws IOException { - MdmRuleValidator mdmRuleValidator = new MdmRuleValidator(ourFhirContext, mySearchParamRetriever); - MdmSettings mdmSettings = new MdmSettings(mdmRuleValidator); + private void setEmpiRuleJson(String theTheS) throws IOException { + EmpiRuleValidator empiRuleValidator = new EmpiRuleValidator(ourFhirContext, mySearchParamRetriever); + EmpiSettings empiSettings = new EmpiSettings(empiRuleValidator); DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); Resource resource = resourceLoader.getResource(theTheS); String json = IOUtils.toString(resource.getInputStream(), Charsets.UTF_8); - mdmSettings.setScriptText(json); + empiSettings.setScriptText(json); } } diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/json/MdmRulesJsonR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/json/EmpiRulesJsonR4Test.java similarity index 70% rename from hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/json/MdmRulesJsonR4Test.java rename to hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/json/EmpiRulesJsonR4Test.java index 90677c5c2a8..a977d9ccb0d 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/json/MdmRulesJsonR4Test.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/json/EmpiRulesJsonR4Test.java @@ -1,9 +1,9 @@ -package ca.uhn.fhir.mdm.rules.json; +package ca.uhn.fhir.empi.rules.json; import ca.uhn.fhir.context.ConfigurationException; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.rules.similarity.MdmSimilarityEnum; -import ca.uhn.fhir.mdm.rules.svc.BaseMdmRulesR4Test; +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import ca.uhn.fhir.empi.rules.similarity.EmpiSimilarityEnum; +import ca.uhn.fhir.empi.rules.svc.BaseEmpiRulesR4Test; import ca.uhn.fhir.util.JsonUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -17,9 +17,9 @@ import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; -public class MdmRulesJsonR4Test extends BaseMdmRulesR4Test { - private static final Logger ourLog = LoggerFactory.getLogger(MdmRulesJsonR4Test.class); - private MdmRulesJson myRules; +public class EmpiRulesJsonR4Test extends BaseEmpiRulesR4Test { + private static final Logger ourLog = LoggerFactory.getLogger(EmpiRulesJsonR4Test.class); + private EmpiRulesJson myRules; @Override @BeforeEach @@ -31,7 +31,7 @@ public class MdmRulesJsonR4Test extends BaseMdmRulesR4Test { @Test public void testValidate() throws IOException { - MdmRulesJson rules = new MdmRulesJson(); + EmpiRulesJson rules = new EmpiRulesJson(); try { JsonUtil.serialize(rules); } catch (NullPointerException e) { @@ -43,17 +43,17 @@ public class MdmRulesJsonR4Test extends BaseMdmRulesR4Test { public void testSerDeser() throws IOException { String json = JsonUtil.serialize(myRules); ourLog.info(json); - MdmRulesJson rulesDeser = JsonUtil.deserialize(json, MdmRulesJson.class); + EmpiRulesJson rulesDeser = JsonUtil.deserialize(json, EmpiRulesJson.class); assertEquals(2, rulesDeser.size()); - assertEquals(MdmMatchResultEnum.MATCH, rulesDeser.getMatchResult(myBothNameFields)); - MdmFieldMatchJson second = rulesDeser.get(1); + assertEquals(EmpiMatchResultEnum.MATCH, rulesDeser.getMatchResult(myBothNameFields)); + EmpiFieldMatchJson second = rulesDeser.get(1); assertEquals("name.family", second.getResourcePath()); - assertEquals(MdmSimilarityEnum.JARO_WINKLER, second.getSimilarity().getAlgorithm()); + assertEquals(EmpiSimilarityEnum.JARO_WINKLER, second.getSimilarity().getAlgorithm()); } @Test public void testMatchResultMap() { - assertEquals(MdmMatchResultEnum.MATCH, myRules.getMatchResult(3L)); + assertEquals(EmpiMatchResultEnum.MATCH, myRules.getMatchResult(3L)); } @Test diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/json/VectorMatchResultMapTest.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/json/VectorMatchResultMapTest.java new file mode 100644 index 00000000000..c5cb95df4c8 --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/json/VectorMatchResultMapTest.java @@ -0,0 +1,42 @@ +package ca.uhn.fhir.empi.rules.json; + +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import ca.uhn.fhir.empi.rules.matcher.EmpiMatcherEnum; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class VectorMatchResultMapTest { + @Test + public void splitFieldMatchNames() { + { + String[] result = VectorMatchResultMap.splitFieldMatchNames("a,b"); + assertEquals(2, result.length); + assertEquals("a", result[0]); + assertEquals("b", result[1]); + } + + { + String[] result = VectorMatchResultMap.splitFieldMatchNames("a, b"); + assertEquals(2, result.length); + assertEquals("a", result[0]); + assertEquals("b", result[1]); + } + } + + @Test + public void testMatchBeforePossibleMatch() { + EmpiRulesJson empiRulesJson = new EmpiRulesJson(); + EmpiMatcherJson matcherJson = new EmpiMatcherJson().setAlgorithm(EmpiMatcherEnum.STRING); + empiRulesJson.addMatchField(new EmpiFieldMatchJson().setName("given").setResourceType("Patient").setResourcePath("name.given").setMatcher(matcherJson)); + empiRulesJson.addMatchField(new EmpiFieldMatchJson().setName("family").setResourceType("Patient").setResourcePath("name.family").setMatcher(matcherJson)); + empiRulesJson.addMatchField(new EmpiFieldMatchJson().setName("prefix").setResourceType("Patient").setResourcePath("name.prefix").setMatcher(matcherJson)); + empiRulesJson.putMatchResult("given,family", EmpiMatchResultEnum.MATCH); + empiRulesJson.putMatchResult("given", EmpiMatchResultEnum.POSSIBLE_MATCH); + + VectorMatchResultMap map = new VectorMatchResultMap(empiRulesJson); + assertEquals(EmpiMatchResultEnum.POSSIBLE_MATCH, map.get(1L)); + assertEquals(EmpiMatchResultEnum.MATCH, map.get(3L)); + assertEquals(EmpiMatchResultEnum.MATCH, map.get(7L)); + } +} diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/BaseMatcherR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/BaseMatcherR4Test.java similarity index 80% rename from hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/BaseMatcherR4Test.java rename to hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/BaseMatcherR4Test.java index f6243442212..baabaedbb4c 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/BaseMatcherR4Test.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/BaseMatcherR4Test.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; import ca.uhn.fhir.context.FhirContext; diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/DateMatcherR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/DateMatcherR4Test.java similarity index 92% rename from hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/DateMatcherR4Test.java rename to hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/DateMatcherR4Test.java index b427777a96d..7ec3f201f9f 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/DateMatcherR4Test.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/DateMatcherR4Test.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import org.hl7.fhir.r4.model.DateTimeType; @@ -43,7 +43,7 @@ public class DateMatcherR4Test extends BaseMatcherR4Test { } private boolean dateMatch(Date theDate, Date theSameMonth, TemporalPrecisionEnum theTheDay) { - return MdmMatcherEnum.DATE.match(ourFhirContext, new DateType(theDate, theTheDay), new DateType(theSameMonth, theTheDay), true, null); + return EmpiMatcherEnum.DATE.match(ourFhirContext, new DateType(theDate, theTheDay), new DateType(theSameMonth, theTheDay), true, null); } @Test @@ -83,6 +83,6 @@ public class DateMatcherR4Test extends BaseMatcherR4Test { } private boolean dateTimeMatch(Date theDate, Date theSameSecond, TemporalPrecisionEnum theTheDay, TemporalPrecisionEnum theTheDay2) { - return MdmMatcherEnum.DATE.match(ourFhirContext, new DateTimeType(theDate, theTheDay), new DateTimeType(theSameSecond, theTheDay2), true, null); + return EmpiMatcherEnum.DATE.match(ourFhirContext, new DateTimeType(theDate, theTheDay), new DateTimeType(theSameSecond, theTheDay2), true, null); } } diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/IdentifierMatcherR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/IdentifierMatcherR4Test.java similarity index 73% rename from hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/IdentifierMatcherR4Test.java rename to hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/IdentifierMatcherR4Test.java index 5e6758af0a0..29930bf60bb 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/IdentifierMatcherR4Test.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/IdentifierMatcherR4Test.java @@ -1,7 +1,7 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; -import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson; -import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson; +import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson; +import ca.uhn.fhir.empi.rules.json.EmpiMatcherJson; import org.hl7.fhir.r4.model.Identifier; import org.junit.jupiter.api.Test; @@ -19,8 +19,8 @@ public class IdentifierMatcherR4Test extends BaseMatcherR4Test { Identifier left = new Identifier().setSystem(MATCHING_SYSTEM).setValue(MATCHING_VALUE); Identifier right = new Identifier().setSystem(MATCHING_SYSTEM).setValue(MATCHING_VALUE); - MdmMatcherJson matcher = new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.IDENTIFIER); - MdmFieldMatchJson fieldMatch = new MdmFieldMatchJson().setMatcher(matcher); + EmpiMatcherJson matcher = new EmpiMatcherJson().setAlgorithm(EmpiMatcherEnum.IDENTIFIER); + EmpiFieldMatchJson fieldMatch = new EmpiFieldMatchJson().setMatcher(matcher); assertTrue(fieldMatch.match(ourFhirContext, left, right).match); } @@ -33,8 +33,8 @@ public class IdentifierMatcherR4Test extends BaseMatcherR4Test { Identifier rightNoSystem = new Identifier().setValue(MATCHING_VALUE); Identifier rightNoValue = new Identifier().setSystem(MATCHING_SYSTEM); - MdmMatcherJson matcher = new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.IDENTIFIER); - MdmFieldMatchJson fieldMatch = new MdmFieldMatchJson().setMatcher(matcher); + EmpiMatcherJson matcher = new EmpiMatcherJson().setAlgorithm(EmpiMatcherEnum.IDENTIFIER); + EmpiFieldMatchJson fieldMatch = new EmpiFieldMatchJson().setMatcher(matcher); assertFalse(fieldMatch.match(ourFhirContext, left, rightWrongSystem).match); assertFalse(fieldMatch.match(ourFhirContext, left, rightWrongValue).match); @@ -51,8 +51,8 @@ public class IdentifierMatcherR4Test extends BaseMatcherR4Test { Identifier left = new Identifier().setSystem(MATCHING_SYSTEM).setValue(MATCHING_VALUE); Identifier right = new Identifier().setSystem(MATCHING_SYSTEM).setValue(MATCHING_VALUE); - MdmMatcherJson matcher = new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.IDENTIFIER).setIdentifierSystem(MATCHING_SYSTEM); - MdmFieldMatchJson fieldMatch = new MdmFieldMatchJson().setMatcher(matcher); + EmpiMatcherJson matcher = new EmpiMatcherJson().setAlgorithm(EmpiMatcherEnum.IDENTIFIER).setIdentifierSystem(MATCHING_SYSTEM); + EmpiFieldMatchJson fieldMatch = new EmpiFieldMatchJson().setMatcher(matcher); assertTrue(fieldMatch.match(ourFhirContext, left, right).match); } @@ -62,8 +62,8 @@ public class IdentifierMatcherR4Test extends BaseMatcherR4Test { Identifier left = new Identifier().setSystem(OTHER_SYSTEM).setValue(MATCHING_VALUE); Identifier right = new Identifier().setSystem(OTHER_SYSTEM).setValue(MATCHING_VALUE); - MdmMatcherJson matcher = new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.IDENTIFIER).setIdentifierSystem(MATCHING_SYSTEM); - MdmFieldMatchJson fieldMatch = new MdmFieldMatchJson().setMatcher(matcher); + EmpiMatcherJson matcher = new EmpiMatcherJson().setAlgorithm(EmpiMatcherEnum.IDENTIFIER).setIdentifierSystem(MATCHING_SYSTEM); + EmpiFieldMatchJson fieldMatch = new EmpiFieldMatchJson().setMatcher(matcher); assertFalse(fieldMatch.match(ourFhirContext, left, right).match); } diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/StringMatcherR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/StringMatcherR4Test.java new file mode 100644 index 00000000000..64449c9cc76 --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/StringMatcherR4Test.java @@ -0,0 +1,154 @@ +package ca.uhn.fhir.empi.rules.matcher; + +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.Enumeration; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.StringType; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class StringMatcherR4Test extends BaseMatcherR4Test { + private static final Logger ourLog = LoggerFactory.getLogger(StringMatcherR4Test.class); + public static final String LEFT = "namadega"; + public static final String RIGHT = "namaedga"; + + @Test + public void testNamadega() { + assertTrue(match(EmpiMatcherEnum.COLOGNE, LEFT, RIGHT)); + assertTrue(match(EmpiMatcherEnum.DOUBLE_METAPHONE, LEFT, RIGHT)); + assertTrue(match(EmpiMatcherEnum.MATCH_RATING_APPROACH, LEFT, RIGHT)); + assertTrue(match(EmpiMatcherEnum.METAPHONE, LEFT, RIGHT)); + assertTrue(match(EmpiMatcherEnum.SOUNDEX, LEFT, RIGHT)); + assertTrue(match(EmpiMatcherEnum.METAPHONE, LEFT, RIGHT)); + + assertFalse(match(EmpiMatcherEnum.CAVERPHONE1, LEFT, RIGHT)); + assertFalse(match(EmpiMatcherEnum.CAVERPHONE2, LEFT, RIGHT)); + assertFalse(match(EmpiMatcherEnum.NYSIIS, LEFT, RIGHT)); + assertFalse(match(EmpiMatcherEnum.REFINED_SOUNDEX, LEFT, RIGHT)); + assertFalse(match(EmpiMatcherEnum.STRING, LEFT, RIGHT)); + assertFalse(match(EmpiMatcherEnum.SUBSTRING, LEFT, RIGHT)); + } + + @Test + public void testMetaphone() { + assertTrue(match(EmpiMatcherEnum.METAPHONE, "Durie", "dury")); + assertTrue(match(EmpiMatcherEnum.METAPHONE, "Balo", "ballo")); + assertTrue(match(EmpiMatcherEnum.METAPHONE, "Hans Peter", "Hanspeter")); + assertTrue(match(EmpiMatcherEnum.METAPHONE, "Lawson", "Law son")); + + assertFalse(match(EmpiMatcherEnum.METAPHONE, "Allsop", "Allsob")); + assertFalse(match(EmpiMatcherEnum.METAPHONE, "Gevne", "Geve")); + assertFalse(match(EmpiMatcherEnum.METAPHONE, "Bruce", "Bruch")); + assertFalse(match(EmpiMatcherEnum.METAPHONE, "Smith", "Schmidt")); + assertFalse(match(EmpiMatcherEnum.METAPHONE, "Jyothi", "Jyoti")); + } + + @Test + public void testDoubleMetaphone() { + assertTrue(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Durie", "dury")); + assertTrue(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Balo", "ballo")); + assertTrue(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Hans Peter", "Hanspeter")); + assertTrue(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Lawson", "Law son")); + assertTrue(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Allsop", "Allsob")); + + assertFalse(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Gevne", "Geve")); + assertFalse(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Bruce", "Bruch")); + assertFalse(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Smith", "Schmidt")); + assertFalse(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Jyothi", "Jyoti")); + } + + @Test + public void testNormalizeCase() { + assertTrue(match(EmpiMatcherEnum.STRING, "joe", "JoE")); + assertTrue(match(EmpiMatcherEnum.STRING, "MCTAVISH", "McTavish")); + + assertFalse(match(EmpiMatcherEnum.STRING, "joey", "joe")); + assertFalse(match(EmpiMatcherEnum.STRING, "joe", "joey")); + } + + @Test + public void testExactString() { + assertTrue(EmpiMatcherEnum.STRING.match(ourFhirContext, new StringType("Jilly"), new StringType("Jilly"), true, null)); + + assertFalse(EmpiMatcherEnum.STRING.match(ourFhirContext, new StringType("MCTAVISH"), new StringType("McTavish"), true, null)); + assertFalse(EmpiMatcherEnum.STRING.match(ourFhirContext, new StringType("Durie"), new StringType("dury"), true, null)); + } + + @Test + public void testExactBoolean() { + assertTrue(EmpiMatcherEnum.STRING.match(ourFhirContext, new BooleanType(true), new BooleanType(true), true, null)); + + assertFalse(EmpiMatcherEnum.STRING.match(ourFhirContext, new BooleanType(true), new BooleanType(false), true, null)); + assertFalse(EmpiMatcherEnum.STRING.match(ourFhirContext, new BooleanType(false), new BooleanType(true), true, null)); + } + + @Test + public void testExactDateString() { + assertTrue(EmpiMatcherEnum.STRING.match(ourFhirContext, new DateType("1965-08-09"), new DateType("1965-08-09"), true, null)); + + assertFalse(EmpiMatcherEnum.STRING.match(ourFhirContext, new DateType("1965-08-09"), new DateType("1965-09-08"), true, null)); + } + + + @Test + public void testExactGender() { + Enumeration male = new Enumeration(new Enumerations.AdministrativeGenderEnumFactory()); + male.setValue(Enumerations.AdministrativeGender.MALE); + + Enumeration female = new Enumeration(new Enumerations.AdministrativeGenderEnumFactory()); + female.setValue(Enumerations.AdministrativeGender.FEMALE); + + assertTrue(EmpiMatcherEnum.STRING.match(ourFhirContext, male, male, true, null)); + + assertFalse(EmpiMatcherEnum.STRING.match(ourFhirContext, male, female, true, null)); + } + + @Test + public void testSoundex() { + assertTrue(match(EmpiMatcherEnum.SOUNDEX, "Gail", "Gale")); + assertTrue(match(EmpiMatcherEnum.SOUNDEX, "John", "Jon")); + assertTrue(match(EmpiMatcherEnum.SOUNDEX, "Thom", "Tom")); + + assertFalse(match(EmpiMatcherEnum.SOUNDEX, "Fred", "Frank")); + assertFalse(match(EmpiMatcherEnum.SOUNDEX, "Thomas", "Tom")); + } + + + @Test + public void testCaverphone1() { + assertTrue(match(EmpiMatcherEnum.CAVERPHONE1, "Gail", "Gael")); + assertTrue(match(EmpiMatcherEnum.CAVERPHONE1, "John", "Jon")); + + assertFalse(match(EmpiMatcherEnum.CAVERPHONE1, "Gail", "Gale")); + assertFalse(match(EmpiMatcherEnum.CAVERPHONE1, "Fred", "Frank")); + assertFalse(match(EmpiMatcherEnum.CAVERPHONE1, "Thomas", "Tom")); + } + + @Test + public void testCaverphone2() { + assertTrue(match(EmpiMatcherEnum.CAVERPHONE2, "Gail", "Gael")); + assertTrue(match(EmpiMatcherEnum.CAVERPHONE2, "John", "Jon")); + assertTrue(match(EmpiMatcherEnum.CAVERPHONE2, "Gail", "Gale")); + + assertFalse(match(EmpiMatcherEnum.CAVERPHONE2, "Fred", "Frank")); + assertFalse(match(EmpiMatcherEnum.CAVERPHONE2, "Thomas", "Tom")); + } + + @Test + public void testNormalizeSubstring() { + assertTrue(match(EmpiMatcherEnum.SUBSTRING, "BILLY", "Bill")); + assertTrue(match(EmpiMatcherEnum.SUBSTRING, "Bill", "Billy")); + assertTrue(match(EmpiMatcherEnum.SUBSTRING, "FRED", "Frederik")); + + assertFalse(match(EmpiMatcherEnum.SUBSTRING, "Fred", "Friederik")); + } + + private boolean match(EmpiMatcherEnum theMatcher, String theLeft, String theRight) { + return theMatcher.match(ourFhirContext, new StringType(theLeft), new StringType(theRight), false, null); + } +} diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/StringSimilarityR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/StringSimilarityR4Test.java similarity index 85% rename from hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/StringSimilarityR4Test.java rename to hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/StringSimilarityR4Test.java index c806be9b99e..fe53d399043 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/StringSimilarityR4Test.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/matcher/StringSimilarityR4Test.java @@ -1,7 +1,7 @@ -package ca.uhn.fhir.mdm.rules.matcher; +package ca.uhn.fhir.empi.rules.matcher; -import ca.uhn.fhir.mdm.rules.similarity.HapiStringSimilarity; -import ca.uhn.fhir.mdm.rules.similarity.IMdmFieldSimilarity; +import ca.uhn.fhir.empi.rules.similarity.HapiStringSimilarity; +import ca.uhn.fhir.empi.rules.similarity.IEmpiFieldSimilarity; import info.debatty.java.stringsimilarity.Cosine; import info.debatty.java.stringsimilarity.Jaccard; import info.debatty.java.stringsimilarity.JaroWinkler; @@ -32,7 +32,7 @@ public class StringSimilarityR4Test extends BaseMatcherR4Test { ourLog.info("" + similarity(SORENSEN_DICE, LEFT, RIGHT)); } - private double similarity(IMdmFieldSimilarity theSimilarity, String theLeft, String theRight) { + private double similarity(IEmpiFieldSimilarity theSimilarity, String theLeft, String theRight) { return theSimilarity.similarity(ourFhirContext, new StringType(theLeft), new StringType(theRight), false); } } diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/BaseEmpiRulesR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/BaseEmpiRulesR4Test.java new file mode 100644 index 00000000000..6d9e321564d --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/BaseEmpiRulesR4Test.java @@ -0,0 +1,63 @@ +package ca.uhn.fhir.empi.rules.svc; + +import ca.uhn.fhir.empi.BaseR4Test; +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson; +import ca.uhn.fhir.empi.rules.json.EmpiFilterSearchParamJson; +import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson; +import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; +import ca.uhn.fhir.empi.rules.json.EmpiSimilarityJson; +import ca.uhn.fhir.empi.rules.similarity.EmpiSimilarityEnum; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.BeforeEach; + +public abstract class BaseEmpiRulesR4Test extends BaseR4Test { + public static final String PATIENT_GIVEN = "patient-given"; + public static final String PATIENT_FAMILY = "patient-last"; + + public static final double NAME_THRESHOLD = 0.8; + protected EmpiFieldMatchJson myGivenNameMatchField; + protected String myBothNameFields; + + @BeforeEach + public void before() { + myGivenNameMatchField = new EmpiFieldMatchJson() + .setName(PATIENT_GIVEN) + .setResourceType("Patient") + .setResourcePath("name.given") + .setSimilarity(new EmpiSimilarityJson().setAlgorithm(EmpiSimilarityEnum.COSINE).setMatchThreshold(NAME_THRESHOLD)); + myBothNameFields = String.join(",", PATIENT_GIVEN, PATIENT_FAMILY); + } + + protected EmpiRulesJson buildActiveBirthdateIdRules() { + EmpiFilterSearchParamJson activePatientsBlockingFilter = new EmpiFilterSearchParamJson() + .setResourceType("Patient") + .setSearchParam(Patient.SP_ACTIVE) + .setFixedValue("true"); + + EmpiResourceSearchParamJson patientBirthdayBlocking = new EmpiResourceSearchParamJson() + .setResourceType("Patient") + .addSearchParam(Patient.SP_BIRTHDATE); + EmpiResourceSearchParamJson patientIdentifierBlocking = new EmpiResourceSearchParamJson() + .setResourceType("Patient") + .addSearchParam(Patient.SP_IDENTIFIER); + + + EmpiFieldMatchJson lastNameMatchField = new EmpiFieldMatchJson() + .setName(PATIENT_FAMILY) + .setResourceType("Patient") + .setResourcePath("name.family") + .setSimilarity(new EmpiSimilarityJson().setAlgorithm(EmpiSimilarityEnum.JARO_WINKLER).setMatchThreshold(NAME_THRESHOLD)); + + EmpiRulesJson retval = new EmpiRulesJson(); + retval.setVersion("test version"); + retval.addResourceSearchParam(patientBirthdayBlocking); + retval.addResourceSearchParam(patientIdentifierBlocking); + retval.addFilterSearchParam(activePatientsBlockingFilter); + retval.addMatchField(myGivenNameMatchField); + retval.addMatchField(lastNameMatchField); + retval.putMatchResult(myBothNameFields, EmpiMatchResultEnum.MATCH); + retval.putMatchResult(PATIENT_GIVEN, EmpiMatchResultEnum.POSSIBLE_MATCH); + return retval; + } +} diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/CustomResourceMatcherR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/CustomResourceMatcherR4Test.java new file mode 100644 index 00000000000..f46c23dbc3d --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/CustomResourceMatcherR4Test.java @@ -0,0 +1,116 @@ +package ca.uhn.fhir.empi.rules.svc; + +import ca.uhn.fhir.empi.BaseR4Test; +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson; +import ca.uhn.fhir.empi.rules.json.EmpiMatcherJson; +import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; +import ca.uhn.fhir.empi.rules.matcher.EmpiMatcherEnum; +import org.hl7.fhir.r4.model.HumanName; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class CustomResourceMatcherR4Test extends BaseR4Test { + + public static final String FIELD_EXACT_MATCH_NAME = EmpiMatcherEnum.NAME_ANY_ORDER.name(); + private static Patient ourJohnHenry; + private static Patient ourJohnHENRY; + private static Patient ourJaneHenry; + private static Patient ourJohnSmith; + private static Patient ourJohnBillyHenry; + private static Patient ourBillyJohnHenry; + private static Patient ourHenryJohn; + private static Patient ourHenryJOHN; + + @Test + public void testExactNameAnyOrder() { + EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMatcherEnum.NAME_ANY_ORDER, true)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry)); + } + + @Test + public void testNormalizedNameAnyOrder() { + EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMatcherEnum.NAME_ANY_ORDER, false)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry)); + } + + @Test + public void testExactNameFirstAndLast() { + EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMatcherEnum.NAME_FIRST_AND_LAST, true)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry)); + assertMatchResult(EmpiMatchResultEnum.MATCH, 1L, 1.0, false, false, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry)); + } + + @Test + public void testNormalizedNameFirstAndLast() { + EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMatcherEnum.NAME_FIRST_AND_LAST, false)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry)); + assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry)); + assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry)); + } + + private EmpiRulesJson buildNameRules(EmpiMatcherEnum theAlgorithm, boolean theExact) { + EmpiMatcherJson matcherJson = new EmpiMatcherJson().setAlgorithm(theAlgorithm).setExact(theExact); + EmpiFieldMatchJson nameAnyOrderFieldMatch = new EmpiFieldMatchJson() + .setName(FIELD_EXACT_MATCH_NAME) + .setResourceType("Patient") + .setResourcePath("name") + .setMatcher(matcherJson); + + EmpiRulesJson retval = new EmpiRulesJson(); + retval.addMatchField(nameAnyOrderFieldMatch); + retval.putMatchResult(FIELD_EXACT_MATCH_NAME, EmpiMatchResultEnum.MATCH); + + return retval; + } + + @BeforeAll + public static void beforeClass() { + ourJohnHenry = buildPatientWithNames("Henry", "John"); + ourJohnHENRY = buildPatientWithNames("HENRY", "John"); + ourJaneHenry = buildPatientWithNames("Henry", "Jane"); + ourJohnSmith = buildPatientWithNames("Smith", "John"); + ourJohnBillyHenry = buildPatientWithNames("Henry", "John", "Billy"); + ourBillyJohnHenry = buildPatientWithNames("Henry", "Billy", "John"); + ourHenryJohn = buildPatientWithNames("John", "Henry"); + ourHenryJOHN = buildPatientWithNames("JOHN", "Henry"); + } + + protected static Patient buildPatientWithNames(String theFamilyName, String... theGivenNames) { + Patient patient = new Patient(); + HumanName name = patient.addName(); + name.setFamily(theFamilyName); + for (String givenName : theGivenNames) { + name.addGiven(givenName); + } + patient.setId("Patient/1"); + return patient; + } +} diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/MdmResourceFieldMatcherR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldMatcherR4Test.java similarity index 71% rename from hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/MdmResourceFieldMatcherR4Test.java rename to hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldMatcherR4Test.java index bfff9f04056..804a04a5073 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/MdmResourceFieldMatcherR4Test.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldMatcherR4Test.java @@ -1,8 +1,8 @@ -package ca.uhn.fhir.mdm.rules.svc; +package ca.uhn.fhir.empi.rules.svc; -import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson; -import ca.uhn.fhir.mdm.rules.json.MdmSimilarityJson; -import ca.uhn.fhir.mdm.rules.similarity.MdmSimilarityEnum; +import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson; +import ca.uhn.fhir.empi.rules.json.EmpiSimilarityJson; +import ca.uhn.fhir.empi.rules.similarity.EmpiSimilarityEnum; import ca.uhn.fhir.parser.DataFormatException; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Patient; @@ -16,8 +16,8 @@ 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 MdmResourceFieldMatcherR4Test extends BaseMdmRulesR4Test { - protected MdmResourceFieldMatcher myComparator; +public class EmpiResourceFieldMatcherR4Test extends BaseEmpiRulesR4Test { + protected EmpiResourceFieldMatcher myComparator; private Patient myJohn; private Patient myJohny; @@ -26,7 +26,8 @@ public class MdmResourceFieldMatcherR4Test extends BaseMdmRulesR4Test { public void before() { super.before(); - myComparator = new MdmResourceFieldMatcher(ourFhirContext, myGivenNameMatchField, myMdmRulesJson); + + myComparator = new EmpiResourceFieldMatcher(ourFhirContext, myGivenNameMatchField); myJohn = buildJohn(); myJohny = buildJohny(); } @@ -61,12 +62,12 @@ public class MdmResourceFieldMatcherR4Test extends BaseMdmRulesR4Test { @Test public void testBadPath() { try { - MdmFieldMatchJson matchField = new MdmFieldMatchJson() + EmpiFieldMatchJson matchField = new EmpiFieldMatchJson() .setName("patient-foo") .setResourceType("Patient") .setResourcePath("foo") - .setSimilarity(new MdmSimilarityJson().setAlgorithm(MdmSimilarityEnum.COSINE).setMatchThreshold(NAME_THRESHOLD)); - MdmResourceFieldMatcher comparator = new MdmResourceFieldMatcher(ourFhirContext, matchField, myMdmRulesJson); + .setSimilarity(new EmpiSimilarityJson().setAlgorithm(EmpiSimilarityEnum.COSINE).setMatchThreshold(NAME_THRESHOLD)); + EmpiResourceFieldMatcher comparator = new EmpiResourceFieldMatcher(ourFhirContext, matchField); comparator.match(myJohn, myJohny); fail(); } catch (DataFormatException e) { diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceMatcherSvcR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceMatcherSvcR4Test.java new file mode 100644 index 00000000000..9e0c814c51b --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceMatcherSvcR4Test.java @@ -0,0 +1,59 @@ +package ca.uhn.fhir.empi.rules.svc; + +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.empi.api.EmpiMatchOutcome; +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class EmpiResourceMatcherSvcR4Test extends BaseEmpiRulesR4Test { + private EmpiResourceMatcherSvc myEmpiResourceMatcherSvc; + private Patient myJohn; + private Patient myJohny; + + @Override + @BeforeEach + public void before() { + super.before(); + + when(mySearchParamRetriever.getActiveSearchParam("Patient", "birthdate")).thenReturn(mock(RuntimeSearchParam.class)); + when(mySearchParamRetriever.getActiveSearchParam("Patient", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); + when(mySearchParamRetriever.getActiveSearchParam("Patient", "active")).thenReturn(mock(RuntimeSearchParam.class)); + + myEmpiResourceMatcherSvc = buildMatcher(buildActiveBirthdateIdRules()); + + myJohn = buildJohn(); + myJohny = buildJohny(); + } + + @Test + public void testCompareFirstNameMatch() { + EmpiMatchOutcome result = myEmpiResourceMatcherSvc.match(myJohn, myJohny); + assertMatchResult(EmpiMatchResultEnum.POSSIBLE_MATCH, 1L, 0.816, false, false, result); + + } + + @Test + public void testCompareBothNamesMatch() { + myJohn.addName().setFamily("Smith"); + myJohny.addName().setFamily("Smith"); + EmpiMatchOutcome result = myEmpiResourceMatcherSvc.match(myJohn, myJohny); + assertMatchResult(EmpiMatchResultEnum.MATCH, 3L, 1.816, false, false, result); + } + + @Test + public void testMatchResult() { + assertMatchResult(EmpiMatchResultEnum.POSSIBLE_MATCH, 1L, 0.816, false, false, myEmpiResourceMatcherSvc.getMatchResult(myJohn, myJohny)); + myJohn.addName().setFamily("Smith"); + myJohny.addName().setFamily("Smith"); + assertMatchResult(EmpiMatchResultEnum.MATCH, 3L, 1.816, false, false, myEmpiResourceMatcherSvc.getMatchResult(myJohn, myJohny)); + Patient patient3 = new Patient(); + patient3.setId("Patient/3"); + patient3.addName().addGiven("Henry"); + assertMatchResult(EmpiMatchResultEnum.NO_MATCH, 0L, 0.0, false, false, myEmpiResourceMatcherSvc.getMatchResult(myJohn, patient3)); + } +} diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/ResourceMatcherR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/ResourceMatcherR4Test.java similarity index 52% rename from hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/ResourceMatcherR4Test.java rename to hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/ResourceMatcherR4Test.java index 7301b2bcbbd..399e4abc373 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/ResourceMatcherR4Test.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/ResourceMatcherR4Test.java @@ -1,23 +1,21 @@ -package ca.uhn.fhir.mdm.rules.svc; +package ca.uhn.fhir.empi.rules.svc; import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.mdm.api.MdmMatchOutcome; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson; -import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson; -import ca.uhn.fhir.mdm.rules.json.MdmRulesJson; -import ca.uhn.fhir.mdm.rules.matcher.MdmMatcherEnum; +import ca.uhn.fhir.empi.api.EmpiMatchOutcome; +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson; +import ca.uhn.fhir.empi.rules.json.EmpiMatcherJson; +import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; +import ca.uhn.fhir.empi.rules.matcher.EmpiMatcherEnum; import org.hl7.fhir.r4.model.HumanName; import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.Arrays; - import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class ResourceMatcherR4Test extends BaseMdmRulesR4Test { +public class ResourceMatcherR4Test extends BaseEmpiRulesR4Test { private static final String PATIENT_PHONE = "phone"; private static final String MATCH_FIELDS = PATIENT_GIVEN + "," + PATIENT_FAMILY + "," + PATIENT_PHONE; public static final String PHONE_NUMBER = "123 456789"; @@ -31,8 +29,6 @@ public class ResourceMatcherR4Test extends BaseMdmRulesR4Test { when(mySearchParamRetriever.getActiveSearchParam("Patient", "birthdate")).thenReturn(mock(RuntimeSearchParam.class)); when(mySearchParamRetriever.getActiveSearchParam("Patient", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); - when(mySearchParamRetriever.getActiveSearchParam("Practitioner", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); - when(mySearchParamRetriever.getActiveSearchParam("Medication", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); when(mySearchParamRetriever.getActiveSearchParam("Patient", "active")).thenReturn(mock(RuntimeSearchParam.class)); { @@ -55,44 +51,43 @@ public class ResourceMatcherR4Test extends BaseMdmRulesR4Test { @Test public void testMetaphoneMatchResult() { - MdmResourceMatcherSvc matcherSvc = buildMatcher(buildNamePhoneRules(MdmMatcherEnum.METAPHONE)); - MdmMatchOutcome result = matcherSvc.match(myLeft, myRight); - assertMatchResult(MdmMatchResultEnum.MATCH, 7L, 3.0, false, false, result); + EmpiResourceMatcherSvc matcherSvc = buildMatcher(buildNamePhoneRules(EmpiMatcherEnum.METAPHONE)); + EmpiMatchOutcome result = matcherSvc.match(myLeft, myRight); + assertMatchResult(EmpiMatchResultEnum.MATCH, 7L, 3.0, false, false, result); } @Test public void testStringMatchResult() { - MdmResourceMatcherSvc matcherSvc = buildMatcher(buildNamePhoneRules(MdmMatcherEnum.STRING)); - MdmMatchOutcome result = matcherSvc.match(myLeft, myRight); - assertMatchResult(MdmMatchResultEnum.NO_MATCH, 5L, 2.0, false, false, result); + EmpiResourceMatcherSvc matcherSvc = buildMatcher(buildNamePhoneRules(EmpiMatcherEnum.STRING)); + EmpiMatchOutcome result = matcherSvc.match(myLeft, myRight); + assertMatchResult(EmpiMatchResultEnum.NO_MATCH, 5L, 2.0, false, false, result); } - protected MdmRulesJson buildNamePhoneRules(MdmMatcherEnum theMatcherEnum) { - MdmFieldMatchJson lastNameMatchField = new MdmFieldMatchJson() + protected EmpiRulesJson buildNamePhoneRules(EmpiMatcherEnum theMatcherEnum) { + EmpiFieldMatchJson lastNameMatchField = new EmpiFieldMatchJson() .setName(PATIENT_FAMILY) .setResourceType("Patient") .setResourcePath("name.family") - .setMatcher(new MdmMatcherJson().setAlgorithm(theMatcherEnum)); + .setMatcher(new EmpiMatcherJson().setAlgorithm(theMatcherEnum)); - MdmFieldMatchJson firstNameMatchField = new MdmFieldMatchJson() + EmpiFieldMatchJson firstNameMatchField = new EmpiFieldMatchJson() .setName(PATIENT_GIVEN) .setResourceType("Patient") .setResourcePath("name.given") - .setMatcher(new MdmMatcherJson().setAlgorithm(theMatcherEnum)); + .setMatcher(new EmpiMatcherJson().setAlgorithm(theMatcherEnum)); - MdmFieldMatchJson phoneField = new MdmFieldMatchJson() + EmpiFieldMatchJson phoneField = new EmpiFieldMatchJson() .setName(PATIENT_PHONE) .setResourceType("Patient") .setResourcePath("telecom.value") - .setMatcher(new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.STRING)); + .setMatcher(new EmpiMatcherJson().setAlgorithm(EmpiMatcherEnum.STRING)); - MdmRulesJson retval = new MdmRulesJson(); + EmpiRulesJson retval = new EmpiRulesJson(); retval.setVersion("test version"); retval.addMatchField(firstNameMatchField); retval.addMatchField(lastNameMatchField); - retval.setMdmTypes(Arrays.asList("Patient", "Practitioner", "Medication")); retval.addMatchField(phoneField); - retval.putMatchResult(MATCH_FIELDS, MdmMatchResultEnum.MATCH); + retval.putMatchResult(MATCH_FIELDS, EmpiMatchResultEnum.MATCH); return retval; } } diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/svc/EIDHelperR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/EIDHelperR4Test.java similarity index 61% rename from hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/svc/EIDHelperR4Test.java rename to hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/EIDHelperR4Test.java index ae93b29fe00..dad0d2306c9 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/svc/EIDHelperR4Test.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/EIDHelperR4Test.java @@ -1,61 +1,45 @@ -package ca.uhn.fhir.mdm.svc; +package ca.uhn.fhir.empi.svc; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.mdm.BaseR4Test; -import ca.uhn.fhir.mdm.model.CanonicalEID; -import ca.uhn.fhir.mdm.rules.config.MdmRuleValidator; -import ca.uhn.fhir.mdm.rules.config.MdmSettings; -import ca.uhn.fhir.mdm.rules.json.MdmRulesJson; -import ca.uhn.fhir.mdm.util.EIDHelper; -import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; +import ca.uhn.fhir.empi.BaseR4Test; +import ca.uhn.fhir.empi.model.CanonicalEID; +import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator; +import ca.uhn.fhir.empi.rules.config.EmpiSettings; +import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; +import ca.uhn.fhir.empi.util.EIDHelper; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.Arrays; -import java.util.HashSet; import java.util.List; -import static ca.uhn.fhir.mdm.api.MdmConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM; +import static ca.uhn.fhir.empi.api.EmpiConstants.HAPI_ENTERPRISE_IDENTIFIER_SYSTEM; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Mockito.when; public class EIDHelperR4Test extends BaseR4Test { private static final FhirContext ourFhirContext = FhirContext.forR4(); - private static final String EXTERNAL_ID_SYSTEM_FOR_TEST = "http://testsystem.io/naming-system/mdm"; + private static final String EXTERNAL_ID_SYSTEM_FOR_TEST = "http://testsystem.io/naming-system/empi"; - private static final MdmRulesJson ourRules = new MdmRulesJson() { - { - setEnterpriseEIDSystem(EXTERNAL_ID_SYSTEM_FOR_TEST); - setMdmTypes(Arrays.asList(new String[] {"Patient"})); - } - }; + private static final EmpiRulesJson ourRules = new EmpiRulesJson() {{ + setEnterpriseEIDSystem(EXTERNAL_ID_SYSTEM_FOR_TEST); + }}; - private MdmSettings myMdmSettings; + private EmpiSettings myEmpiSettings; private EIDHelper myEidHelper; @BeforeEach public void before() { - when(mySearchParamRetriever.getActiveSearchParam("Patient", "identifier")) - .thenReturn(new RuntimeSearchParam( - "identifier", "Description", "identifier", RestSearchParameterTypeEnum.STRING, - new HashSet<>(), new HashSet<>(), RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE - )); - - myMdmSettings = new MdmSettings(new MdmRuleValidator(ourFhirContext, mySearchParamRetriever)) { - { - setMdmRules(ourRules); - } - }; - myEidHelper = new EIDHelper(ourFhirContext, myMdmSettings); + myEmpiSettings = new EmpiSettings(new EmpiRuleValidator(ourFhirContext, mySearchParamRetriever)) {{ + setEmpiRules(ourRules); + }}; + myEidHelper = new EIDHelper(ourFhirContext, myEmpiSettings); } @Test diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/svc/NameUtilTestDSTU3.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/NameUtilTestDSTU3.java similarity index 95% rename from hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/svc/NameUtilTestDSTU3.java rename to hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/NameUtilTestDSTU3.java index 780f71c6f4e..46529bd3b75 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/svc/NameUtilTestDSTU3.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/NameUtilTestDSTU3.java @@ -1,7 +1,7 @@ -package ca.uhn.fhir.mdm.svc; +package ca.uhn.fhir.empi.svc; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.util.NameUtil; +import ca.uhn.fhir.empi.util.NameUtil; import ca.uhn.fhir.util.FhirTerser; import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.dstu3.model.StringType; diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/svc/NameUtilTestR4.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/NameUtilTestR4.java similarity index 95% rename from hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/svc/NameUtilTestR4.java rename to hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/NameUtilTestR4.java index 35fb0dd2bb4..047f87f8374 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/svc/NameUtilTestR4.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/NameUtilTestR4.java @@ -1,7 +1,7 @@ -package ca.uhn.fhir.mdm.svc; +package ca.uhn.fhir.empi.svc; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.util.NameUtil; +import ca.uhn.fhir.empi.util.NameUtil; import ca.uhn.fhir.util.FhirTerser; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.r4.model.Patient; diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/PersonHelperDSTU3Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/PersonHelperDSTU3Test.java new file mode 100644 index 00000000000..c225e1beb2d --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/PersonHelperDSTU3Test.java @@ -0,0 +1,84 @@ +package ca.uhn.fhir.empi.svc; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.empi.model.CanonicalIdentityAssuranceLevel; +import ca.uhn.fhir.empi.util.PersonHelper; +import ca.uhn.fhir.model.primitive.IdDt; +import org.hl7.fhir.dstu3.model.Person; +import org.hl7.fhir.dstu3.model.Reference; +import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Collectors; + +import static ca.uhn.fhir.empi.util.TestUtils.createDummyContext; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +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 PersonHelperDSTU3Test { + public static final FhirContext ourFhirContext = FhirContext.forDstu3(); + public static final String PATIENT_1 = "Patient/1"; + public static final String PATIENT_2 = "Patient/2"; + public static final String PATIENT_BAD = "Patient/BAD"; + public static final PersonHelper MY_PERSON_HELPER = new PersonHelper(ourFhirContext); + + @Test + public void testGetLinks() { + Person person = new Person(); + person.addLink().setTarget(new Reference(PATIENT_1)); + person.addLink().setTarget(new Reference(PATIENT_2)); + + { + List links = MY_PERSON_HELPER.getLinkIds(person).collect(Collectors.toList()); + assertEquals(2, links.size()); + assertEquals(PATIENT_1, links.get(0).getValue()); + assertEquals(PATIENT_2, links.get(1).getValue()); + assertTrue(MY_PERSON_HELPER.containsLinkTo(person, new IdDt(PATIENT_1))); + assertTrue(MY_PERSON_HELPER.containsLinkTo(person, new IdDt(PATIENT_2))); + assertFalse(MY_PERSON_HELPER.containsLinkTo(person, new IdDt(PATIENT_BAD))); + } + + { + MY_PERSON_HELPER.removeLink(person, new IdDt(PATIENT_1), createDummyContext()); + List links = MY_PERSON_HELPER.getLinkIds(person).collect(Collectors.toList()); + assertEquals(1, links.size()); + assertEquals(PATIENT_2, links.get(0).getValue()); + } + + + } + + @Test + public void testAddOrUpdateLinks() { + Person person = new Person(); + + //Links with no assurance level are rejected + { + MY_PERSON_HELPER.addOrUpdateLink(person, new IdDt(PATIENT_1), null, createDummyContext()); + assertThat(person.getLink().size(), is(equalTo(0))); + } + //Original link addition + { + MY_PERSON_HELPER.addOrUpdateLink(person, new IdDt(PATIENT_1), CanonicalIdentityAssuranceLevel.LEVEL3, createDummyContext()); + assertThat(person.getLink().size(), is(equalTo(1))); + } + + //Link update + { + MY_PERSON_HELPER.addOrUpdateLink(person, new IdDt(PATIENT_1), CanonicalIdentityAssuranceLevel.LEVEL4, createDummyContext()); + assertThat(person.getLink().size(), is(equalTo(1))); + } + + //New link + { + MY_PERSON_HELPER.addOrUpdateLink(person, new IdDt(PATIENT_2), CanonicalIdentityAssuranceLevel.LEVEL4, createDummyContext()); + assertThat(person.getLink().size(), is(equalTo(2))); + } + } + +} diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/PersonHelperR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/PersonHelperR4Test.java new file mode 100644 index 00000000000..a1e3b2d8675 --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/PersonHelperR4Test.java @@ -0,0 +1,82 @@ +package ca.uhn.fhir.empi.svc; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.empi.model.CanonicalIdentityAssuranceLevel; +import ca.uhn.fhir.empi.util.PersonHelper; +import ca.uhn.fhir.model.primitive.IdDt; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Person; +import org.hl7.fhir.r4.model.Reference; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Collectors; + +import static ca.uhn.fhir.empi.util.TestUtils.createDummyContext; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +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 PersonHelperR4Test { + public static final FhirContext ourFhirContext = FhirContext.forR4(); + public static final String PATIENT_1 = "Patient/1"; + public static final String PATIENT_2 = "Patient/2"; + public static final String PATIENT_BAD = "Patient/BAD"; + + public static final PersonHelper MY_PERSON_HELPER = new PersonHelper(ourFhirContext); + + @Test + public void testGetLinks() { + Person person = new Person(); + person.addLink().setTarget(new Reference(PATIENT_1)); + person.addLink().setTarget(new Reference(PATIENT_2)); + + { + List links = MY_PERSON_HELPER.getLinkIds(person).collect(Collectors.toList()); + assertEquals(2, links.size()); + assertEquals(PATIENT_1, links.get(0).getValue()); + assertEquals(PATIENT_2, links.get(1).getValue()); + assertTrue(MY_PERSON_HELPER.containsLinkTo(person, new IdDt(PATIENT_1))); + assertTrue(MY_PERSON_HELPER.containsLinkTo(person, new IdDt(PATIENT_2))); + assertFalse(MY_PERSON_HELPER.containsLinkTo(person, new IdDt(PATIENT_BAD))); + } + + { + MY_PERSON_HELPER.removeLink(person, new IdDt(PATIENT_1), createDummyContext()); + List links = MY_PERSON_HELPER.getLinkIds(person).collect(Collectors.toList()); + assertEquals(1, links.size()); + assertEquals(PATIENT_2, links.get(0).getValue()); + } + } + + @Test + public void testAddOrUpdateLinks() { + Person person = new Person(); + + //Link addition without assurance level should NOOP + { + MY_PERSON_HELPER.addOrUpdateLink(person, new IdDt(PATIENT_1), null, null); + assertThat(person.getLink().size(), is(equalTo(0))); + } + //Original link addition + { + MY_PERSON_HELPER.addOrUpdateLink(person, new IdDt(PATIENT_1), CanonicalIdentityAssuranceLevel.LEVEL3, createDummyContext()); + assertThat(person.getLink().size(), is(equalTo(1))); + } + + //Link update + { + MY_PERSON_HELPER.addOrUpdateLink(person, new IdDt(PATIENT_1), CanonicalIdentityAssuranceLevel.LEVEL4, createDummyContext()); + assertThat(person.getLink().size(), is(equalTo(1))); + } + + //New link + { + MY_PERSON_HELPER.addOrUpdateLink(person, new IdDt(PATIENT_2), CanonicalIdentityAssuranceLevel.LEVEL4, createDummyContext()); + assertThat(person.getLink().size(), is(equalTo(2))); + } + } +} diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/util/AssuranceLevelUtilTest.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/util/AssuranceLevelUtilTest.java new file mode 100644 index 00000000000..4f75eda8c28 --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/util/AssuranceLevelUtilTest.java @@ -0,0 +1,65 @@ +package ca.uhn.fhir.empi.util; + +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import org.junit.jupiter.api.Test; + +import static ca.uhn.fhir.empi.api.EmpiLinkSourceEnum.AUTO; +import static ca.uhn.fhir.empi.api.EmpiLinkSourceEnum.MANUAL; +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 ca.uhn.fhir.empi.model.CanonicalIdentityAssuranceLevel.LEVEL1; +import static ca.uhn.fhir.empi.model.CanonicalIdentityAssuranceLevel.LEVEL2; +import static ca.uhn.fhir.empi.model.CanonicalIdentityAssuranceLevel.LEVEL3; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class AssuranceLevelUtilTest { + + @Test + public void testValidPersonLinkLevels() { + assertThat(AssuranceLevelUtil.getAssuranceLevel(POSSIBLE_MATCH, AUTO), is(equalTo(LEVEL1))); + assertThat(AssuranceLevelUtil.getAssuranceLevel(MATCH, AUTO), is(equalTo(LEVEL2))); + assertThat(AssuranceLevelUtil.getAssuranceLevel(MATCH, MANUAL), is(equalTo(LEVEL3))); + + } + + @Test + public void testInvalidPersonLinkLevels() { + try { + AssuranceLevelUtil.getAssuranceLevel(NO_MATCH, AUTO); + fail(); + } catch (InvalidRequestException e) { + assertEquals("An AUTO EMPI Link may not have a match result of NO_MATCH", e.getMessage()); + } + try { + AssuranceLevelUtil.getAssuranceLevel(POSSIBLE_DUPLICATE, AUTO); + fail(); + } catch (InvalidRequestException e) { + assertEquals("An AUTO EMPI Link may not have a match result of POSSIBLE_DUPLICATE", e.getMessage()); + } + try { + AssuranceLevelUtil.getAssuranceLevel(NO_MATCH, MANUAL); + fail(); + } catch (InvalidRequestException e) { + assertEquals("A MANUAL EMPI Link may not have a match result of NO_MATCH", e.getMessage()); + } + try { + AssuranceLevelUtil.getAssuranceLevel(POSSIBLE_MATCH, MANUAL); + fail(); + } catch (InvalidRequestException e) { + assertEquals("A MANUAL EMPI Link may not have a match result of POSSIBLE_MATCH", e.getMessage()); + } + try { + AssuranceLevelUtil.getAssuranceLevel(POSSIBLE_DUPLICATE, MANUAL); + fail(); + } catch (InvalidRequestException e) { + assertEquals("A MANUAL EMPI Link may not have a match result of POSSIBLE_DUPLICATE", e.getMessage()); + } + } + +} diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/util/TestUtils.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/util/TestUtils.java new file mode 100644 index 00000000000..d70c9015609 --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/util/TestUtils.java @@ -0,0 +1,10 @@ +package ca.uhn.fhir.empi.util; + +import ca.uhn.fhir.empi.model.EmpiTransactionContext; + +public class TestUtils { + public static EmpiTransactionContext createDummyContext() { + EmpiTransactionContext context = new EmpiTransactionContext(); + return context; + } +} diff --git a/hapi-fhir-server-mdm/src/test/resources/bad-rules-bad-filter.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-filter.json similarity index 77% rename from hapi-fhir-server-mdm/src/test/resources/bad-rules-bad-filter.json rename to hapi-fhir-server-empi/src/test/resources/bad-rules-bad-filter.json index d1b90d976f8..671c8c1164b 100644 --- a/hapi-fhir-server-mdm/src/test/resources/bad-rules-bad-filter.json +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-filter.json @@ -1,5 +1,4 @@ { - "mdmTypes": ["Patient", "Practitioner", "Medication"], "version": "1", "candidateSearchParams" : [], "candidateFilterSearchParams" : [{ diff --git a/hapi-fhir-server-mdm/src/test/resources/bad-rules-bad-path.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-path.json similarity index 85% rename from hapi-fhir-server-mdm/src/test/resources/bad-rules-bad-path.json rename to hapi-fhir-server-empi/src/test/resources/bad-rules-bad-path.json index 47fcd5cf248..fc473b216ee 100644 --- a/hapi-fhir-server-mdm/src/test/resources/bad-rules-bad-path.json +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-path.json @@ -1,6 +1,5 @@ { "version": "1", - "mdmTypes": ["Patient", "Practitioner", "Medication"], "candidateSearchParams" : [], "candidateFilterSearchParams" : [], "matchFields" : [ { diff --git a/hapi-fhir-server-mdm/src/test/resources/bad-rules-bad-searchparam.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-searchparam.json similarity index 77% rename from hapi-fhir-server-mdm/src/test/resources/bad-rules-bad-searchparam.json rename to hapi-fhir-server-empi/src/test/resources/bad-rules-bad-searchparam.json index 0ed945ede6e..8585752dcbf 100644 --- a/hapi-fhir-server-mdm/src/test/resources/bad-rules-bad-searchparam.json +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-searchparam.json @@ -1,6 +1,5 @@ { "version": "1", - "mdmTypes": ["Patient", "Practitioner", "Medication"], "candidateSearchParams" : [{ "resourceType" : "Patient", "searchParams" : ["foo"] diff --git a/hapi-fhir-server-mdm/src/test/resources/bad-rules-bad-url.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-url.json similarity index 74% rename from hapi-fhir-server-mdm/src/test/resources/bad-rules-bad-url.json rename to hapi-fhir-server-empi/src/test/resources/bad-rules-bad-url.json index dba341f6c83..d626556062d 100644 --- a/hapi-fhir-server-mdm/src/test/resources/bad-rules-bad-url.json +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-url.json @@ -1,6 +1,5 @@ { "version": "1", - "mdmTypes": ["Patient", "Practitioner", "Medication"], "candidateSearchParams" : [], "candidateFilterSearchParams" : [], "matchFields" : [], diff --git a/hapi-fhir-server-mdm/src/test/resources/bad-rules-duplicate-name.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-duplicate-name.json similarity index 89% rename from hapi-fhir-server-mdm/src/test/resources/bad-rules-duplicate-name.json rename to hapi-fhir-server-empi/src/test/resources/bad-rules-duplicate-name.json index 80547f23988..5a6e1c0b3b3 100644 --- a/hapi-fhir-server-mdm/src/test/resources/bad-rules-duplicate-name.json +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-duplicate-name.json @@ -1,6 +1,5 @@ { "version": "1", - "mdmTypes": ["Patient", "Practitioner", "Medication"], "candidateSearchParams": [], "candidateFilterSearchParams": [], "matchFields": [ diff --git a/hapi-fhir-server-mdm/src/test/resources/bad-rules-missing-name.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-missing-name.json similarity index 86% rename from hapi-fhir-server-mdm/src/test/resources/bad-rules-missing-name.json rename to hapi-fhir-server-empi/src/test/resources/bad-rules-missing-name.json index 6f7e0e185d4..a7806f1b25c 100644 --- a/hapi-fhir-server-mdm/src/test/resources/bad-rules-missing-name.json +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-missing-name.json @@ -1,6 +1,5 @@ { "version": "1", - "mdmTypes": ["Patient", "Practitioner", "Medication"], "candidateSearchParams" : [], "candidateFilterSearchParams" : [], "matchFields" : [ { diff --git a/hapi-fhir-server-mdm/src/test/resources/bad-rules-missing-threshold.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-missing-threshold.json similarity index 84% rename from hapi-fhir-server-mdm/src/test/resources/bad-rules-missing-threshold.json rename to hapi-fhir-server-empi/src/test/resources/bad-rules-missing-threshold.json index 7b47db64a7e..97af1f7dc25 100644 --- a/hapi-fhir-server-mdm/src/test/resources/bad-rules-missing-threshold.json +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-missing-threshold.json @@ -1,6 +1,5 @@ { "version": "1", - "mdmTypes": ["Patient", "Practitioner", "Medication"], "candidateSearchParams" : [], "candidateFilterSearchParams" : [], "matchFields" : [ { diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IGoldenResourceMergerSvc.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IGoldenResourceMergerSvc.java deleted file mode 100644 index fb8be43aa63..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IGoldenResourceMergerSvc.java +++ /dev/null @@ -1,36 +0,0 @@ -package ca.uhn.fhir.mdm.api; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import org.hl7.fhir.instance.model.api.IAnyResource; - -public interface IGoldenResourceMergerSvc { - /** - * Move all links from the theFromGoldenResource to theToGoldenResource and then set active=false on theFromGoldenResource. - * Merge all Golden Resource fields subject to survivorship rules. - * - * @param theFromGoldenResource the golden resource we are merging from - * @param theToGoldenResource the golden resource we are merging to - * @return updated theToGoldenResource with the merged fields and links. - */ - IAnyResource mergeGoldenResources(IAnyResource theFromGoldenResource, IAnyResource theToGoldenResource, MdmTransactionContext theMdmTransactionContext); -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmControllerSvc.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmControllerSvc.java deleted file mode 100644 index 0ca08944bfc..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmControllerSvc.java +++ /dev/null @@ -1,40 +0,0 @@ -package ca.uhn.fhir.mdm.api; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import org.hl7.fhir.instance.model.api.IAnyResource; - -import javax.annotation.Nullable; -import java.util.stream.Stream; - -public interface IMdmControllerSvc { - - Stream queryLinks(@Nullable String theGoldenResourceId, @Nullable String theSourceResourceId, @Nullable String theMatchResult, @Nullable String theLinkSource, MdmTransactionContext theMdmTransactionContext); - - Stream getDuplicateGoldenResources(MdmTransactionContext theMdmTransactionContext); - - void notDuplicateGoldenResource(String theGoldenResourceId, String theTargetGoldenResourceId, MdmTransactionContext theMdmTransactionContext); - - IAnyResource mergeGoldenResources(String theFromGoldenResourceId, String theToGoldenResourceId, MdmTransactionContext theMdmTransactionContext); - - IAnyResource updateLink(String theGoldenResourceId, String theSourceResourceId, String theMatchResult, MdmTransactionContext theMdmTransactionContext); -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmLinkSvc.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmLinkSvc.java deleted file mode 100644 index 54933eef81d..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmLinkSvc.java +++ /dev/null @@ -1,48 +0,0 @@ -package ca.uhn.fhir.mdm.api; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import org.hl7.fhir.instance.model.api.IAnyResource; - -public interface IMdmLinkSvc { - - /** - * Update a link between a Golden Resource record and its source resource record. If a link does not exist between - * these two records, create it. - * - * @param theGoldenResource the Golden Resource to link the source resource to. - * @param theSourceResource the source resource, which can be of any of the MDM supported types - * @param theMatchResult the current status of the match to set the link to. - * @param theLinkSource MANUAL or AUTO: what caused the link. - * @param theMdmTransactionContext - */ - void updateLink(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmMatchOutcome theMatchResult, MdmLinkSourceEnum theLinkSource, MdmTransactionContext theMdmTransactionContext); - - /** - * Delete a link between given Golden Resource and the corresponding source resource - * - * @param theExistingGoldenResource - * @param theSourceResource - * @param theMdmTransactionContext - */ - void deleteLink(IAnyResource theExistingGoldenResource, IAnyResource theSourceResource, MdmTransactionContext theMdmTransactionContext); -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmSettings.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmSettings.java deleted file mode 100644 index 41d8eb7c90e..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmSettings.java +++ /dev/null @@ -1,53 +0,0 @@ -package ca.uhn.fhir.mdm.api; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.rules.json.MdmRulesJson; - -import java.util.stream.Collectors; - -public interface IMdmSettings { - - String MDM_CHANNEL_NAME = "mdm"; - - // Parallel processing of MDM can result in missed matches. Best to single-thread. - int MDM_DEFAULT_CONCURRENT_CONSUMERS = 1; - - boolean isEnabled(); - - int getConcurrentConsumers(); - - MdmRulesJson getMdmRules(); - - boolean isPreventEidUpdates(); - - boolean isPreventMultipleEids(); - - String getRuleVersion(); - - default boolean isSupportedMdmType(String theResourceName) { - return getMdmRules().getMdmTypes().contains(theResourceName); - } - - default String getSupportedMdmTypes() { - return getMdmRules().getMdmTypes().stream().collect(Collectors.joining(", ")); - } -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmSubmitSvc.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmSubmitSvc.java deleted file mode 100644 index fa7329c53ec..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/IMdmSubmitSvc.java +++ /dev/null @@ -1,81 +0,0 @@ -package ca.uhn.fhir.mdm.api; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import org.hl7.fhir.instance.model.api.IIdType; - -import javax.annotation.Nullable; - -public interface IMdmSubmitSvc { - - /** - * Submit all eligible resources for MDM processing. - * - * @param theCriteria The FHIR search critieria for filtering the resources to be submitted for MDM processing. - * NOTE: - * When using this function, the criteria supplied must be valid for all MDM types. e.g. , if you - * run this with the criteria birthDate=1990-06-28, it will fail, as Practitioners do not have a birthday. - * Use with caution. - * - * @return - */ - long submitAllSourceTypesToMdm(@Nullable String theCriteria); - - /** - * Given a type and a search criteria, submit all found resources for MDM processing. - * - * @param theSourceResourceType the resource type that you wish to execute a search over for submission to MDM. - * @param theCriteria The FHIR search critieria for filtering the resources to be submitted for MDM processing.. - * @return the number of resources submitted for MDM processing. - */ - long submitSourceResourceTypeToMdm(String theSourceResourceType, String theCriteria); - - /** - * Convenience method that calls {@link #submitSourceResourceTypeToMdm(String, String)} with the type pre-populated. - * - * @param theCriteria The FHIR search critieria for filtering the resources to be submitted for MDM processing. - * @return the number of resources submitted for MDM processing. - */ - long submitPractitionerTypeToMdm(String theCriteria); - - /** - * Convenience method that calls {@link #submitSourceResourceTypeToMdm(String, String)} with the type pre-populated. - * - * @param theCriteria The FHIR search critieria for filtering the resources to be submitted for MDM processing. - * @return the number of resources submitted for MDM processing. - */ - long submitPatientTypeToMdm(String theCriteria); - - /** - * Given an ID and a source resource type valid for MDM, manually submit the given ID for MDM processing. - * - * @param theId the ID of the resource to process for MDM. - * @return the constant `1`, as if this function returns successfully, it will have processed one resource for MDM. - */ - long submitSourceResourceToMdm(IIdType theId); - - /** - * This setter exists to allow imported modules to override settings. - * - * @param theMdmSettings Settings to set - */ - void setMdmSettings(IMdmSettings theMdmSettings); -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmConstants.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmConstants.java deleted file mode 100644 index f90d735f037..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmConstants.java +++ /dev/null @@ -1,44 +0,0 @@ -package ca.uhn.fhir.mdm.api; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -public class MdmConstants { - /** - * TAG system for Golden Resources which are managed by HAPI MDM. - */ - - public static final String SYSTEM_MDM_MANAGED = "https://hapifhir.org/NamingSystem/managing-mdm-system"; - public static final String CODE_HAPI_MDM_MANAGED = "HAPI-MDM"; - public static final String DISPLAY_HAPI_MDM_MANAGED = "This Golden Resource can only be modified by Smile CDR's MDM system."; - public static final String CODE_NO_MDM_MANAGED = "NO-MDM"; - public static final String HAPI_ENTERPRISE_IDENTIFIER_SYSTEM = "http://hapifhir.io/fhir/NamingSystem/mdm-golden-resource-enterprise-id"; - public static final String ALL_RESOURCE_SEARCH_PARAM_TYPE = "*"; - - public static final String FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE = "http://hl7.org/fhir/StructureDefinition/match-grade"; - - public static final String SYSTEM_GOLDEN_RECORD_STATUS = "http://hapifhir.io/fhir/NamingSystem/mdm-record-status"; - public static final String CODE_GOLDEN_RECORD = "GOLDEN_RECORD"; - public static final String CODE_GOLDEN_RECORD_REDIRECTED = "REDIRECTED"; - public static final String DISPLAY_GOLDEN_RECORD = "Golden Record"; - public static final String DISPLAY_GOLDEN_REDIRECT = "This resource was found to be a duplicate and has been redirected."; - - public static final String UNKNOWN_MDM_TYPES = "Unknown Resource Types"; -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmMatchOutcome.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmMatchOutcome.java deleted file mode 100644 index 6feb0188f23..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/api/MdmMatchOutcome.java +++ /dev/null @@ -1,160 +0,0 @@ -package ca.uhn.fhir.mdm.api; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import org.apache.commons.lang3.builder.ToStringBuilder; - -/** - * This data object captures the final outcome of an MDM match - */ -public final class MdmMatchOutcome { - - public static final MdmMatchOutcome POSSIBLE_DUPLICATE = new MdmMatchOutcome(null, null).setMatchResultEnum(MdmMatchResultEnum.POSSIBLE_DUPLICATE); - public static final MdmMatchOutcome NO_MATCH = new MdmMatchOutcome(null, null).setMatchResultEnum(MdmMatchResultEnum.NO_MATCH); - public static final MdmMatchOutcome NEW_GOLDEN_RESOURCE_MATCH = new MdmMatchOutcome(null, null).setMatchResultEnum(MdmMatchResultEnum.MATCH).setCreatedNewResource(true); - public static final MdmMatchOutcome EID_MATCH = new MdmMatchOutcome(null, null).setMatchResultEnum(MdmMatchResultEnum.MATCH).setEidMatch(true); - public static final MdmMatchOutcome POSSIBLE_MATCH = new MdmMatchOutcome(null, null).setMatchResultEnum(MdmMatchResultEnum.POSSIBLE_MATCH); - - /** - * A bitmap that indicates which rules matched - */ - public final Long vector; - - /** - * The sum of all scores for all rules evaluated. Similarity rules add the similarity score (between 0.0 and 1.0) whereas - * matcher rules add either a 0.0 or 1.0. - */ - public final Double score; - - /** - * Did the MDM match operation result in creating a new golden resource resource? - */ - private boolean myCreatedNewResource; - - /** - * Did the MDM match occur as a result of EIDs matching? - */ - private boolean myEidMatch; - - /** - * Based on the MDM Rules, what was the final match result? - */ - private MdmMatchResultEnum myMatchResultEnum; - - /** - * Total number of MDM rules checked for this outcome - */ - private int myMdmRuleCount; - - public MdmMatchOutcome(Long theVector, Double theScore) { - vector = theVector; - score = theScore; - } - - public boolean isMatch() { - return myMatchResultEnum == MdmMatchResultEnum.MATCH; - } - - public boolean isPossibleMatch() { - return myMatchResultEnum == MdmMatchResultEnum.POSSIBLE_MATCH; - } - - - public boolean isPossibleDuplicate() { - return myMatchResultEnum == MdmMatchResultEnum.POSSIBLE_DUPLICATE; - } - - public MdmMatchResultEnum getMatchResultEnum() { - return myMatchResultEnum; - } - - public MdmMatchOutcome setMatchResultEnum(MdmMatchResultEnum theMatchResultEnum) { - myMatchResultEnum = theMatchResultEnum; - return this; - } - - public boolean isCreatedNewResource() { - return myCreatedNewResource; - } - - /** @param theCreatedNewResource this match is creating a new golden resource */ - public MdmMatchOutcome setCreatedNewResource(boolean theCreatedNewResource) { - myCreatedNewResource = theCreatedNewResource; - return this; - } - - public boolean isEidMatch() { - return myEidMatch; - } - - /** - * Sets the number of MDM rules checked for this match outcome - * - * @param theMdmRuleCount - * Number of MDM rules that were checked for this match outcome - * @return - * Returns this instance - */ - public MdmMatchOutcome setMdmRuleCount(int theMdmRuleCount) { - myMdmRuleCount = theMdmRuleCount; - return this; - } - - /** - * Gets the number of MDM rules checked for this match outcome - * - * @return - * Returns the number of rules - */ - public int getMdmRuleCount() { - return myMdmRuleCount; - } - - /** @param theEidMatch the link was established via a shared EID */ - public MdmMatchOutcome setEidMatch(boolean theEidMatch) { - myEidMatch = theEidMatch; - return this; - } - - /** - * Gets normalized score that is in the range from zero to one - * - * @return - * Returns the normalized score - */ - public Double getNormalizedScore() { - if (myMdmRuleCount == 0) { - return 0.0; - } - return score / myMdmRuleCount; - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .append("vector", vector) - .append("score", score) - .append("myCreatedNewResource", myCreatedNewResource) - .append("myEidMatch", myEidMatch) - .append("myMatchResultEnum", myMatchResultEnum) - .toString(); - } -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/BaseMdmProvider.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/BaseMdmProvider.java deleted file mode 100644 index 8a5e84e0e18..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/BaseMdmProvider.java +++ /dev/null @@ -1,116 +0,0 @@ -package ca.uhn.fhir.mdm.provider; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.MdmLinkJson; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.server.TransactionLogMessages; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.provider.ProviderConstants; -import ca.uhn.fhir.util.ParametersUtil; -import org.hl7.fhir.instance.model.api.IBase; -import org.hl7.fhir.instance.model.api.IBaseParameters; -import org.hl7.fhir.instance.model.api.IPrimitiveType; - -import java.util.stream.Stream; - -public abstract class BaseMdmProvider { - - protected final FhirContext myFhirContext; - - public BaseMdmProvider(FhirContext theFhirContext) { - myFhirContext = theFhirContext; - } - - protected void validateMergeParameters(IPrimitiveType theFromGoldenResourceId, IPrimitiveType theToGoldenResourceId) { - // TODO NG - Add validation to check that types are the same? - validateNotNull(ProviderConstants.MDM_MERGE_GR_FROM_GOLDEN_RESOURCE_ID, theFromGoldenResourceId); - validateNotNull(ProviderConstants.MDM_MERGE_GR_TO_GOLDEN_RESOURCE_ID, theToGoldenResourceId); - if (theFromGoldenResourceId.getValue().equals(theToGoldenResourceId.getValue())) { - throw new InvalidRequestException("fromGoldenResourceId must be different from toGoldenResourceId"); - } - } - - private void validateNotNull(String theName, IPrimitiveType theString) { - if (theString == null || theString.getValue() == null) { - throw new InvalidRequestException(theName + " cannot be null"); - } - } - - protected void validateUpdateLinkParameters(IPrimitiveType theGoldenResourceId, IPrimitiveType theResourceId, IPrimitiveType theMatchResult) { - // TODO NG - Add validation to check that types are the same? - This is done deeper in the code, perhaps move here? - validateNotNull(ProviderConstants.MDM_UPDATE_LINK_GOLDEN_RESOURCE_ID, theGoldenResourceId); - validateNotNull(ProviderConstants.MDM_UPDATE_LINK_RESOURCE_ID, theResourceId); - validateNotNull(ProviderConstants.MDM_UPDATE_LINK_MATCH_RESULT, theMatchResult); - MdmMatchResultEnum matchResult = MdmMatchResultEnum.valueOf(theMatchResult.getValue()); - switch (matchResult) { - case NO_MATCH: - case MATCH: - break; - default: - throw new InvalidRequestException(ProviderConstants.MDM_UPDATE_LINK + " illegal " + ProviderConstants.MDM_UPDATE_LINK_MATCH_RESULT + - " value '" + matchResult + "'. Must be " + MdmMatchResultEnum.NO_MATCH + " or " + MdmMatchResultEnum.MATCH); - } - } - - protected void validateNotDuplicateParameters(IPrimitiveType theGoldenResourceId, IPrimitiveType theResourceId) { - validateNotNull(ProviderConstants.MDM_UPDATE_LINK_GOLDEN_RESOURCE_ID, theGoldenResourceId); - validateNotNull(ProviderConstants.MDM_UPDATE_LINK_RESOURCE_ID, theResourceId); - } - - - protected MdmTransactionContext createMdmContext(RequestDetails theRequestDetails, MdmTransactionContext.OperationType theOperationType, String theResourceType) { - TransactionLogMessages transactionLogMessages = TransactionLogMessages.createFromTransactionGuid(theRequestDetails.getTransactionGuid()); - MdmTransactionContext mdmTransactionContext = new MdmTransactionContext(transactionLogMessages, theOperationType); - mdmTransactionContext.setResourceType(theResourceType); - return mdmTransactionContext; - } - - protected String extractStringOrNull(IPrimitiveType theString) { - if (theString == null) { - return null; - } - return theString.getValue(); - } - - protected IBaseParameters parametersFromMdmLinks(Stream theMdmLinkStream, boolean includeResultAndSource) { - IBaseParameters retval = ParametersUtil.newInstance(myFhirContext); - - theMdmLinkStream.forEach(mdmLink -> { - IBase resultPart = ParametersUtil.addParameterToParameters(myFhirContext, retval, "link"); - ParametersUtil.addPartString(myFhirContext, resultPart, "goldenResourceId", mdmLink.getGoldenResourceId()); - ParametersUtil.addPartString(myFhirContext, resultPart, "sourceResourceId", mdmLink.getSourceId()); - - if (includeResultAndSource) { - ParametersUtil.addPartString(myFhirContext, resultPart, "matchResult", mdmLink.getMatchResult().name()); - ParametersUtil.addPartString(myFhirContext, resultPart, "linkSource", mdmLink.getLinkSource().name()); - ParametersUtil.addPartBoolean(myFhirContext, resultPart, "eidMatch", mdmLink.getEidMatch()); - ParametersUtil.addPartBoolean(myFhirContext, resultPart, "hadToCreateNewResource", mdmLink.getLinkCreatedNewResource()); - ParametersUtil.addPartDecimal(myFhirContext, resultPart, "score", mdmLink.getScore()); - } - }); - return retval; - } - -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmControllerUtil.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmControllerUtil.java deleted file mode 100644 index 3bfafd2b82b..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmControllerUtil.java +++ /dev/null @@ -1,74 +0,0 @@ -package ca.uhn.fhir.mdm.provider; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import org.hl7.fhir.instance.model.api.IIdType; - -public class MdmControllerUtil { - public static MdmMatchResultEnum extractMatchResultOrNull(String theMatchResult) { - if (theMatchResult == null) { - return null; - } - return MdmMatchResultEnum.valueOf(theMatchResult); - } - - public static MdmLinkSourceEnum extractLinkSourceOrNull(String theLinkSource) { - if (theLinkSource == null) { - return null; - } - return MdmLinkSourceEnum.valueOf(theLinkSource); - } - - public static IIdType extractGoldenResourceIdDtOrNull(String theName, String theGoldenResourceId) { - if (theGoldenResourceId == null) { - return null; - } - return getGoldenIdDtOrThrowException(theName, theGoldenResourceId); - } - - public static IIdType extractSourceIdDtOrNull(String theName, String theSourceId) { - if (theSourceId == null) { - return null; - } - return getSourceIdDtOrThrowException(theName, theSourceId); - } - - static IdDt getGoldenIdDtOrThrowException(String theParamName, String theId) { - IdDt goldenResourceId = new IdDt(theId); - //TODO GGG MDM: maybe add a gate here to only consider resources that can possibly be EMPI'ed? - if (goldenResourceId.getIdPart() == null) { - throw new InvalidRequestException(theParamName + " is '" + theId + "'. must have form / where is the id of the resource"); - } - return goldenResourceId; - } - - public static IIdType getSourceIdDtOrThrowException(String theParamName, String theSourceId) { - IdDt sourceId = new IdDt(theSourceId); - if (sourceId.getIdPart() == null) { - throw new InvalidRequestException(theParamName + " is '" + theSourceId + "'. must have form / where is the id of the resource and is the type of the resource"); - } - return sourceId; - } -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderDstu3.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderDstu3.java deleted file mode 100644 index ba1236b5d43..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderDstu3.java +++ /dev/null @@ -1,316 +0,0 @@ -package ca.uhn.fhir.mdm.provider; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.IMdmControllerSvc; -import ca.uhn.fhir.mdm.api.IMdmExpungeSvc; -import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc; -import ca.uhn.fhir.mdm.api.IMdmSubmitSvc; -import ca.uhn.fhir.mdm.api.MatchedTarget; -import ca.uhn.fhir.mdm.api.MdmConstants; -import ca.uhn.fhir.mdm.api.MdmLinkJson; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationParam; -import ca.uhn.fhir.rest.api.server.RequestDetails; -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 ca.uhn.fhir.util.ParametersUtil; -import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.dstu3.model.Bundle; -import org.hl7.fhir.dstu3.model.CodeType; -import org.hl7.fhir.dstu3.model.DecimalType; -import org.hl7.fhir.dstu3.model.InstantType; -import org.hl7.fhir.dstu3.model.IntegerType; -import org.hl7.fhir.dstu3.model.Parameters; -import org.hl7.fhir.dstu3.model.Patient; -import org.hl7.fhir.dstu3.model.Practitioner; -import org.hl7.fhir.dstu3.model.Resource; -import org.hl7.fhir.dstu3.model.StringType; -import org.hl7.fhir.dstu3.model.codesystems.MatchGrade; -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 javax.annotation.Nonnull; -import java.util.Comparator; -import java.util.List; -import java.util.UUID; -import java.util.stream.Stream; - -public class MdmProviderDstu3 extends BaseMdmProvider { - private final IMdmControllerSvc myMdmControllerSvc; - private final IMdmMatchFinderSvc myMdmMatchFinderSvc; - private final IMdmExpungeSvc myMdmExpungeSvc; - private final IMdmSubmitSvc myMdmSubmitSvc; - - /** - * Constructor - * - * Note that this is not a spring bean. Any necessary injections should - * happen in the constructor - */ - public MdmProviderDstu3(FhirContext theFhirContext, IMdmControllerSvc theMdmControllerSvc, IMdmMatchFinderSvc theMdmMatchFinderSvc, IMdmExpungeSvc theMdmExpungeSvc, IMdmSubmitSvc theMdmSubmitSvc) { - super(theFhirContext); - myMdmControllerSvc = theMdmControllerSvc; - myMdmMatchFinderSvc = theMdmMatchFinderSvc; - myMdmExpungeSvc = theMdmExpungeSvc; - myMdmSubmitSvc = theMdmSubmitSvc; - } - - @Operation(name = ProviderConstants.EMPI_MATCH, type = Patient.class) - public Bundle match(@OperationParam(name = ProviderConstants.MDM_MATCH_RESOURCE, min = 1, max = 1) Patient thePatient) { - if (thePatient == null) { - throw new InvalidRequestException("resource may not be null"); - } - - return getMatchesAndPossibleMatchesForResource(thePatient, "Patient"); - } - - @Operation(name = ProviderConstants.MDM_MATCH) - public Bundle serverMatch(@OperationParam(name = ProviderConstants.MDM_MATCH_RESOURCE, min = 1, max = 1) IAnyResource theResource, - @OperationParam(name = ProviderConstants.MDM_RESOURCE_TYPE, min = 1, max = 1) StringType theResourceType - ) { - if (theResource == null) { - throw new InvalidRequestException("resource may not be null"); - } - return getMatchesAndPossibleMatchesForResource(theResource, theResourceType.getValueNotNull()); - } - - /** - * Helper method which will return a bundle of all Matches and Possible Matches. - */ - private Bundle getMatchesAndPossibleMatchesForResource(IAnyResource theResource, String theResourceType) { - List matches = myMdmMatchFinderSvc.getMatchedTargets(theResourceType, theResource); - matches.sort(Comparator.comparing((MatchedTarget m) -> m.getMatchResult().getNormalizedScore()).reversed()); - - Bundle retVal = new Bundle(); - retVal.setType(Bundle.BundleType.SEARCHSET); - retVal.setId(UUID.randomUUID().toString()); - retVal.getMeta().setLastUpdatedElement(InstantType.now()); - - for (MatchedTarget next : matches) { - boolean shouldKeepThisEntry = next.isMatch() || next.isPossibleMatch(); - if (!shouldKeepThisEntry) { - continue; - } - - Bundle.BundleEntryComponent entry = new Bundle.BundleEntryComponent(); - entry.setResource((Resource) next.getTarget()); - entry.setSearch(toBundleEntrySearchComponent(next)); - - retVal.addEntry(entry); - } - return retVal; - } - - private Bundle.BundleEntrySearchComponent toBundleEntrySearchComponent(MatchedTarget theMatchedTarget) { - Bundle.BundleEntrySearchComponent searchComponent = new Bundle.BundleEntrySearchComponent(); - searchComponent.setMode(Bundle.SearchEntryMode.MATCH); - searchComponent.setScore(theMatchedTarget.getMatchResult().getNormalizedScore()); - - MatchGrade matchGrade = getMatchGrade(theMatchedTarget); - - searchComponent.addExtension(MdmConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE, new CodeType(matchGrade.toCode())); - return searchComponent; - } - - @Operation(name = ProviderConstants.MDM_MERGE_GOLDEN_RESOURCES) - public IBaseResource mergeGoldenResources(@OperationParam(name=ProviderConstants.MDM_MERGE_GR_FROM_GOLDEN_RESOURCE_ID, min = 1, max = 1) StringType theFromGoldenResourceId, - @OperationParam(name=ProviderConstants.MDM_MERGE_GR_TO_GOLDEN_RESOURCE_ID, min = 1, max = 1) StringType theToGoldenResourceId, - RequestDetails theRequestDetails) { - validateMergeParameters(theFromGoldenResourceId, theToGoldenResourceId); - - return myMdmControllerSvc.mergeGoldenResources(theFromGoldenResourceId.getValue(), theToGoldenResourceId.getValue(), - createMdmContext(theRequestDetails, MdmTransactionContext.OperationType.MERGE_GOLDEN_RESOURCES, - getResourceType(ProviderConstants.MDM_MERGE_GR_FROM_GOLDEN_RESOURCE_ID, theFromGoldenResourceId)) - ); - } - - @Operation(name = ProviderConstants.MDM_UPDATE_LINK) - public IBaseResource updateLink(@OperationParam(name=ProviderConstants.MDM_UPDATE_LINK_GOLDEN_RESOURCE_ID, min = 1, max = 1) StringType theGoldenResourceId, - @OperationParam(name=ProviderConstants.MDM_UPDATE_LINK_RESOURCE_ID, min = 1, max = 1) StringType theResourceId, - @OperationParam(name=ProviderConstants.MDM_UPDATE_LINK_MATCH_RESULT, min = 1, max = 1) StringType theMatchResult, - ServletRequestDetails theRequestDetails) { - validateUpdateLinkParameters(theGoldenResourceId, theResourceId, theMatchResult); - return myMdmControllerSvc.updateLink(theGoldenResourceId.getValueNotNull(), theResourceId.getValue(), theMatchResult.getValue(), - createMdmContext(theRequestDetails, MdmTransactionContext.OperationType.UPDATE_LINK, - getResourceType(ProviderConstants.MDM_UPDATE_LINK_GOLDEN_RESOURCE_ID, theGoldenResourceId)) - ); - } - - @Operation(name = ProviderConstants.MDM_CLEAR, returnParameters = { - @OperationParam(name = ProviderConstants.OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type= DecimalType.class) - }) - public Parameters clearMdmLinks(@OperationParam(name=ProviderConstants.MDM_CLEAR_SOURCE_TYPE, min = 0, max = 1) StringType theSourceResourceType, - ServletRequestDetails theRequestDetails) { - long resetCount; - if (theSourceResourceType == null || StringUtils.isBlank(theSourceResourceType.getValue())) { - resetCount = myMdmExpungeSvc.expungeAllMdmLinks(theRequestDetails); - } else { - resetCount = myMdmExpungeSvc.expungeAllMdmLinksOfSourceType(theSourceResourceType.getValueNotNull(), theRequestDetails); - } - Parameters parameters = new Parameters(); - parameters.addParameter().setName(ProviderConstants.OPERATION_MDM_CLEAR_OUT_PARAM_DELETED_COUNT) - .setValue(new DecimalType(resetCount)); - return parameters; - } - - @Operation(name = ProviderConstants.MDM_QUERY_LINKS, idempotent = true) - public Parameters queryLinks(@OperationParam(name=ProviderConstants.MDM_QUERY_LINKS_GOLDEN_RESOURCE_ID, min = 0, max = 1) StringType theGoldenResourceId, - @OperationParam(name=ProviderConstants.MDM_QUERY_LINKS_RESOURCE_ID, min = 0, max = 1) StringType theResourceId, - @OperationParam(name=ProviderConstants.MDM_QUERY_LINKS_MATCH_RESULT, min = 0, max = 1) StringType theMatchResult, - @OperationParam(name=ProviderConstants.MDM_QUERY_LINKS_LINK_SOURCE, min = 0, max = 1) StringType theLinkSource, - ServletRequestDetails theRequestDetails) { - - Stream mdmLinkJson = myMdmControllerSvc.queryLinks(extractStringOrNull(theGoldenResourceId), extractStringOrNull(theResourceId), - extractStringOrNull(theMatchResult), extractStringOrNull(theLinkSource), - createMdmContext(theRequestDetails, MdmTransactionContext.OperationType.QUERY_LINKS, - getResourceType(ProviderConstants.MDM_QUERY_LINKS_GOLDEN_RESOURCE_ID, theGoldenResourceId)) - ); - return (Parameters) parametersFromMdmLinks(mdmLinkJson, true); - } - - @Operation(name = ProviderConstants.MDM_DUPLICATE_GOLDEN_RESOURCES, idempotent = true) - public Parameters getDuplicateGoldenResources(ServletRequestDetails theRequestDetails) { - Stream possibleDuplicates = myMdmControllerSvc.getDuplicateGoldenResources( - createMdmContext(theRequestDetails, MdmTransactionContext.OperationType.DUPLICATE_GOLDEN_RESOURCES, (String) null) - ); - return (Parameters) parametersFromMdmLinks(possibleDuplicates, false); - } - - @Operation(name = ProviderConstants.MDM_NOT_DUPLICATE) - public Parameters notDuplicate(@OperationParam(name=ProviderConstants.MDM_QUERY_LINKS_GOLDEN_RESOURCE_ID, min = 1, max = 1) StringType theGoldenResourceId, - @OperationParam(name=ProviderConstants.MDM_QUERY_LINKS_RESOURCE_ID, min = 1, max = 1) StringType theResourceId, - ServletRequestDetails theRequestDetails) { - - validateNotDuplicateParameters(theGoldenResourceId, theResourceId); - myMdmControllerSvc.notDuplicateGoldenResource(theGoldenResourceId.getValue(), theResourceId.getValue(), - createMdmContext(theRequestDetails, MdmTransactionContext.OperationType.NOT_DUPLICATE, - getResourceType(ProviderConstants.MDM_QUERY_LINKS_GOLDEN_RESOURCE_ID, theGoldenResourceId)) - ); - - Parameters retval = (Parameters) ParametersUtil.newInstance(myFhirContext); - ParametersUtil.addParameterToParametersBoolean(myFhirContext, retval, "success", true); - return retval; - } - - @Operation(name = ProviderConstants.OPERATION_MDM_SUBMIT, idempotent = false, returnParameters = { - @OperationParam(name = ProviderConstants.OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type= IntegerType.class) - }) - public Parameters mdmBatchOnAllSourceResources( - @OperationParam(name= ProviderConstants.MDM_BATCH_RUN_RESOURCE_TYPE, min = 0 , max = 1) StringType theResourceType, - @OperationParam(name= ProviderConstants.MDM_BATCH_RUN_CRITERIA,min = 0 , max = 1) StringType theCriteria, - ServletRequestDetails theRequestDetails) { - String criteria = convertStringTypeToString(theCriteria); - String resourceType = convertStringTypeToString(theResourceType); - - long submittedCount; - if (resourceType != null) { - submittedCount = myMdmSubmitSvc.submitSourceResourceTypeToMdm(resourceType, criteria); - } else { - submittedCount = myMdmSubmitSvc.submitAllSourceTypesToMdm(criteria); - } - return buildMdmOutParametersWithCount(submittedCount); - } - - private String convertStringTypeToString(StringType theCriteria) { - return theCriteria == null ? null : theCriteria.getValueAsString(); - } - - @Operation(name = ProviderConstants.OPERATION_MDM_SUBMIT, idempotent = false, type = Patient.class, returnParameters = { - @OperationParam(name = ProviderConstants.OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class) - }) - public Parameters mdmBatchPatientInstance( - @IdParam IIdType theIdParam, - RequestDetails theRequest) { - long submittedCount = myMdmSubmitSvc.submitSourceResourceToMdm(theIdParam); - return buildMdmOutParametersWithCount(submittedCount); - } - - @Operation(name = ProviderConstants.OPERATION_MDM_SUBMIT, idempotent = false, type = Patient.class, returnParameters = { - @OperationParam(name = ProviderConstants.OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class) - }) - public Parameters mdmBatchPatientType( - @OperationParam(name = ProviderConstants.MDM_BATCH_RUN_CRITERIA) StringType theCriteria, - RequestDetails theRequest) { - String criteria = convertStringTypeToString(theCriteria); - long submittedCount = myMdmSubmitSvc.submitPatientTypeToMdm(criteria); - return buildMdmOutParametersWithCount(submittedCount); - } - - @Operation(name = ProviderConstants.OPERATION_MDM_SUBMIT, idempotent = false, type = Practitioner.class, returnParameters = { - @OperationParam(name = ProviderConstants.OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class) - }) - public Parameters mdmBatchPractitionerInstance( - @IdParam IIdType theIdParam, - RequestDetails theRequest) { - long submittedCount = myMdmSubmitSvc.submitSourceResourceToMdm(theIdParam); - return buildMdmOutParametersWithCount(submittedCount); - } - - @Operation(name = ProviderConstants.OPERATION_MDM_SUBMIT, idempotent = false, type = Practitioner.class, returnParameters = { - @OperationParam(name = ProviderConstants.OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class) - }) - public Parameters mdmBatchPractitionerType( - @OperationParam(name = ProviderConstants.MDM_BATCH_RUN_CRITERIA) StringType theCriteria, - RequestDetails theRequest) { - String criteria = convertStringTypeToString(theCriteria); - long submittedCount = myMdmSubmitSvc.submitPractitionerTypeToMdm(criteria); - return buildMdmOutParametersWithCount(submittedCount); - } - - /** - * Helper function to build the out-parameters for all batch MDM operations. - */ - private Parameters buildMdmOutParametersWithCount(long theCount) { - Parameters parameters = new Parameters(); - parameters.addParameter() - .setName(ProviderConstants.OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT) - .setValue(new DecimalType(theCount)); - return parameters; - } - - - - @Nonnull - protected MatchGrade getMatchGrade(MatchedTarget theTheMatchedTarget) { - MatchGrade matchGrade = MatchGrade.PROBABLE; - if (theTheMatchedTarget.isMatch()) { - matchGrade = MatchGrade.CERTAIN; - } else if (theTheMatchedTarget.isPossibleMatch()) { - matchGrade = MatchGrade.POSSIBLE; - } - return matchGrade; - } - - private String getResourceType(String theParamName, StringType theResourceId) { - if (theResourceId != null) { - IIdType idType = MdmControllerUtil.getGoldenIdDtOrThrowException(theParamName, theResourceId.getValueNotNull()); - return idType.getResourceType(); - } else { - return MdmConstants.UNKNOWN_MDM_TYPES; - } - } -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderR4.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderR4.java deleted file mode 100644 index 55a4587f11f..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/provider/MdmProviderR4.java +++ /dev/null @@ -1,317 +0,0 @@ -package ca.uhn.fhir.mdm.provider; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.MdmConstants; -import ca.uhn.fhir.mdm.api.MdmLinkJson; -import ca.uhn.fhir.mdm.api.IMdmControllerSvc; -import ca.uhn.fhir.mdm.api.IMdmExpungeSvc; -import ca.uhn.fhir.mdm.api.IMdmMatchFinderSvc; -import ca.uhn.fhir.mdm.api.IMdmSubmitSvc; -import ca.uhn.fhir.mdm.api.MatchedTarget; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationParam; -import ca.uhn.fhir.rest.api.server.RequestDetails; -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 ca.uhn.fhir.util.ParametersUtil; -import org.apache.commons.lang3.StringUtils; -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.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.CodeType; -import org.hl7.fhir.r4.model.DecimalType; -import org.hl7.fhir.r4.model.InstantType; -import org.hl7.fhir.r4.model.IntegerType; -import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.Practitioner; -import org.hl7.fhir.r4.model.Resource; -import org.hl7.fhir.r4.model.StringType; -import org.hl7.fhir.r4.model.codesystems.MatchGrade; - -import javax.annotation.Nonnull; -import java.util.Comparator; -import java.util.List; -import java.util.UUID; -import java.util.stream.Stream; - -public class MdmProviderR4 extends BaseMdmProvider { - private final IMdmControllerSvc myMdmControllerSvc; - private final IMdmMatchFinderSvc myMdmMatchFinderSvc; - private final IMdmExpungeSvc myMdmExpungeSvc; - private final IMdmSubmitSvc myMdmSubmitSvc; - - /** - * Constructor - * - * Note that this is not a spring bean. Any necessary injections should - * happen in the constructor - */ - public MdmProviderR4(FhirContext theFhirContext, IMdmControllerSvc theMdmControllerSvc, IMdmMatchFinderSvc theMdmMatchFinderSvc, IMdmExpungeSvc theMdmExpungeSvc, IMdmSubmitSvc theMdmSubmitSvc) { - super(theFhirContext); - myMdmControllerSvc = theMdmControllerSvc; - myMdmMatchFinderSvc = theMdmMatchFinderSvc; - myMdmExpungeSvc = theMdmExpungeSvc; - myMdmSubmitSvc = theMdmSubmitSvc; - } - - @Operation(name = ProviderConstants.EMPI_MATCH, type = Patient.class) - public Bundle match(@OperationParam(name = ProviderConstants.MDM_MATCH_RESOURCE, min = 1, max = 1) Patient thePatient) { - if (thePatient == null) { - throw new InvalidRequestException("resource may not be null"); - } - return getMatchesAndPossibleMatchesForResource(thePatient, "Patient"); - } - - @Operation(name = ProviderConstants.MDM_MATCH) - public Bundle serverMatch(@OperationParam(name = ProviderConstants.MDM_MATCH_RESOURCE, min = 1, max = 1) IAnyResource theResource, - @OperationParam(name = ProviderConstants.MDM_RESOURCE_TYPE, min = 1, max = 1) StringType theResourceType - ) { - if (theResource == null) { - throw new InvalidRequestException("resource may not be null"); - } - return getMatchesAndPossibleMatchesForResource(theResource, theResourceType.getValueNotNull()); - } - - /** - * Helper method which will return a bundle of all Matches and Possible Matches. - */ - private Bundle getMatchesAndPossibleMatchesForResource(IAnyResource theResource, String theResourceType) { - List matches = myMdmMatchFinderSvc.getMatchedTargets(theResourceType, theResource); - - matches.sort(Comparator.comparing((MatchedTarget m) -> m.getMatchResult().getNormalizedScore()).reversed()); - - Bundle retVal = new Bundle(); - retVal.setType(Bundle.BundleType.SEARCHSET); - retVal.setId(UUID.randomUUID().toString()); - retVal.getMeta().setLastUpdatedElement(InstantType.now()); - - for (MatchedTarget next : matches) { - boolean shouldKeepThisEntry = next.isMatch() || next.isPossibleMatch(); - if (!shouldKeepThisEntry) { - continue; - } - - Bundle.BundleEntryComponent entry = new Bundle.BundleEntryComponent(); - entry.setResource((Resource) next.getTarget()); - entry.setSearch(toBundleEntrySearchComponent(next)); - - retVal.addEntry(entry); - } - return retVal; - } - - - private Bundle.BundleEntrySearchComponent toBundleEntrySearchComponent(MatchedTarget theMatchedTarget) { - Bundle.BundleEntrySearchComponent searchComponent = new Bundle.BundleEntrySearchComponent(); - searchComponent.setMode(Bundle.SearchEntryMode.MATCH); - searchComponent.setScore(theMatchedTarget.getMatchResult().getNormalizedScore()); - - MatchGrade matchGrade = getMatchGrade(theMatchedTarget); - - searchComponent.addExtension(MdmConstants.FIHR_STRUCTURE_DEF_MATCH_GRADE_URL_NAMESPACE, new CodeType(matchGrade.toCode())); - return searchComponent; - } - - @Operation(name = ProviderConstants.MDM_MERGE_GOLDEN_RESOURCES) - public IBaseResource mergeGoldenResources(@OperationParam(name=ProviderConstants.MDM_MERGE_GR_FROM_GOLDEN_RESOURCE_ID, min = 1, max = 1) StringType theFromGoldenResourceId, - @OperationParam(name=ProviderConstants.MDM_MERGE_GR_TO_GOLDEN_RESOURCE_ID, min = 1, max = 1) StringType theToGoldenResourceId, - RequestDetails theRequestDetails) { - validateMergeParameters(theFromGoldenResourceId, theToGoldenResourceId); - - return myMdmControllerSvc.mergeGoldenResources(theFromGoldenResourceId.getValue(), theToGoldenResourceId.getValue(), - createMdmContext(theRequestDetails, MdmTransactionContext.OperationType.MERGE_GOLDEN_RESOURCES, - getResourceType(ProviderConstants.MDM_MERGE_GR_FROM_GOLDEN_RESOURCE_ID, theFromGoldenResourceId)) - ); - } - - @Operation(name = ProviderConstants.MDM_UPDATE_LINK) - public IBaseResource updateLink(@OperationParam(name=ProviderConstants.MDM_UPDATE_LINK_GOLDEN_RESOURCE_ID, min = 1, max = 1) StringType theGoldenResourceId, - @OperationParam(name=ProviderConstants.MDM_UPDATE_LINK_RESOURCE_ID, min = 1, max = 1) StringType theResourceId, - @OperationParam(name=ProviderConstants.MDM_UPDATE_LINK_MATCH_RESULT, min = 1, max = 1) StringType theMatchResult, - ServletRequestDetails theRequestDetails) { - validateUpdateLinkParameters(theGoldenResourceId, theResourceId, theMatchResult); - return myMdmControllerSvc.updateLink(theGoldenResourceId.getValueNotNull(), theResourceId.getValue(), theMatchResult.getValue(), - createMdmContext(theRequestDetails, MdmTransactionContext.OperationType.UPDATE_LINK, - getResourceType(ProviderConstants.MDM_UPDATE_LINK_GOLDEN_RESOURCE_ID, theGoldenResourceId)) - ); - } - - @Operation(name = ProviderConstants.MDM_CLEAR, returnParameters = { - @OperationParam(name = ProviderConstants.OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type=DecimalType.class) - }) - public Parameters clearMdmLinks(@OperationParam(name=ProviderConstants.MDM_CLEAR_SOURCE_TYPE, min = 0, max = 1) StringType theSourceType, - ServletRequestDetails theRequestDetails) { - long resetCount; - if (theSourceType == null || StringUtils.isBlank(theSourceType.getValue())) { - resetCount = myMdmExpungeSvc.expungeAllMdmLinks(theRequestDetails); - } else { - resetCount = myMdmExpungeSvc.expungeAllMdmLinksOfSourceType(theSourceType.getValueNotNull(), theRequestDetails); - } - Parameters parameters = new Parameters(); - parameters.addParameter().setName(ProviderConstants.OPERATION_MDM_CLEAR_OUT_PARAM_DELETED_COUNT) - .setValue(new DecimalType(resetCount)); - return parameters; - } - - - @Operation(name = ProviderConstants.MDM_QUERY_LINKS, idempotent = true) - public Parameters queryLinks(@OperationParam(name=ProviderConstants.MDM_QUERY_LINKS_GOLDEN_RESOURCE_ID, min = 0, max = 1) StringType theGoldenResourceId, - @OperationParam(name=ProviderConstants.MDM_QUERY_LINKS_RESOURCE_ID, min = 0, max = 1) StringType theResourceId, - @OperationParam(name=ProviderConstants.MDM_QUERY_LINKS_MATCH_RESULT, min = 0, max = 1) StringType theMatchResult, - @OperationParam(name=ProviderConstants.MDM_QUERY_LINKS_LINK_SOURCE, min = 0, max = 1) StringType theLinkSource, - ServletRequestDetails theRequestDetails) { - - Stream mdmLinkJson = myMdmControllerSvc.queryLinks(extractStringOrNull(theGoldenResourceId), - extractStringOrNull(theResourceId), extractStringOrNull(theMatchResult), extractStringOrNull(theLinkSource), - createMdmContext(theRequestDetails, MdmTransactionContext.OperationType.QUERY_LINKS, - getResourceType(ProviderConstants.MDM_QUERY_LINKS_GOLDEN_RESOURCE_ID, theGoldenResourceId)) - ); - return (Parameters) parametersFromMdmLinks(mdmLinkJson, true); - } - - @Operation(name = ProviderConstants.MDM_DUPLICATE_GOLDEN_RESOURCES, idempotent = true) - public Parameters getDuplicateGoldenResources(ServletRequestDetails theRequestDetails) { - Stream possibleDuplicates = myMdmControllerSvc.getDuplicateGoldenResources( - createMdmContext(theRequestDetails, MdmTransactionContext.OperationType.DUPLICATE_GOLDEN_RESOURCES, (String) null) - ); - return (Parameters) parametersFromMdmLinks(possibleDuplicates, false); - } - - @Operation(name = ProviderConstants.MDM_NOT_DUPLICATE) - public Parameters notDuplicate(@OperationParam(name=ProviderConstants.MDM_QUERY_LINKS_GOLDEN_RESOURCE_ID, min = 1, max = 1) StringType theGoldenResourceId, - @OperationParam(name=ProviderConstants.MDM_QUERY_LINKS_RESOURCE_ID, min = 1, max = 1) StringType theResourceId, - ServletRequestDetails theRequestDetails) { - - validateNotDuplicateParameters(theGoldenResourceId, theResourceId); - myMdmControllerSvc.notDuplicateGoldenResource(theGoldenResourceId.getValue(), theResourceId.getValue(), - createMdmContext(theRequestDetails, MdmTransactionContext.OperationType.NOT_DUPLICATE, - getResourceType(ProviderConstants.MDM_QUERY_LINKS_GOLDEN_RESOURCE_ID, theGoldenResourceId)) - ); - - Parameters retval = (Parameters) ParametersUtil.newInstance(myFhirContext); - ParametersUtil.addParameterToParametersBoolean(myFhirContext, retval, "success", true); - return retval; - } - - @Operation(name = ProviderConstants.OPERATION_MDM_SUBMIT, idempotent = false, returnParameters = { - @OperationParam(name = ProviderConstants.OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type= IntegerType.class) - }) - public Parameters mdmBatchOnAllSourceResources( - @OperationParam(name= ProviderConstants.MDM_BATCH_RUN_RESOURCE_TYPE, min = 0 , max = 1) StringType theResourceType, - @OperationParam(name= ProviderConstants.MDM_BATCH_RUN_CRITERIA,min = 0 , max = 1) StringType theCriteria, - ServletRequestDetails theRequestDetails) { - String criteria = convertStringTypeToString(theCriteria); - String resourceType = convertStringTypeToString(theResourceType); - - long submittedCount; - if (resourceType != null) { - submittedCount = myMdmSubmitSvc.submitSourceResourceTypeToMdm(resourceType, criteria); - } else { - submittedCount = myMdmSubmitSvc.submitAllSourceTypesToMdm(criteria); - } - return buildMdmOutParametersWithCount(submittedCount); - } - - private String convertStringTypeToString(StringType theCriteria) { - return theCriteria == null ? null : theCriteria.getValueAsString(); - } - - - @Operation(name = ProviderConstants.OPERATION_MDM_SUBMIT, idempotent = false, type = Patient.class, returnParameters = { - @OperationParam(name = ProviderConstants.OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class) - }) - public Parameters mdmBatchPatientInstance( - @IdParam IIdType theIdParam, - RequestDetails theRequest) { - long submittedCount = myMdmSubmitSvc.submitSourceResourceToMdm(theIdParam); - return buildMdmOutParametersWithCount(submittedCount); - } - - @Operation(name = ProviderConstants.OPERATION_MDM_SUBMIT, idempotent = false, type = Patient.class, returnParameters = { - @OperationParam(name = ProviderConstants.OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class) - }) - public Parameters mdmBatchPatientType( - @OperationParam(name = ProviderConstants.MDM_BATCH_RUN_CRITERIA) StringType theCriteria, - RequestDetails theRequest) { - String criteria = convertStringTypeToString(theCriteria); - long submittedCount = myMdmSubmitSvc.submitPatientTypeToMdm(criteria); - return buildMdmOutParametersWithCount(submittedCount); - } - - @Operation(name = ProviderConstants.OPERATION_MDM_SUBMIT, idempotent = false, type = Practitioner.class, returnParameters = { - @OperationParam(name = ProviderConstants.OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class) - }) - public Parameters mdmBatchPractitionerInstance( - @IdParam IIdType theIdParam, - RequestDetails theRequest) { - long submittedCount = myMdmSubmitSvc.submitSourceResourceToMdm(theIdParam); - return buildMdmOutParametersWithCount(submittedCount); - } - - @Operation(name = ProviderConstants.OPERATION_MDM_SUBMIT, idempotent = false, type = Practitioner.class, returnParameters = { - @OperationParam(name = ProviderConstants.OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class) - }) - public Parameters mdmBatchPractitionerType( - @OperationParam(name = ProviderConstants.MDM_BATCH_RUN_CRITERIA) StringType theCriteria, - RequestDetails theRequest) { - String criteria = convertStringTypeToString(theCriteria); - long submittedCount = myMdmSubmitSvc.submitPractitionerTypeToMdm(criteria); - return buildMdmOutParametersWithCount(submittedCount); - } - - /** - * Helper function to build the out-parameters for all batch MDM operations. - */ - private Parameters buildMdmOutParametersWithCount(long theCount) { - Parameters parameters = new Parameters(); - parameters.addParameter() - .setName(ProviderConstants.OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT) - .setValue(new DecimalType(theCount)); - return parameters; - } - - @Nonnull - protected MatchGrade getMatchGrade(MatchedTarget theTheMatchedTarget) { - MatchGrade matchGrade = MatchGrade.PROBABLE; - if (theTheMatchedTarget.isMatch()) { - matchGrade = MatchGrade.CERTAIN; - } else if (theTheMatchedTarget.isPossibleMatch()) { - matchGrade = MatchGrade.POSSIBLE; - } - return matchGrade; - } - - private String getResourceType(String theParamName, StringType theResourceId) { - if (theResourceId != null) { - IIdType idType = MdmControllerUtil.getGoldenIdDtOrThrowException(theParamName, theResourceId.getValueNotNull()); - return idType.getResourceType(); - } else { - return MdmConstants.UNKNOWN_MDM_TYPES; - } - } -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/svc/MdmResourceMatcherSvc.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/svc/MdmResourceMatcherSvc.java deleted file mode 100644 index 4d0320f188d..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/svc/MdmResourceMatcherSvc.java +++ /dev/null @@ -1,152 +0,0 @@ -package ca.uhn.fhir.mdm.rules.svc; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.context.ConfigurationException; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.MdmConstants; -import ca.uhn.fhir.mdm.api.MdmMatchEvaluation; -import ca.uhn.fhir.mdm.api.MdmMatchOutcome; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.api.IMdmSettings; -import ca.uhn.fhir.mdm.log.Logs; -import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson; -import ca.uhn.fhir.mdm.rules.json.MdmRulesJson; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.slf4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.annotation.PostConstruct; -import java.util.ArrayList; -import java.util.List; - -/** - * The MdmResourceComparator is in charge of performing actual comparisons between left and right records. - * It does so by calling individual comparators, and returning a vector based on the combination of - * field comparators that matched. - */ - -@Service -public class MdmResourceMatcherSvc { - private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); - - private final FhirContext myFhirContext; - private final IMdmSettings myMdmSettings; - private MdmRulesJson myMdmRulesJson; - private final List myFieldMatchers = new ArrayList<>(); - - @Autowired - public MdmResourceMatcherSvc(FhirContext theFhirContext, IMdmSettings theMdmRules) { - myFhirContext = theFhirContext; - myMdmSettings = theMdmRules; - } - - @PostConstruct - public void init() { - myMdmRulesJson = myMdmSettings.getMdmRules(); - if (myMdmRulesJson == null) { - throw new ConfigurationException("Failed to load MDM Rules. If MDM is enabled, then MDM rules must be available in context."); - } - for (MdmFieldMatchJson matchFieldJson : myMdmRulesJson.getMatchFields()) { - myFieldMatchers.add(new MdmResourceFieldMatcher( myFhirContext, matchFieldJson, myMdmRulesJson)); - } - - } - - /** - * Given two {@link IBaseResource}s, perform all comparisons on them to determine an {@link MdmMatchResultEnum}, indicating - * to what level the two resources are considered to be matching. - * - * @param theLeftResource The first {@link IBaseResource}. - * @param theRightResource The second {@link IBaseResource} - * - * @return an {@link MdmMatchResultEnum} indicating the result of the comparison. - */ - public MdmMatchOutcome getMatchResult(IBaseResource theLeftResource, IBaseResource theRightResource) { - return match(theLeftResource, theRightResource); - } - - MdmMatchOutcome match(IBaseResource theLeftResource, IBaseResource theRightResource) { - MdmMatchOutcome matchResult = getMatchOutcome(theLeftResource, theRightResource); - MdmMatchResultEnum matchResultEnum = myMdmRulesJson.getMatchResult(matchResult.vector); - matchResult.setMatchResultEnum(matchResultEnum); - if (ourLog.isDebugEnabled()) { - if (matchResult.isMatch() || matchResult.isPossibleMatch()) { - ourLog.debug("{} {} with field matchers {}", matchResult, theRightResource.getIdElement().toUnqualifiedVersionless(), myMdmRulesJson.getFieldMatchNamesForVector(matchResult.vector)); - } else if (ourLog.isTraceEnabled()) { - ourLog.trace("{} {}. Field matcher results: {}", matchResult, theRightResource.getIdElement().toUnqualifiedVersionless(), myMdmRulesJson.getDetailedFieldMatchResultForUnmatchedVector(matchResult.vector)); - } - } - return matchResult; - } - - /** - * This function generates a `match vector`, which is a long representation of a binary string - * generated by the results of each of the given comparator matches. For example. - * start with a binary representation of the value 0 for long: 0000 - * first_name matches, so the value `1` is bitwise-ORed to the current value (0) in right-most position. - * `0001` - * - * Next, we look at the second field comparator, and see if it matches. If it does, we left-shift 1 by the index - * of the comparator, in this case also 1. - * `0010` - * - * Then, we bitwise-or it with the current retval: - * 0001|0010 = 0011 - * The binary string is now `0011`, which when you return it as a long becomes `3`. - */ - private MdmMatchOutcome getMatchOutcome(IBaseResource theLeftResource, IBaseResource theRightResource) { - long vector = 0; - double score = 0.0; - int appliedRuleCount = 0; - - //TODO GGG MDM: This grabs ALL comparators, not just the ones we care about (e.g. the ones for Medication) - String resourceType = myFhirContext.getResourceType(theLeftResource); - - for (int i = 0; i < myFieldMatchers.size(); ++i) { - //any that are not for the resourceType in question. - MdmResourceFieldMatcher fieldComparator = myFieldMatchers.get(i); - if (!isValidResourceType(resourceType, fieldComparator.getResourceType())) { - ourLog.debug("Matcher {} is not valid for resource type: {}. Skipping it.", fieldComparator.getName(), resourceType); - continue; - } - ourLog.debug("Matcher {} is valid for resource type: {}. Evaluating match.", fieldComparator.getName(), resourceType); - MdmMatchEvaluation matchEvaluation = fieldComparator.match(theLeftResource, theRightResource); - if (matchEvaluation.match) { - vector |= (1 << i); - } - score += matchEvaluation.score; - appliedRuleCount += 1; - } - - MdmMatchOutcome retVal = new MdmMatchOutcome(vector, score); - retVal.setMdmRuleCount(appliedRuleCount); - return retVal; - } - - private boolean isValidResourceType(String theResourceType, String theFieldComparatorType) { - return ( - theFieldComparatorType.equalsIgnoreCase(MdmConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE) - || theFieldComparatorType.equalsIgnoreCase(theResourceType) - ); - } -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/GoldenResourceHelper.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/GoldenResourceHelper.java deleted file mode 100644 index fd1b3851232..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/GoldenResourceHelper.java +++ /dev/null @@ -1,278 +0,0 @@ -package ca.uhn.fhir.mdm.util; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.context.BaseRuntimeChildDefinition; -import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.fhirpath.IFhirPath; -import ca.uhn.fhir.mdm.api.IMdmSettings; -import ca.uhn.fhir.mdm.log.Logs; -import ca.uhn.fhir.mdm.model.CanonicalEID; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.util.FhirTerser; -import org.hl7.fhir.instance.model.api.IAnyResource; -import org.hl7.fhir.instance.model.api.IBase; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IPrimitiveType; -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.Objects; -import java.util.Optional; -import java.util.stream.Collectors; - -import static ca.uhn.fhir.context.FhirVersionEnum.DSTU3; -import static ca.uhn.fhir.context.FhirVersionEnum.R4; - -@Service -public class GoldenResourceHelper { - - private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); - - static final String FIELD_NAME_IDENTIFIER = "identifier"; - - @Autowired - private IMdmSettings myMdmSettings; - @Autowired - private EIDHelper myEIDHelper; - - - private final FhirContext myFhirContext; - - @Autowired - public GoldenResourceHelper(FhirContext theFhirContext) { - myFhirContext = theFhirContext; - } - - /** - * Creates a copy of the specified resource. This method will carry over resource EID if it exists. If it does not exist, - * a randomly generated UUID EID will be created. - * - * @param Supported MDM resource type (e.g. Patient, Practitioner) - * @param theIncomingResource The resource that will be used as the starting point for the MDM linking. - */ - public T createGoldenResourceFromMdmSourceResource(T theIncomingResource) { - validateContextSupported(); - - // get a ref to the actual ID Field - RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theIncomingResource); - IBaseResource newGoldenResource = resourceDefinition.newInstance(); - - // hapi has 2 metamodels: for children and types - BaseRuntimeChildDefinition goldenResourceIdentifier = resourceDefinition.getChildByName(FIELD_NAME_IDENTIFIER); - - cloneAllExternalEidsIntoNewGoldenResource(goldenResourceIdentifier, theIncomingResource, newGoldenResource); - - addHapiEidIfNoExternalEidIsPresent(newGoldenResource, goldenResourceIdentifier, theIncomingResource); - - MdmResourceUtil.setMdmManaged(newGoldenResource); - MdmResourceUtil.setGoldenResource(newGoldenResource); - - return (T) newGoldenResource; - } - - /** - * If there are no external EIDs on the incoming resource, create a new HAPI EID on the new Golden Resource. - */ - //TODO GGG ask james if there is any way we can convert this canonical EID into a generic STU-agnostic IBase. - private void addHapiEidIfNoExternalEidIsPresent( - IBaseResource theNewGoldenResource, BaseRuntimeChildDefinition theGoldenResourceIdentifier, IAnyResource theSourceResource) { - - List eidsToApply = myEIDHelper.getExternalEid(theNewGoldenResource); - if (!eidsToApply.isEmpty()) { - return; - } - - CanonicalEID hapiEid = myEIDHelper.createHapiEid(); - theGoldenResourceIdentifier.getMutator().addValue(theNewGoldenResource, IdentifierUtil.toId(myFhirContext, hapiEid)); - - // set identifier on the source resource - TerserUtil.cloneEidIntoResource(myFhirContext, theSourceResource, hapiEid); - } - - private void cloneAllExternalEidsIntoNewGoldenResource(BaseRuntimeChildDefinition theGoldenResourceIdentifier, - IBase theGoldenResource, IBase theNewGoldenResource) { - // FHIR choice types - fields within fhir where we have a choice of ids - IFhirPath fhirPath = myFhirContext.newFhirPath(); - List goldenResourceIdentifiers = theGoldenResourceIdentifier.getAccessor().getValues(theGoldenResource); - - for (IBase base : goldenResourceIdentifiers) { - Optional system = fhirPath.evaluateFirst(base, "system", IPrimitiveType.class); - if (system.isPresent()) { - String mdmSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystem(); - String baseSystem = system.get().getValueAsString(); - if (Objects.equals(baseSystem, mdmSystem)) { - TerserUtil.cloneEidIntoResource(myFhirContext, theGoldenResourceIdentifier, base, theNewGoldenResource); - ourLog.debug("System {} differs from system in the MDM rules {}", baseSystem, mdmSystem); - } - } else { - ourLog.debug("System is missing, skipping"); - } - } - } - - private void validateContextSupported() { - FhirVersionEnum fhirVersion = myFhirContext.getVersion().getVersion(); - if (fhirVersion == R4 || fhirVersion == DSTU3) { - return; - } - throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); - } - - /** - * Updates EID on Golden Resource, based on the incoming source resource. If the incoming resource has an external EID, it is applied - * to the Golden Resource, unless that golden resource already has an external EID which does not match, in which case throw {@link IllegalArgumentException} - *

- * If running in multiple EID mode, then incoming EIDs are simply added to the Golden Resource without checking for matches. - * - * @param theGoldenResource The golden resource to update the external EID on. - * @param theSourceResource The source we will retrieve the external EID from. - * @return the modified {@link IBaseResource} representing the Golden Resource. - */ - public IAnyResource updateGoldenResourceExternalEidFromSourceResource(IAnyResource theGoldenResource, IAnyResource - theSourceResource, MdmTransactionContext theMdmTransactionContext) { - //This handles overwriting an automatically assigned EID if a patient that links is coming in with an official EID. - List incomingSourceEid = myEIDHelper.getExternalEid(theSourceResource); - List goldenResourceOfficialEid = myEIDHelper.getExternalEid(theGoldenResource); - - if (incomingSourceEid.isEmpty()) { - return theGoldenResource; - } - - if (goldenResourceOfficialEid.isEmpty() || !myMdmSettings.isPreventMultipleEids()) { - log(theMdmTransactionContext, "Incoming resource:" + theSourceResource.getIdElement().toUnqualifiedVersionless() + " + with EID " + incomingSourceEid.stream().map(CanonicalEID::toString).collect(Collectors.joining(",")) - + " is applying this EIDs to its related Source Resource, as this Source Resource does not yet have an external EID"); - addCanonicalEidsToGoldenResourceIfAbsent(theGoldenResource, incomingSourceEid); - } else if (!goldenResourceOfficialEid.isEmpty() && myEIDHelper.eidMatchExists(goldenResourceOfficialEid, incomingSourceEid)) { - log(theMdmTransactionContext, "incoming resource:" + theSourceResource.getIdElement().toVersionless() + " with EIDs " + incomingSourceEid.stream().map(CanonicalEID::toString).collect(Collectors.joining(",")) + " does not need to overwrite Golden Resource, as this EID is already present"); - } else { - throw new IllegalArgumentException( - String.format("Source EIDs %s would create a duplicate golden resource, as EIDs %s already exist!", - incomingSourceEid.toString(), goldenResourceOfficialEid.toString())); - } - return theGoldenResource; - } - - public IBaseResource overwriteExternalEids(IBaseResource theGoldenResource, List theNewEid) { - clearExternalEids(theGoldenResource); - addCanonicalEidsToGoldenResourceIfAbsent(theGoldenResource, theNewEid); - return theGoldenResource; - } - - private void clearExternalEidsFromTheGoldenResource(BaseRuntimeChildDefinition theGoldenResourceIdentifier, IBase theGoldenResource) { - IFhirPath fhirPath = myFhirContext.newFhirPath(); - List goldenResourceIdentifiers = theGoldenResourceIdentifier.getAccessor().getValues(theGoldenResource); - List clonedIdentifiers = new ArrayList<>(); - FhirTerser terser = myFhirContext.newTerser(); - - for (IBase base : goldenResourceIdentifiers) { - Optional system = fhirPath.evaluateFirst(base, "system", IPrimitiveType.class); - if (system.isPresent()) { - String mdmSystem = myMdmSettings.getMdmRules().getEnterpriseEIDSystem(); - String baseSystem = system.get().getValueAsString(); - if (Objects.equals(baseSystem, mdmSystem)) { - ourLog.debug("Found EID confirming to MDM rules {}. It should not be copied, skipping", baseSystem); - continue; - } - } - - BaseRuntimeElementCompositeDefinition childIdentifier = (BaseRuntimeElementCompositeDefinition) - theGoldenResourceIdentifier.getChildByName(FIELD_NAME_IDENTIFIER); - IBase goldenResourceNewIdentifier = childIdentifier.newInstance(); - terser.cloneInto(base, goldenResourceNewIdentifier, true); - - clonedIdentifiers.add(goldenResourceNewIdentifier); - } - - goldenResourceIdentifiers.clear(); - goldenResourceIdentifiers.addAll(clonedIdentifiers); - } - - private void clearExternalEids(IBaseResource theGoldenResource) { - // validate the system - if it's set to EID system - then clear it - type and STU version - validateContextSupported(); - - // get a ref to the actual ID Field - RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theGoldenResource); - BaseRuntimeChildDefinition goldenResourceIdentifier = resourceDefinition.getChildByName(FIELD_NAME_IDENTIFIER); - clearExternalEidsFromTheGoldenResource(goldenResourceIdentifier, theGoldenResource); - } - - /** - * Given a list of incoming External EIDs, and a Golden Resource, apply all the EIDs to this resource, which did not already exist on it. - */ - private void addCanonicalEidsToGoldenResourceIfAbsent(IBaseResource theGoldenResource, List theIncomingSourceExternalEids) { - List goldenResourceExternalEids = myEIDHelper.getExternalEid(theGoldenResource); - - for (CanonicalEID incomingExternalEid : theIncomingSourceExternalEids) { - if (goldenResourceExternalEids.contains(incomingExternalEid)) { - continue; - } else { - TerserUtil.cloneEidIntoResource(myFhirContext, theGoldenResource, incomingExternalEid); - } - } - } - - public void mergeFields(IBaseResource theFromGoldenResource, IBaseResource theToGoldenResource) { - // TODO NG - Revisit when merge rules are defined - TerserUtil.cloneCompositeField(myFhirContext, theFromGoldenResource, theToGoldenResource, FIELD_NAME_IDENTIFIER); - -// switch (myFhirContext.getVersion().getVersion()) { -// case R4: -// mergeR4PersonFields(theFromGoldenResource, theToGoldenResource); -// break; -// case DSTU3: -// mergeDstu3PersonFields(theFromGoldenResource, theToGoldenResource); -// break; -// default: -// throw new UnsupportedOperationException("Version not supported: " + myFhirContext.getVersion().getVersion()); -// } - } - - /** - * An incoming resource is a potential duplicate if it matches a source that has a golden resource with an official - * EID, but the incoming resource also has an EID that does not match. - */ - public boolean isPotentialDuplicate(IAnyResource theExistingGoldenResource, IAnyResource theComparingGoldenResource) { - List externalEidsGoldenResource = myEIDHelper.getExternalEid(theExistingGoldenResource); - List externalEidsResource = myEIDHelper.getExternalEid(theComparingGoldenResource); - return !externalEidsGoldenResource.isEmpty() && !externalEidsResource.isEmpty() && !myEIDHelper.eidMatchExists(externalEidsResource, externalEidsGoldenResource); - } - - private void log(MdmTransactionContext theMdmTransactionContext, String theMessage) { - theMdmTransactionContext.addTransactionLogMessage(theMessage); - ourLog.debug(theMessage); - } - - public void handleExternalEidAddition(IAnyResource theGoldenResource, IAnyResource theSourceResource, MdmTransactionContext - theMdmTransactionContext) { - List eidFromResource = myEIDHelper.getExternalEid(theSourceResource); - if (!eidFromResource.isEmpty()) { - updateGoldenResourceExternalEidFromSourceResource(theGoldenResource, theSourceResource, theMdmTransactionContext); - } - } -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/MdmResourceUtil.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/MdmResourceUtil.java deleted file mode 100644 index 2002922c5a8..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/MdmResourceUtil.java +++ /dev/null @@ -1,128 +0,0 @@ -package ca.uhn.fhir.mdm.util; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.mdm.api.MdmConstants; -import org.hl7.fhir.instance.model.api.IBaseCoding; -import org.hl7.fhir.instance.model.api.IBaseResource; - -import javax.annotation.Nonnull; -import java.util.Optional; - -public final class MdmResourceUtil { - - private MdmResourceUtil() { - } - - /** - * If the resource is tagged as not managed by MDM, return false. Otherwise true. - * - * @param theBaseResource The FHIR resource that is potentially managed by MDM. - * @return A boolean indicating whether MDM can manage this resource. - */ - public static boolean isMdmAllowed(IBaseResource theBaseResource) { - return theBaseResource.getMeta().getTag(MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_NO_MDM_MANAGED) == null; - } - - /** - * Checks for the presence of the MDM-managed tag, indicating the MDM system has ownership - * of this golden resource's links. - * - * @param theBaseResource the resource to check. - * @return a boolean indicating whether or not MDM manages this FHIR resource. - */ - public static boolean isMdmManaged(IBaseResource theBaseResource) { - return resourceHasTag(theBaseResource, MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_HAPI_MDM_MANAGED); - } - - public static boolean isGoldenRecord(IBaseResource theBaseResource) { - return resourceHasTag(theBaseResource, MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS, MdmConstants.CODE_GOLDEN_RECORD); - } - - public static boolean hasGoldenRecordSystemTag(IBaseResource theIBaseResource) { - return resourceHasTagWithSystem(theIBaseResource, MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS); - } - - public static boolean containsTagWithSystem(IBaseResource theBaseResource) { - return resourceHasTagWithSystem(theBaseResource, MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS); - } - - public static boolean isGoldenRecordRedirected(IBaseResource theBaseResource) { - return resourceHasTag(theBaseResource, MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS, MdmConstants.CODE_GOLDEN_RECORD_REDIRECTED); - } - - private static boolean resourceHasTag(IBaseResource theTheBaseResource, String theSystem, String theCode) { - return theTheBaseResource.getMeta().getTag(theSystem, theCode) != null; - } - - private static boolean resourceHasTagWithSystem(IBaseResource theTheBaseResource, String theSystem) { - return theTheBaseResource.getMeta().getTag().stream().anyMatch(tag -> tag.getSystem().equalsIgnoreCase(theSystem)); - } - - private static Optional getTagWithSystem(IBaseResource theResource, String theSystem) { - return theResource.getMeta().getTag().stream().filter(tag -> tag.getSystem().equalsIgnoreCase(theSystem)).findFirst(); - } - - public static void removeTagWithSystem(IBaseResource theResource, String theSystem) { - theResource.getMeta().getTag().removeIf(tag -> tag.getSystem().equalsIgnoreCase(theSystem)); - } - - /** - * Sets the MDM-managed tag, indicating the MDM system has ownership of this - * Resource. No changes are made if resource is already maanged by MDM. - * - * @param theBaseResource resource to set the tag for - * @return Returns resource with the tag set. - */ - public static IBaseResource setMdmManaged(IBaseResource theBaseResource) { - return setTagOnResource(theBaseResource, MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_HAPI_MDM_MANAGED, MdmConstants.DISPLAY_HAPI_MDM_MANAGED); - } - - public static IBaseResource setGoldenResource(IBaseResource theBaseResource) { - return setTagOnResource(theBaseResource, MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS, MdmConstants.CODE_GOLDEN_RECORD, MdmConstants.DISPLAY_GOLDEN_RECORD); - } - - public static IBaseResource setGoldenResourceRedirected(IBaseResource theBaseResource) { - return setTagOnResource(theBaseResource, MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS, MdmConstants.CODE_GOLDEN_RECORD_REDIRECTED, MdmConstants.DISPLAY_GOLDEN_REDIRECT); - } - - /** - * WARNING: This code may _look_ like it replaces in place a code of a tag, but this DOES NOT ACTUALLY WORK!. In reality what will - * happen is a secondary tag will be created with the same system. the only way to actually remove a tag from a resource - * is by calling dao.removeTag(). This logic here is for the case where our representation of the resource still happens to contain - * a reference to a tag, to make sure it isn't double-added. - */ - @Nonnull - private static IBaseResource setTagOnResource(IBaseResource theGoldenResource, String theSystem, String theCode, String theDisplay) { - Optional tagWithSystem = getTagWithSystem(theGoldenResource, theSystem); - if (tagWithSystem.isPresent()) { - tagWithSystem.get().setCode(theCode); - tagWithSystem.get().setDisplay(theDisplay); - } else { - IBaseCoding tag = theGoldenResource.getMeta().addTag(); - tag.setSystem(theSystem); - tag.setCode(theCode); - tag.setDisplay(theDisplay); - - } - return theGoldenResource; - } -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/MessageHelper.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/MessageHelper.java deleted file mode 100644 index e446c3f9771..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/MessageHelper.java +++ /dev/null @@ -1,93 +0,0 @@ -package ca.uhn.fhir.mdm.util; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.api.MdmConstants; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.api.IMdmSettings; -import ca.uhn.fhir.rest.server.provider.ProviderConstants; -import org.hl7.fhir.instance.model.api.IAnyResource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -public class MessageHelper { - - @Autowired - private final IMdmSettings myMdmSettings; - - @Autowired - private final FhirContext myFhirContext; - - public MessageHelper(IMdmSettings theMdmSettings, FhirContext theFhirContext) { - myMdmSettings = theMdmSettings; - myFhirContext = theFhirContext; - } - - public String getMessageForUnmanagedResource() { - return String.format( - "Only MDM managed resources can be merged. MDM managed resources must have the %s tag.", - MdmConstants.CODE_HAPI_MDM_MANAGED); - } - - public String getMessageForUnsupportedResource(String theName, IAnyResource theResource) { - return getMessageForUnsupportedResource(theName, myFhirContext.getResourceType(theResource)); - } - - public String getMessageForUnsupportedResource(String theName, String theResourceType) { - return String.format("Only %s resources can be merged. The %s points to a %s", - myMdmSettings.getSupportedMdmTypes(), theName, theResourceType); - } - - public String getMessageForUnsupportedMatchResult() { - return "Match Result may only be set to " + MdmMatchResultEnum.NO_MATCH + " or " + MdmMatchResultEnum.MATCH; - } - - public String getMessageForUnsupportedFirstArgumentTypeInUpdate(String goldenRecordType) { - return "First argument to " + ProviderConstants.MDM_UPDATE_LINK + " must be a " - + myMdmSettings.getSupportedMdmTypes() + ". Was " + goldenRecordType; - } - - public String getMessageForUnsupportedSecondArgumentTypeInUpdate(String theGoldenRecordType) { - return "First argument to " + ProviderConstants.MDM_UPDATE_LINK + " must be a " - + myMdmSettings.getSupportedMdmTypes() + ". Was " + theGoldenRecordType; - } - - public String getMessageForArgumentTypeMismatchInUpdate(String theGoldenRecordType, String theSourceResourceType) { - return "Arguments to " + ProviderConstants.MDM_UPDATE_LINK + " must be of the same type. Were " + - theGoldenRecordType + " and " + theSourceResourceType; - } - - public String getMessageForUnsupportedSourceResource() { - return "The source resource is marked with the " + MdmConstants.CODE_NO_MDM_MANAGED - + " tag which means it may not be MDM linked."; - } - - public String getMessageForNoLink(IAnyResource theGoldenRecord, IAnyResource theSourceResource) { - return getMessageForNoLink(theGoldenRecord.getIdElement().toVersionless().toString(), - theSourceResource.getIdElement().toVersionless().toString()); - } - - public String getMessageForNoLink(String theGoldenRecord, String theSourceResource) { - return "No link exists between " + theGoldenRecord + " and " + theSourceResource; - } -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/PrimitiveTypeEqualsPredicate.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/PrimitiveTypeEqualsPredicate.java deleted file mode 100644 index 1dd9b4493b6..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/PrimitiveTypeEqualsPredicate.java +++ /dev/null @@ -1,87 +0,0 @@ -package ca.uhn.fhir.mdm.util; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import org.hl7.fhir.instance.model.api.IPrimitiveType; - -import java.lang.reflect.Field; -import java.util.function.BiPredicate; - -public class PrimitiveTypeEqualsPredicate implements BiPredicate { - - @Override - public boolean test(Object theBase1, Object theBase2) { - if (theBase1 == null) { - return theBase2 == null; - } - if (theBase2 == null) { - return false; - } - if (!theBase1.getClass().equals(theBase2.getClass())) { - return false; - } - - for (Field f : theBase1.getClass().getDeclaredFields()) { - Class fieldClass = f.getType(); - - if (!IPrimitiveType.class.isAssignableFrom(fieldClass)) { - continue; - } - - IPrimitiveType val1, val2; - - f.setAccessible(true); - try { - val1 = (IPrimitiveType) f.get(theBase1); - val2 = (IPrimitiveType) f.get(theBase2); - } catch (Exception e) { - // swallow - continue; - } - - if (val1 == null && val2 == null) { - continue; - } - if (val1 == null && val2 != null) { - return false; - } - if (val1 != null && val2 == null) { - return false; - } - - Object actualVal1 = val1.getValue(); - Object actualVal2 = val2.getValue(); - - if (actualVal1 == null && actualVal2 == null) { - continue; - } - if (actualVal1 == null && actualVal2 != null) { - return false; - } - if (!actualVal1.equals(actualVal2)) { - return false; - } - - } - - return true; - } -} diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/TerserUtil.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/TerserUtil.java deleted file mode 100644 index 70c0966ebce..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/TerserUtil.java +++ /dev/null @@ -1,106 +0,0 @@ -package ca.uhn.fhir.mdm.util; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.context.BaseRuntimeChildDefinition; -import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.mdm.model.CanonicalEID; -import ca.uhn.fhir.util.FhirTerser; -import org.hl7.fhir.instance.model.api.IBase; -import org.hl7.fhir.instance.model.api.IBaseResource; - -import java.util.List; - -import static ca.uhn.fhir.mdm.util.GoldenResourceHelper.FIELD_NAME_IDENTIFIER; - -final class TerserUtil { - - private TerserUtil() { - } - - /** - * Clones the specified canonical EID into the identifier field on the resource - * - * @param theFhirContext Context to pull resource definitions from - * @param theResourceToCloneInto Resource to set the EID on - * @param theEid EID to be set - */ - public static void cloneEidIntoResource(FhirContext theFhirContext, IBaseResource theResourceToCloneInto, CanonicalEID theEid) { - // get a ref to the actual ID Field - RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResourceToCloneInto); - // hapi has 2 metamodels: for children and types - BaseRuntimeChildDefinition resourceIdentifier = resourceDefinition.getChildByName(FIELD_NAME_IDENTIFIER); - cloneEidIntoResource(theFhirContext, resourceIdentifier, IdentifierUtil.toId(theFhirContext, theEid), theResourceToCloneInto); - } - - /** - * Given an Child Definition of `identifier`, a R4/DSTU3 EID Identifier, and a new resource, clone the EID into that resources' identifier list. - */ - public static void cloneEidIntoResource(FhirContext theFhirContext, BaseRuntimeChildDefinition theIdentifierDefinition, IBase theEid, IBase theResourceToCloneEidInto) { - // FHIR choice types - fields within fhir where we have a choice of ids - BaseRuntimeElementCompositeDefinition childIdentifier = (BaseRuntimeElementCompositeDefinition) theIdentifierDefinition.getChildByName(FIELD_NAME_IDENTIFIER); - IBase resourceNewIdentifier = childIdentifier.newInstance(); - - FhirTerser terser = theFhirContext.newTerser(); - terser.cloneInto(theEid, resourceNewIdentifier, true); - theIdentifierDefinition.getMutator().addValue(theResourceToCloneEidInto, resourceNewIdentifier); - } - - /** - * Clones specified composite field (collection). Composite field values must confirm to the collections - * contract. - * - * @param theFrom Resource to clone the specified filed from - * @param theTo Resource to clone the specified filed to - * @param field Field name to be copied - */ - public static void cloneCompositeField(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, String field) { - FhirTerser terser = theFhirContext.newTerser(); - - RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theFrom); - BaseRuntimeChildDefinition childDefinition = definition.getChildByName(field); - - List theFromFieldValues = childDefinition.getAccessor().getValues(theFrom); - List theToFieldValues = childDefinition.getAccessor().getValues(theTo); - - for (IBase theFromFieldValue : theFromFieldValues) { - if (contains(theFromFieldValue, theToFieldValues)) { - continue; - } - - BaseRuntimeElementCompositeDefinition compositeDefinition = (BaseRuntimeElementCompositeDefinition) childDefinition.getChildByName(field); - IBase newFieldValue = compositeDefinition.newInstance(); - terser.cloneInto(theFromFieldValue, newFieldValue, true); - - theToFieldValues.add(newFieldValue); - } - } - - private static boolean contains(IBase theItem, List theItems) { - PrimitiveTypeEqualsPredicate predicate = new PrimitiveTypeEqualsPredicate(); - return theItems.stream().anyMatch(i -> { - return predicate.test(i, theItem); - }); - } - -} diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/json/VectorMatchResultMapTest.java b/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/json/VectorMatchResultMapTest.java deleted file mode 100644 index e129cf2f835..00000000000 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/json/VectorMatchResultMapTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package ca.uhn.fhir.mdm.rules.json; - -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.rules.matcher.MdmMatcherEnum; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class VectorMatchResultMapTest { - @Test - public void splitFieldMatchNames() { - { - String[] result = VectorMatchResultMap.splitFieldMatchNames("a,b"); - assertEquals(2, result.length); - assertEquals("a", result[0]); - assertEquals("b", result[1]); - } - - { - String[] result = VectorMatchResultMap.splitFieldMatchNames("a, b"); - assertEquals(2, result.length); - assertEquals("a", result[0]); - assertEquals("b", result[1]); - } - } - - @Test - public void testMatchBeforePossibleMatch() { - MdmRulesJson mdmRulesJson = new MdmRulesJson(); - MdmMatcherJson matcherJson = new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.STRING); - mdmRulesJson.addMatchField(new MdmFieldMatchJson().setName("given").setResourceType("Patient").setResourcePath("name.given").setMatcher(matcherJson)); - mdmRulesJson.addMatchField(new MdmFieldMatchJson().setName("family").setResourceType("Patient").setResourcePath("name.family").setMatcher(matcherJson)); - mdmRulesJson.addMatchField(new MdmFieldMatchJson().setName("prefix").setResourceType("Patient").setResourcePath("name.prefix").setMatcher(matcherJson)); - mdmRulesJson.putMatchResult("given,family", MdmMatchResultEnum.MATCH); - mdmRulesJson.putMatchResult("given", MdmMatchResultEnum.POSSIBLE_MATCH); - - VectorMatchResultMap map = new VectorMatchResultMap(mdmRulesJson); - assertEquals(MdmMatchResultEnum.POSSIBLE_MATCH, map.get(1L)); - assertEquals(MdmMatchResultEnum.MATCH, map.get(3L)); - assertEquals(MdmMatchResultEnum.MATCH, map.get(7L)); - } -} diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/StringMatcherR4Test.java b/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/StringMatcherR4Test.java deleted file mode 100644 index 42508046adc..00000000000 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/StringMatcherR4Test.java +++ /dev/null @@ -1,154 +0,0 @@ -package ca.uhn.fhir.mdm.rules.matcher; - -import org.hl7.fhir.r4.model.BooleanType; -import org.hl7.fhir.r4.model.DateType; -import org.hl7.fhir.r4.model.Enumeration; -import org.hl7.fhir.r4.model.Enumerations; -import org.hl7.fhir.r4.model.StringType; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class StringMatcherR4Test extends BaseMatcherR4Test { - private static final Logger ourLog = LoggerFactory.getLogger(StringMatcherR4Test.class); - public static final String LEFT = "namadega"; - public static final String RIGHT = "namaedga"; - - @Test - public void testNamadega() { - assertTrue(match(MdmMatcherEnum.COLOGNE, LEFT, RIGHT)); - assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, LEFT, RIGHT)); - assertTrue(match(MdmMatcherEnum.MATCH_RATING_APPROACH, LEFT, RIGHT)); - assertTrue(match(MdmMatcherEnum.METAPHONE, LEFT, RIGHT)); - assertTrue(match(MdmMatcherEnum.SOUNDEX, LEFT, RIGHT)); - assertTrue(match(MdmMatcherEnum.METAPHONE, LEFT, RIGHT)); - - assertFalse(match(MdmMatcherEnum.CAVERPHONE1, LEFT, RIGHT)); - assertFalse(match(MdmMatcherEnum.CAVERPHONE2, LEFT, RIGHT)); - assertFalse(match(MdmMatcherEnum.NYSIIS, LEFT, RIGHT)); - assertFalse(match(MdmMatcherEnum.REFINED_SOUNDEX, LEFT, RIGHT)); - assertFalse(match(MdmMatcherEnum.STRING, LEFT, RIGHT)); - assertFalse(match(MdmMatcherEnum.SUBSTRING, LEFT, RIGHT)); - } - - @Test - public void testMetaphone() { - assertTrue(match(MdmMatcherEnum.METAPHONE, "Durie", "dury")); - assertTrue(match(MdmMatcherEnum.METAPHONE, "Balo", "ballo")); - assertTrue(match(MdmMatcherEnum.METAPHONE, "Hans Peter", "Hanspeter")); - assertTrue(match(MdmMatcherEnum.METAPHONE, "Lawson", "Law son")); - - assertFalse(match(MdmMatcherEnum.METAPHONE, "Allsop", "Allsob")); - assertFalse(match(MdmMatcherEnum.METAPHONE, "Gevne", "Geve")); - assertFalse(match(MdmMatcherEnum.METAPHONE, "Bruce", "Bruch")); - assertFalse(match(MdmMatcherEnum.METAPHONE, "Smith", "Schmidt")); - assertFalse(match(MdmMatcherEnum.METAPHONE, "Jyothi", "Jyoti")); - } - - @Test - public void testDoubleMetaphone() { - assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Durie", "dury")); - assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Balo", "ballo")); - assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Hans Peter", "Hanspeter")); - assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Lawson", "Law son")); - assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Allsop", "Allsob")); - - assertFalse(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Gevne", "Geve")); - assertFalse(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Bruce", "Bruch")); - assertFalse(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Smith", "Schmidt")); - assertFalse(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Jyothi", "Jyoti")); - } - - @Test - public void testNormalizeCase() { - assertTrue(match(MdmMatcherEnum.STRING, "joe", "JoE")); - assertTrue(match(MdmMatcherEnum.STRING, "MCTAVISH", "McTavish")); - - assertFalse(match(MdmMatcherEnum.STRING, "joey", "joe")); - assertFalse(match(MdmMatcherEnum.STRING, "joe", "joey")); - } - - @Test - public void testExactString() { - assertTrue(MdmMatcherEnum.STRING.match(ourFhirContext, new StringType("Jilly"), new StringType("Jilly"), true, null)); - - assertFalse(MdmMatcherEnum.STRING.match(ourFhirContext, new StringType("MCTAVISH"), new StringType("McTavish"), true, null)); - assertFalse(MdmMatcherEnum.STRING.match(ourFhirContext, new StringType("Durie"), new StringType("dury"), true, null)); - } - - @Test - public void testExactBoolean() { - assertTrue(MdmMatcherEnum.STRING.match(ourFhirContext, new BooleanType(true), new BooleanType(true), true, null)); - - assertFalse(MdmMatcherEnum.STRING.match(ourFhirContext, new BooleanType(true), new BooleanType(false), true, null)); - assertFalse(MdmMatcherEnum.STRING.match(ourFhirContext, new BooleanType(false), new BooleanType(true), true, null)); - } - - @Test - public void testExactDateString() { - assertTrue(MdmMatcherEnum.STRING.match(ourFhirContext, new DateType("1965-08-09"), new DateType("1965-08-09"), true, null)); - - assertFalse(MdmMatcherEnum.STRING.match(ourFhirContext, new DateType("1965-08-09"), new DateType("1965-09-08"), true, null)); - } - - - @Test - public void testExactGender() { - Enumeration male = new Enumeration(new Enumerations.AdministrativeGenderEnumFactory()); - male.setValue(Enumerations.AdministrativeGender.MALE); - - Enumeration female = new Enumeration(new Enumerations.AdministrativeGenderEnumFactory()); - female.setValue(Enumerations.AdministrativeGender.FEMALE); - - assertTrue(MdmMatcherEnum.STRING.match(ourFhirContext, male, male, true, null)); - - assertFalse(MdmMatcherEnum.STRING.match(ourFhirContext, male, female, true, null)); - } - - @Test - public void testSoundex() { - assertTrue(match(MdmMatcherEnum.SOUNDEX, "Gail", "Gale")); - assertTrue(match(MdmMatcherEnum.SOUNDEX, "John", "Jon")); - assertTrue(match(MdmMatcherEnum.SOUNDEX, "Thom", "Tom")); - - assertFalse(match(MdmMatcherEnum.SOUNDEX, "Fred", "Frank")); - assertFalse(match(MdmMatcherEnum.SOUNDEX, "Thomas", "Tom")); - } - - - @Test - public void testCaverphone1() { - assertTrue(match(MdmMatcherEnum.CAVERPHONE1, "Gail", "Gael")); - assertTrue(match(MdmMatcherEnum.CAVERPHONE1, "John", "Jon")); - - assertFalse(match(MdmMatcherEnum.CAVERPHONE1, "Gail", "Gale")); - assertFalse(match(MdmMatcherEnum.CAVERPHONE1, "Fred", "Frank")); - assertFalse(match(MdmMatcherEnum.CAVERPHONE1, "Thomas", "Tom")); - } - - @Test - public void testCaverphone2() { - assertTrue(match(MdmMatcherEnum.CAVERPHONE2, "Gail", "Gael")); - assertTrue(match(MdmMatcherEnum.CAVERPHONE2, "John", "Jon")); - assertTrue(match(MdmMatcherEnum.CAVERPHONE2, "Gail", "Gale")); - - assertFalse(match(MdmMatcherEnum.CAVERPHONE2, "Fred", "Frank")); - assertFalse(match(MdmMatcherEnum.CAVERPHONE2, "Thomas", "Tom")); - } - - @Test - public void testNormalizeSubstring() { - assertTrue(match(MdmMatcherEnum.SUBSTRING, "BILLY", "Bill")); - assertTrue(match(MdmMatcherEnum.SUBSTRING, "Bill", "Billy")); - assertTrue(match(MdmMatcherEnum.SUBSTRING, "FRED", "Frederik")); - - assertFalse(match(MdmMatcherEnum.SUBSTRING, "Fred", "Friederik")); - } - - private boolean match(MdmMatcherEnum theMatcher, String theLeft, String theRight) { - return theMatcher.match(ourFhirContext, new StringType(theLeft), new StringType(theRight), false, null); - } -} diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/BaseMdmRulesR4Test.java b/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/BaseMdmRulesR4Test.java deleted file mode 100644 index e7ab67d3b52..00000000000 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/BaseMdmRulesR4Test.java +++ /dev/null @@ -1,74 +0,0 @@ -package ca.uhn.fhir.mdm.rules.svc; - -import ca.uhn.fhir.mdm.BaseR4Test; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson; -import ca.uhn.fhir.mdm.rules.json.MdmFilterSearchParamJson; -import ca.uhn.fhir.mdm.rules.json.MdmResourceSearchParamJson; -import ca.uhn.fhir.mdm.rules.json.MdmRulesJson; -import ca.uhn.fhir.mdm.rules.json.MdmSimilarityJson; -import ca.uhn.fhir.mdm.rules.similarity.MdmSimilarityEnum; -import org.hl7.fhir.r4.model.Patient; -import org.junit.jupiter.api.BeforeEach; - -import java.util.ArrayList; -import java.util.Arrays; - -public abstract class BaseMdmRulesR4Test extends BaseR4Test { - public static final String PATIENT_GIVEN = "patient-given"; - public static final String PATIENT_FAMILY = "patient-last"; - - public static final double NAME_THRESHOLD = 0.8; - protected MdmFieldMatchJson myGivenNameMatchField; - protected String myBothNameFields; - protected MdmRulesJson myMdmRulesJson; - - @BeforeEach - public void before() { - myMdmRulesJson = new MdmRulesJson(); - - ArrayList myLegalMdmTypes = new ArrayList<>(); - myLegalMdmTypes.add("Patient"); - myMdmRulesJson.setMdmTypes(myLegalMdmTypes); - - myGivenNameMatchField = new MdmFieldMatchJson() - .setName(PATIENT_GIVEN) - .setResourceType("Patient") - .setResourcePath("name.given") - .setSimilarity(new MdmSimilarityJson().setAlgorithm(MdmSimilarityEnum.COSINE).setMatchThreshold(NAME_THRESHOLD)); - myBothNameFields = String.join(",", PATIENT_GIVEN, PATIENT_FAMILY); - } - - protected MdmRulesJson buildActiveBirthdateIdRules() { - MdmFilterSearchParamJson activePatientsBlockingFilter = new MdmFilterSearchParamJson() - .setResourceType("Patient") - .setSearchParam(Patient.SP_ACTIVE) - .setFixedValue("true"); - - MdmResourceSearchParamJson patientBirthdayBlocking = new MdmResourceSearchParamJson() - .setResourceType("Patient") - .addSearchParam(Patient.SP_BIRTHDATE); - MdmResourceSearchParamJson patientIdentifierBlocking = new MdmResourceSearchParamJson() - .setResourceType("Patient") - .addSearchParam(Patient.SP_IDENTIFIER); - - - MdmFieldMatchJson lastNameMatchField = new MdmFieldMatchJson() - .setName(PATIENT_FAMILY) - .setResourceType("Patient") - .setResourcePath("name.family") - .setSimilarity(new MdmSimilarityJson().setAlgorithm(MdmSimilarityEnum.JARO_WINKLER).setMatchThreshold(NAME_THRESHOLD)); - - MdmRulesJson retval = new MdmRulesJson(); - retval.setVersion("test version"); - retval.addResourceSearchParam(patientBirthdayBlocking); - retval.addResourceSearchParam(patientIdentifierBlocking); - retval.addFilterSearchParam(activePatientsBlockingFilter); - retval.addMatchField(myGivenNameMatchField); - retval.addMatchField(lastNameMatchField); - retval.setMdmTypes(Arrays.asList("Patient", "Practitioner", "Medication")); - retval.putMatchResult(myBothNameFields, MdmMatchResultEnum.MATCH); - retval.putMatchResult(PATIENT_GIVEN, MdmMatchResultEnum.POSSIBLE_MATCH); - return retval; - } -} diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/CustomResourceMatcherR4Test.java b/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/CustomResourceMatcherR4Test.java deleted file mode 100644 index 7232394673e..00000000000 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/CustomResourceMatcherR4Test.java +++ /dev/null @@ -1,132 +0,0 @@ -package ca.uhn.fhir.mdm.rules.svc; - -import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.mdm.BaseR4Test; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson; -import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson; -import ca.uhn.fhir.mdm.rules.json.MdmRulesJson; -import ca.uhn.fhir.mdm.rules.matcher.MdmMatcherEnum; -import org.hl7.fhir.r4.model.HumanName; -import org.hl7.fhir.r4.model.Patient; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class CustomResourceMatcherR4Test extends BaseR4Test { - - public static final String FIELD_EXACT_MATCH_NAME = MdmMatcherEnum.NAME_ANY_ORDER.name(); - private static Patient ourJohnHenry; - private static Patient ourJohnHENRY; - private static Patient ourJaneHenry; - private static Patient ourJohnSmith; - private static Patient ourJohnBillyHenry; - private static Patient ourBillyJohnHenry; - private static Patient ourHenryJohn; - private static Patient ourHenryJOHN; - - @BeforeEach - public void before() { - when(mySearchParamRetriever.getActiveSearchParam("Patient", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); - when(mySearchParamRetriever.getActiveSearchParam("Practitioner", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); - when(mySearchParamRetriever.getActiveSearchParam("Medication", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); - when(mySearchParamRetriever.getActiveSearchParam("AllergyIntolerance", "identifier")).thenReturn(null); - } - - @Test - public void testExactNameAnyOrder() { - MdmResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(MdmMatcherEnum.NAME_ANY_ORDER, true)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry)); - } - - @Test - public void testNormalizedNameAnyOrder() { - MdmResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(MdmMatcherEnum.NAME_ANY_ORDER, false)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry)); - } - - @Test - public void testExactNameFirstAndLast() { - MdmResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(MdmMatcherEnum.NAME_FIRST_AND_LAST, true)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry)); - assertMatchResult(MdmMatchResultEnum.MATCH, 1L, 1.0, false, false, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry)); - } - - @Test - public void testNormalizedNameFirstAndLast() { - MdmResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(MdmMatcherEnum.NAME_FIRST_AND_LAST, false)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry)); - assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry)); - assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry)); - } - - private MdmRulesJson buildNameRules(MdmMatcherEnum theAlgorithm, boolean theExact) { - MdmMatcherJson matcherJson = new MdmMatcherJson().setAlgorithm(theAlgorithm).setExact(theExact); - MdmFieldMatchJson nameAnyOrderFieldMatch = new MdmFieldMatchJson() - .setName(FIELD_EXACT_MATCH_NAME) - .setResourceType("Patient") - .setResourcePath("name") - .setMatcher(matcherJson); - - MdmRulesJson retval = new MdmRulesJson(); - retval.addMatchField(nameAnyOrderFieldMatch); - retval.setMdmTypes(Arrays.asList("Patient", "Practitioner", "Medication")); - retval.putMatchResult(FIELD_EXACT_MATCH_NAME, MdmMatchResultEnum.MATCH); - - return retval; - } - - @BeforeAll - public static void beforeClass() { - ourJohnHenry = buildPatientWithNames("Henry", "John"); - ourJohnHENRY = buildPatientWithNames("HENRY", "John"); - ourJaneHenry = buildPatientWithNames("Henry", "Jane"); - ourJohnSmith = buildPatientWithNames("Smith", "John"); - ourJohnBillyHenry = buildPatientWithNames("Henry", "John", "Billy"); - ourBillyJohnHenry = buildPatientWithNames("Henry", "Billy", "John"); - ourHenryJohn = buildPatientWithNames("John", "Henry"); - ourHenryJOHN = buildPatientWithNames("JOHN", "Henry"); - } - - protected static Patient buildPatientWithNames(String theFamilyName, String... theGivenNames) { - Patient patient = new Patient(); - HumanName name = patient.addName(); - name.setFamily(theFamilyName); - for (String givenName : theGivenNames) { - name.addGiven(givenName); - } - patient.setId("Patient/1"); - return patient; - } -} diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/MdmResourceMatcherSvcR4Test.java b/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/MdmResourceMatcherSvcR4Test.java deleted file mode 100644 index 912348f5980..00000000000 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/svc/MdmResourceMatcherSvcR4Test.java +++ /dev/null @@ -1,61 +0,0 @@ -package ca.uhn.fhir.mdm.rules.svc; - -import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.mdm.api.MdmMatchOutcome; -import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import org.hl7.fhir.r4.model.Patient; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class MdmResourceMatcherSvcR4Test extends BaseMdmRulesR4Test { - private MdmResourceMatcherSvc myMdmResourceMatcherSvc; - private Patient myJohn; - private Patient myJohny; - - @Override - @BeforeEach - public void before() { - super.before(); - - when(mySearchParamRetriever.getActiveSearchParam("Patient", "birthdate")).thenReturn(mock(RuntimeSearchParam.class)); - when(mySearchParamRetriever.getActiveSearchParam("Patient", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); - when(mySearchParamRetriever.getActiveSearchParam("Practitioner", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); - when(mySearchParamRetriever.getActiveSearchParam("Medication", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); - when(mySearchParamRetriever.getActiveSearchParam("Patient", "active")).thenReturn(mock(RuntimeSearchParam.class)); - - myMdmResourceMatcherSvc = buildMatcher(buildActiveBirthdateIdRules()); - - myJohn = buildJohn(); - myJohny = buildJohny(); - } - - @Test - public void testCompareFirstNameMatch() { - MdmMatchOutcome result = myMdmResourceMatcherSvc.match(myJohn, myJohny); - assertMatchResult(MdmMatchResultEnum.POSSIBLE_MATCH, 1L, 0.816, false, false, result); - - } - - @Test - public void testCompareBothNamesMatch() { - myJohn.addName().setFamily("Smith"); - myJohny.addName().setFamily("Smith"); - MdmMatchOutcome result = myMdmResourceMatcherSvc.match(myJohn, myJohny); - assertMatchResult(MdmMatchResultEnum.MATCH, 3L, 1.816, false, false, result); - } - - @Test - public void testMatchResult() { - assertMatchResult(MdmMatchResultEnum.POSSIBLE_MATCH, 1L, 0.816, false, false, myMdmResourceMatcherSvc.getMatchResult(myJohn, myJohny)); - myJohn.addName().setFamily("Smith"); - myJohny.addName().setFamily("Smith"); - assertMatchResult(MdmMatchResultEnum.MATCH, 3L, 1.816, false, false, myMdmResourceMatcherSvc.getMatchResult(myJohn, myJohny)); - Patient patient3 = new Patient(); - patient3.setId("Patient/3"); - patient3.addName().addGiven("Henry"); - assertMatchResult(MdmMatchResultEnum.NO_MATCH, 0L, 0.0, false, false, myMdmResourceMatcherSvc.getMatchResult(myJohn, patient3)); - } -} diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/util/PrimitiveTypeEqualsPredicateTest.java b/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/util/PrimitiveTypeEqualsPredicateTest.java deleted file mode 100644 index 6410faaa8db..00000000000 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/util/PrimitiveTypeEqualsPredicateTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package ca.uhn.fhir.mdm.util; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.util.FhirTerser; -import org.hl7.fhir.instance.model.api.IBase; -import org.hl7.fhir.r4.model.Address; -import org.hl7.fhir.r4.model.BooleanType; -import org.hl7.fhir.r4.model.DateType; -import org.hl7.fhir.r4.model.Enumerations; -import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.Person; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.Collections; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class PrimitiveTypeEqualsPredicateTest { - - private static FhirContext myFhirContext; - - private FhirTerser myTerser; - - private IBase myPositiveTest1; - - private IBase myPositiveTest2; - - private IBase myPositiveTest3; - - private IBase myNegativeTest; - - private PrimitiveTypeEqualsPredicate cut = new PrimitiveTypeEqualsPredicate(); - - @BeforeAll - public static void initContext() { - myFhirContext = FhirContext.forR4(); - } - - @BeforeEach - public void init() { - myTerser = myFhirContext.newTerser(); - - myPositiveTest1 = newPatient(); - myPositiveTest2 = newPatient(); - myPositiveTest3 = newPatient(); - - Patient inactivePatientForNegativeTest = newPatient(); - inactivePatientForNegativeTest.setActive(false); - inactivePatientForNegativeTest.setMultipleBirth(new BooleanType(false)); - myNegativeTest = inactivePatientForNegativeTest; - } - - private Patient newPatient() { - Patient patient; - patient = new Patient(); - patient.setActive(true); - patient.setGender(Enumerations.AdministrativeGender.FEMALE); - patient.setBirthDateElement(new DateType("1901-01-01")); - - Address address = new Address(); - address.addLine("Somwhere"); - address.setCity("Toronto"); - address.setCountry("Canada"); - patient.setAddress(Collections.singletonList(address)); - return patient; - } - - @Test - public void testNegativeMatchOnTheSameType() { - assertFalse(cut.test(myPositiveTest1, myNegativeTest)); - assertFalse(cut.test(myNegativeTest, myPositiveTest1)); - } - - @Test - public void testNegativeMatchOnDifferentTypes() { - Person person = new Person(); - person.addName().addGiven("John"); - assertFalse(cut.test(myNegativeTest, person)); - } - - @Test - public void testNulls() { - assertTrue(cut.test(null, null)); - assertFalse(cut.test(myPositiveTest1, null)); - assertFalse(cut.test(null, myPositiveTest1)); - } - - @Test - public void testPositiveMatchOnTheSameType() { - assertTrue(cut.test(myPositiveTest1, myPositiveTest2)); - assertTrue(cut.test(myPositiveTest1, myPositiveTest1)); - } - -} diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/util/TestUtils.java b/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/util/TestUtils.java deleted file mode 100644 index b6e757f9590..00000000000 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/util/TestUtils.java +++ /dev/null @@ -1,10 +0,0 @@ -package ca.uhn.fhir.mdm.util; - -import ca.uhn.fhir.mdm.model.MdmTransactionContext; - -public class TestUtils { - public static MdmTransactionContext createDummyContext() { - MdmTransactionContext context = new MdmTransactionContext(); - return context; - } -} diff --git a/hapi-fhir-server-mdm/src/test/resources/bad-rules-invalid-path.json b/hapi-fhir-server-mdm/src/test/resources/bad-rules-invalid-path.json deleted file mode 100644 index aa6f4826c37..00000000000 --- a/hapi-fhir-server-mdm/src/test/resources/bad-rules-invalid-path.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "1", - "mdmTypes": ["Organization"], - "candidateSearchParams": [], - "candidateFilterSearchParams": [], - "matchFields": [ - { - "name": "name-prefix", - "resourceType": "Organization", - "resourcePath": "name.prefix", - "matcher": { - "algorithm": "STRING" - } - } - ], - "matchResultMap": {} -} diff --git a/hapi-fhir-server-mdm/src/test/resources/bad-rules-missing-mdm-types.json b/hapi-fhir-server-mdm/src/test/resources/bad-rules-missing-mdm-types.json deleted file mode 100644 index 0096784b26f..00000000000 --- a/hapi-fhir-server-mdm/src/test/resources/bad-rules-missing-mdm-types.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "1", - "candidateSearchParams" : [], - "candidateFilterSearchParams" : [], - "matchFields" : [ { - "name" : "given-name", - "resourceType" : "*", - "resourcePath" : "name.given", - "similarity" : { - "algorithm": "COSINE", - "matchThreshold": 0.8 - } - }], - "matchResultMap" : { - "given-name" : "POSSIBLE_MATCH" - } -} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceMessage.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceMessage.java index 0ed9535a827..8b9241b1ba6 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceMessage.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceMessage.java @@ -136,8 +136,8 @@ public abstract class BaseResourceMessage implements IResourceMessage, IModelJso * Adds a transaction ID to this message. This ID can be used for many purposes. For example, performing tracing * across asynchronous hooks, tying data together, or downstream logging purposes. * - * One current internal implementation uses this field to tie back MDM processing results (which are asynchronous) - * to the original transaction log that caused the MDM processing to occur. + * One current internal implementation uses this field to tie back EMPI processing results (which are asynchronous) + * to the original transaction log that caused the EMPI processing to occur. * * @param theTransactionId An ID representing a transaction of relevance to this message. */ diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index 7a9f9304d41..011cd35a8ae 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -62,35 +62,30 @@ public class ProviderConstants { * EMPI Operations */ public static final String EMPI_MATCH = "$match"; - //TODO GGG MDM: implement a server-level MDM match to complement the FHIR-spec $match for /Patient - public static final String MDM_MATCH = "$mdm-match"; - public static final String MDM_MATCH_RESOURCE = "resource"; - public static final String MDM_RESOURCE_TYPE = "resourceType"; + public static final String EMPI_MATCH_RESOURCE = "resource"; - //TODO GGG MDM: rename all these vars - public static final String MDM_MERGE_GOLDEN_RESOURCES = "$mdm-merge-golden-resources"; - public static final String MDM_MERGE_GR_FROM_GOLDEN_RESOURCE_ID = "fromGoldenResourceId"; - public static final String MDM_MERGE_GR_TO_GOLDEN_RESOURCE_ID = "toGoldenResourceId"; + public static final String EMPI_MERGE_PERSONS = "$empi-merge-persons"; + public static final String EMPI_MERGE_PERSONS_FROM_PERSON_ID = "fromPersonId"; + public static final String EMPI_MERGE_PERSONS_TO_PERSON_ID = "toPersonId"; - public static final String MDM_UPDATE_LINK = "$mdm-update-link"; - public static final String MDM_UPDATE_LINK_GOLDEN_RESOURCE_ID = "goldenResourceId"; - public static final String MDM_UPDATE_LINK_RESOURCE_ID = "resourceId"; - public static final String MDM_UPDATE_LINK_MATCH_RESULT = "matchResult"; + public static final String EMPI_UPDATE_LINK = "$empi-update-link"; + public static final String EMPI_UPDATE_LINK_PERSON_ID = "personId"; + public static final String EMPI_UPDATE_LINK_TARGET_ID = "targetId"; + public static final String EMPI_UPDATE_LINK_MATCH_RESULT = "matchResult"; - public static final String MDM_QUERY_LINKS = "$mdm-query-links"; - public static final String MDM_QUERY_LINKS_GOLDEN_RESOURCE_ID = "goldenResourceId"; - public static final String MDM_QUERY_LINKS_RESOURCE_ID = "resourceId"; - public static final String MDM_QUERY_LINKS_MATCH_RESULT = "matchResult"; - public static final String MDM_QUERY_LINKS_LINK_SOURCE = "linkSource"; + public static final String EMPI_QUERY_LINKS = "$empi-query-links"; + public static final String EMPI_QUERY_LINKS_PERSON_ID = "personId"; + public static final String EMPI_QUERY_LINKS_TARGET_ID = "targetId"; + public static final String EMPI_QUERY_LINKS_MATCH_RESULT = "matchResult"; + public static final String EMPI_QUERY_LINKS_LINK_SOURCE = "linkSource"; - public static final String MDM_DUPLICATE_GOLDEN_RESOURCES = "$mdm-duplicate-golden-resources"; - public static final String MDM_NOT_DUPLICATE = "$mdm-not-duplicate"; + public static final String EMPI_DUPLICATE_PERSONS = "$empi-duplicate-persons"; + public static final String EMPI_NOT_DUPLICATE = "$empi-not-duplicate"; - public static final String MDM_CLEAR = "$mdm-clear"; - public static final String MDM_CLEAR_SOURCE_TYPE = "sourceType"; - public static final String OPERATION_MDM_SUBMIT = "$mdm-submit"; - public static final String MDM_BATCH_RUN_CRITERIA = "criteria" ; - public static final String OPERATION_MDM_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT = "submitted" ; - public static final String OPERATION_MDM_CLEAR_OUT_PARAM_DELETED_COUNT = "deleted"; - public static final String MDM_BATCH_RUN_RESOURCE_TYPE = "resourceType"; + public static final String EMPI_CLEAR = "$empi-clear"; + public static final String EMPI_CLEAR_TARGET_TYPE = "targetType"; + public static final String OPERATION_EMPI_SUBMIT = "$empi-submit"; + public static final String EMPI_BATCH_RUN_CRITERIA= "criteria" ; + public static final String OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT = "submitted" ; + public static final String OPERATION_EMPI_CLEAR_OUT_PARAM_DELETED_COUNT = "deleted"; } diff --git a/lgtm.yml b/lgtm.yml index 4d7a8bd1ca6..58cd15e3e18 100644 --- a/lgtm.yml +++ b/lgtm.yml @@ -5,3 +5,5 @@ extraction: - exclude: "**/jquery*.js" - exclude: "**/bootstrap*.js" - exclude: "**/webapp/fa/*.js" + + diff --git a/pom.xml b/pom.xml index 162aedb4c19..4b6eb0759d6 100644 --- a/pom.xml +++ b/pom.xml @@ -2087,8 +2087,8 @@ - - + + @@ -2099,8 +2099,8 @@ - - + + @@ -2506,7 +2506,7 @@ hapi-tinder-test hapi-fhir-client hapi-fhir-server - hapi-fhir-server-mdm + hapi-fhir-server-empi hapi-fhir-converter hapi-fhir-validation @@ -2531,7 +2531,7 @@ hapi-fhir-jaxrsserver-example hapi-fhir-jpaserver-batch hapi-fhir-jpaserver-base - hapi-fhir-jpaserver-mdm + hapi-fhir-jpaserver-empi hapi-fhir-jpaserver-migrate restful-server-example hapi-fhir-testpage-overlay