Empi 28 matchers (#1918)
* adding matchers * reorganize resource matching api * added precision sensitive date matcher * stricter rules validation * validate thresholds * validate paths. with FIXMES * validate searchparams * fix merge compile error * add soundex, validate no duplicate names * add normalize substring * add exact field to matcher * EXACT -> STRING, exact=true * cleanup test method * match test passes with fixmes * fixed vector matching * fixed vector matching * updating documentation and fixing tests * updated rules documentation with latest matchers * updated rules documentation with latest matchers * created eid page * eid documentation * pre-review cleanup * clean up beans * disentangling beans * checkstyle * noop to trigger CI * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi.md Co-authored-by: Tadgh <tadgh@cs.toronto.edu> * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi.md Co-authored-by: Tadgh <tadgh@cs.toronto.edu> * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi.md Co-authored-by: Tadgh <tadgh@cs.toronto.edu> * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md Co-authored-by: Tadgh <tadgh@cs.toronto.edu> * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md Co-authored-by: Tadgh <tadgh@cs.toronto.edu> * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md Co-authored-by: Tadgh <tadgh@cs.toronto.edu> * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md Co-authored-by: Tadgh <tadgh@cs.toronto.edu> * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_rules.md Co-authored-by: Tadgh <tadgh@cs.toronto.edu> * review feedback * review feedback * review feedback * review feedback * review feedback Co-authored-by: Tadgh <tadgh@cs.toronto.edu>
|
@ -50,9 +50,11 @@ page.server_jpa.diff=Diff Operation
|
|||
page.server_jpa.lastn=LastN Operation
|
||||
|
||||
section.server_jpa_empi.title=JPA Server: EMPI
|
||||
page.server_jpa_empi.empi=Enterprise Master Patient Index
|
||||
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_settings=Enabling EMPI in HAPI FHIR
|
||||
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
|
||||
|
|
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 43 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 56 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 76 KiB |
|
@ -1,158 +1,37 @@
|
|||
# Enterprise Master Person Index (EMPI)
|
||||
# EMPI Getting Started
|
||||
|
||||
HAPI FHIR 5.0.0 introduced preliminary support for **EMPI**.
|
||||
## Introduction
|
||||
|
||||
HAPI FHIR 5.1.0 introduces preliminary support for **EMPI**.
|
||||
|
||||
An 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 may be created and updated using different combinations of automatic linking as well as manual linking.
|
||||
These links may be created and updated using different combinations of automatic linking and manual linking.
|
||||
|
||||
Note: The following sections describe linking between Patient and Person resources. The same information applies for linking between Practitioner and Person, but for readability it is not repeated.
|
||||
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
|
||||
|
||||
The [JPA Server Starter](/hapi-fhir/docs/server_jpa/get_started.html) project contains a complete working example of the HAPI EMPI feature and documentation about how to enable and configure it. You may wish to browse its source to see how this works.
|
||||
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.
|
||||
|
||||
## Person linking in FHIR
|
||||
## Overview
|
||||
|
||||
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.
|
||||
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].
|
||||
|
||||
<a href="/hapi-fhir/docs/images/empi-links.svg"><img src="/hapi-fhir/docs/images/empi-links.svg" alt="EMPI links" style="margin-left: 15px; margin-bottom: 15px; width: 500px;" /></a>
|
||||
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)
|
||||
|
||||
There are several resources that are used:
|
||||
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.
|
||||
|
||||
* 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
|
||||
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.
|
||||
|
||||
# Automatic Linking
|
||||
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.
|
||||
|
||||
With EMPI enabled, the basic default behavior of the EMPI is simply 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 (i.e. via the forthcoming empi operations).
|
||||
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.
|
||||
|
||||
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.
|
||||
## EMPI Settings
|
||||
|
||||
This automatic linking is done via configurable matching rules that create a 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 MATCHED.
|
||||
Follow these steps to enable EMPI on the server:
|
||||
|
||||
## Design Principles
|
||||
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.
|
||||
|
||||
Below are some simplifying principles HAPI EMPI enforces 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 via the FHIR endpoint. These Person resources are managed exclusively by HAPI EMPI. Users can only directly change them via special empi operations. 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. Every Patient resource in the system has a MATCH link to a Person resource unless that Patient has the "no-empi" tag or it has 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, the person created from this patient is given an internal 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 MATCHED, the system will not change it.
|
||||
|
||||
1. When a new Patient resource is created/updated then it is compared to all other Patient resources in the repository. The outcome of each of these comparisons is either NO_MATCH, POSSIBLE_MATCH or MATCHED.
|
||||
|
||||
1. Whenever a MATCHED link is established between a Patient resource and a Person resource, that Patient is always added to that Person resource links. All MATCHED links have corresponding Person resource links and all Person resource links have corresponding MATCHED empi-link records. You can think of the fields of the empi-link records as extra meta-data associated with each Person.link.target.
|
||||
|
||||
### 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 cases:
|
||||
|
||||
* CASE 1: No MATCHED and no POSSIBLE_MATCHED outcomes -> a new Person resource is created and linked to that Patient as MATCHED. 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 created and used as the internal EID.
|
||||
|
||||
* CASE 2: All of the MATCHED 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 MATCHED.
|
||||
|
||||
* CASE 3: The MATCHED Patient resources link to more than one Person -> Mark all links as POSSIBLE_MATCHED. 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, empi-link records are created with POSSIBLE_MATCH outcome and await manual assignment to either NO_MATCH or MATCHED. Person resources are not changed.
|
||||
|
||||
# Rules
|
||||
|
||||
HAPI EMPI rules are managed via a single json document. This document contains a version. empi-links derived from these rules are marked with this version. The following configuration is stored in the rules:
|
||||
|
||||
* **resourceSearchParams**: 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.
|
||||
```json
|
||||
[ {
|
||||
"resourceType" : "Patient",
|
||||
"searchParam" : "birthdate"
|
||||
}, {
|
||||
"resourceType" : "Patient",
|
||||
"searchParam" : "identifier"
|
||||
} ]
|
||||
```
|
||||
|
||||
* **filterSearchParams** 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",
|
||||
"searchParam" : "active",
|
||||
"fixedValue" : "true"
|
||||
} ]
|
||||
```
|
||||
|
||||
* **matchFields** Once the match candidates have been found, they are then each assigned a match vector that marks which fields match. The match vector is determined by a list of matchFields. Each matchField defines a name, distance metric, a success threshold, a resource type, and resource path to check. For example:
|
||||
```json
|
||||
{
|
||||
"name" : "given-name-cosine",
|
||||
"resourceType" : "Patient",
|
||||
"resourcePath" : "name.given",
|
||||
"metric" : "COSINE",
|
||||
"matchThreshold" : 0.8
|
||||
}
|
||||
```
|
||||
|
||||
Note that in all the above json, valid options for `resourceType` are `Patient`, `Practitioner`, and `All`. Use `All` if the criteria is identical across both resource types, and you would like to apply the pre-search to both practitioners and patients.
|
||||
|
||||
The following metrics are currently supported:
|
||||
* JARO_WINKLER
|
||||
* COSINE
|
||||
* JACCARD
|
||||
* NORMALIZED_LEVENSCHTEIN
|
||||
* SORENSEN_DICE
|
||||
* STANDARD_NAME_ANY_ORDER
|
||||
* EXACT_NAME_ANY_ORDER
|
||||
* STANDARD_NAME_FIRST_AND_LAST
|
||||
* EXACT_NAME_FIRST_AND_LAST
|
||||
|
||||
See [java-string-similarity](https://github.com/tdebatty/java-string-similarity) for a description of the first five metrics. For the last four, STANDARd means ignore case and accents whereas EXACT must match casing and accents exactly. Name any order matches first and last names irrespective of order, whereas FIRST_AND_LAST metrics require the name match to be in order.
|
||||
|
||||
* **matchResultMap** A map which converts combinations of successful matchFields into an EMPI Match Result score for overall matching of a given pair of resources.
|
||||
|
||||
```json
|
||||
"matchResultMap" : {
|
||||
"given-name-cosine" : "POSSIBLE_MATCH",
|
||||
"given-name-jaro, last-name-jaro" : "MATCH"
|
||||
}
|
||||
```
|
||||
|
||||
* **eidSystem**: The external EID system that the HAPI EMPI system should expect to see on incoming Patient resources. Must be a valid URI.
|
||||
|
||||
# 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 mentioned `empi-rules.json` file. If a Patient or Practitioner with a valid EID is added to the system, 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 install.
|
||||
|
||||
There are many edge cases for determining what will happen in merge and update scenarios, which will be provided in future documentation.
|
||||
|
||||
|
||||
# HAPI EMPI Technical Details
|
||||
|
||||
When EMPI is enabled, the HAPI FHIR JPA Server does the following things on startup:
|
||||
|
||||
1. HAPI EMPI stores the extra link details in a table called `MPI_LINK`.
|
||||
1. Each record in an `MPI_LINK` table corresponds to a `link.target` entry on a Person resource. HAPI EMPI uses the following convention for the Person.link.assurance level:
|
||||
1. Level 1: not used
|
||||
1. Level 2: POSSIBLE_MATCH
|
||||
1. Level 3: AUTO MATCHED
|
||||
1. Level 4: MANUAL MATCHED
|
||||
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. It registers the `Patient/$match` operation. See [$match](https://www.hl7.org/fhir/operation-patient-match.html) for a description of this operation.
|
||||
1. It registers a new dao interceptor that restricts access to EMPI managed Person records.
|
||||
See [EMPI EID Settings](/hapi-fhir/docs/server_jpa_empi/empi_eid.html#empi-eid-settings) for a description of the EID-related settings.
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
# EMPI Implementation Details
|
||||
|
||||
This section describes details of how EMPI functionality is implemented in HAPI FHIR.
|
||||
|
||||
## Person linking in FHIR
|
||||
|
||||
Because HAPI EMPI is implemented on the HAPI JPA Server, it uses the FHIR model to represent roles and links. The following illustration shows an example of how these links work.
|
||||
|
||||
<a href="/hapi-fhir/docs/images/empi-links.svg"><img src="/hapi-fhir/docs/images/empi-links.svg" alt="EMPI links" style="margin-left: 15px; margin-bottom: 15px; width: 500px;" /></a>
|
||||
|
||||
There are several resources that are used:
|
||||
|
||||
* Patient - Represents the record of a person who receives healthcare services
|
||||
* Person - Represents a master record with links to one or more Patient and/or Practitioner resources that belong to the same person
|
||||
|
||||
# Automatic Linking
|
||||
|
||||
With EMPI enabled, the basic default behavior of the EMPI is simply 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 (i.e. via the forthcoming empi operations).
|
||||
|
||||
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.
|
||||
|
||||
## Design
|
||||
|
||||
Below are some simplifying principles HAPI EMPI enforces 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 via the FHIR endpoint. These Person resources are managed exclusively by HAPI EMPI. Users can only directly change them via special empi operations. 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. Every Patient resource in the system has a MATCH link to a Person resource unless that Patient has the "no-empi" tag or it has 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, the person created from this patient is given an internal 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: not used
|
||||
1. Level 2: POSSIBLE_MATCH
|
||||
1. Level 3: AUTO MATCH
|
||||
1. Level 4: MANUAL MATCH
|
||||
|
||||
### 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 cases:
|
||||
|
||||
* 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 created 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, empi-link records are created with POSSIBLE_MATCH outcome and await manual assignment to either NO_MATCH or MATCH. Person resources are not changed.
|
||||
|
||||
# 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. It registers the `Patient/$match` operation. See [$match](https://www.hl7.org/fhir/operation-patient-match.html) for a description of this operation.
|
||||
1. It registers a new dao interceptor that restricts access to EMPI managed Person records.
|
|
@ -0,0 +1,42 @@
|
|||
# EMPI Enterprise Identifiers
|
||||
|
||||
An Enterprise Identifier(EID) is a unique identifier that can be attached to Patients or Practitioners. Each implementation is expected to use exactly one EID system for incoming resources, defined in the EMPI Rules file. If a Patient or Practitioner with a valid EID is submitted, that EID will be copied over to the Person that was matched. In the case that the incoming Patient or Practitioner had no EID assigned, an internal EID will be created for it. There are thus two classes of EID. Internal EIDs, created by HAPI-EMPI, and External EIDs, provided by the submitted resources.
|
||||
|
||||
## EMPI EID Settings
|
||||
|
||||
The [EmpiSettings](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html) bean contains two EID related settings. Both are enabled by default.
|
||||
|
||||
* **Prevent EID Updates** ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html#setPreventEidUpdates(boolean))): If this is enabled, then once an EID is set on a resource, it cannot be changed. If disabled, patients may have their EID updated.
|
||||
|
||||
* **Prevent multiple EIDs**: ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html#setPreventMultipleEids(boolean))): If this is enabled, then a resource cannot have more than one EID, and incoming resources that break this rule will be rejected.
|
||||
|
||||
## EMPI EID Scenarios
|
||||
|
||||
EMPI EID management follows a complex set of rules to link related Patient records via their Enterprise Id. The following diagrams outline how EIDs are replicated from Patient resources to their linked Person resources under various scenarios according to the values of the EID Settings.
|
||||
|
||||
## EMPI EID Create Scenarios
|
||||
|
||||
<a href="/hapi-fhir/docs/images/empi-create-1.svg"><img src="/hapi-fhir/docs/images/empi-create-1.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
|
||||
|
||||
<a href="/hapi-fhir/docs/images/empi-create-2.svg"><img src="/hapi-fhir/docs/images/empi-create-2.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
|
||||
|
||||
<a href="/hapi-fhir/docs/images/empi-create-3.svg"><img src="/hapi-fhir/docs/images/empi-create-3.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
|
||||
|
||||
<a href="/hapi-fhir/docs/images/empi-create-4.svg"><img src="/hapi-fhir/docs/images/empi-create-4.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
|
||||
|
||||
<a href="/hapi-fhir/docs/images/empi-create-5.svg"><img src="/hapi-fhir/docs/images/empi-create-5.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
|
||||
|
||||
## EMPI EID Update Scenarios
|
||||
|
||||
<a href="/hapi-fhir/docs/images/empi-update-1.svg"><img src="/hapi-fhir/docs/images/empi-update-1.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
|
||||
|
||||
<a href="/hapi-fhir/docs/images/empi-update-2.svg"><img src="/hapi-fhir/docs/images/empi-update-2.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
|
||||
|
||||
<a href="/hapi-fhir/docs/images/empi-update-3.svg"><img src="/hapi-fhir/docs/images/empi-update-3.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
|
||||
|
||||
<a href="/hapi-fhir/docs/images/empi-update-4.svg"><img src="/hapi-fhir/docs/images/empi-update-4.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
|
||||
|
||||
<a href="/hapi-fhir/docs/images/empi-update-5.svg"><img src="/hapi-fhir/docs/images/empi-update-5.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
|
||||
|
||||
<a href="/hapi-fhir/docs/images/empi-update-6.svg"><img src="/hapi-fhir/docs/images/empi-update-6.svg" alt="EMPI Create 1" style="margin-left: 15px; margin-bottom: 15px;" /></a>
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
# Rules
|
||||
|
||||
HAPI EMPI rules are managed via a single json document.
|
||||
|
||||
Note that in all the following configuration, valid options for `resourceType` are `Patient`, `Practitioner`, and `*`. Use `*` if the criteria is identical across both resource types, and you would like to apply it to both practitioners and patients.
|
||||
|
||||
Here is an example of a full HAPI EMPI rules json document:
|
||||
|
||||
```json
|
||||
{
|
||||
"candidateSearchParams": [
|
||||
{
|
||||
"resourceType": "Patient",
|
||||
"searchParam": "birthdate"
|
||||
},
|
||||
{
|
||||
"resourceType": "*",
|
||||
"searchParam": "identifier"
|
||||
},
|
||||
{
|
||||
"resourceType": "Patient",
|
||||
"searchParam": "general-practitioner"
|
||||
}
|
||||
],
|
||||
"candidateFilterSearchParams": [
|
||||
{
|
||||
"resourceType": "*",
|
||||
"searchParam": "active",
|
||||
"fixedValue": "true"
|
||||
}
|
||||
],
|
||||
"matchFields": [
|
||||
{
|
||||
"name": "cosine-given-name",
|
||||
"resourceType": "*",
|
||||
"resourcePath": "name.given",
|
||||
"metric": "COSINE",
|
||||
"matchThreshold": 0.8,
|
||||
"exact": true
|
||||
},
|
||||
{
|
||||
"name": "jaro-last-name",
|
||||
"resourceType": "*",
|
||||
"resourcePath": "name.family",
|
||||
"metric": "JARO_WINKLER",
|
||||
"matchThreshold": 0.8
|
||||
}
|
||||
],
|
||||
"matchResultMap": {
|
||||
"cosine-given-name" : "POSSIBLE_MATCH",
|
||||
"cosine-given-name,jaro-last-name" : "MATCH"
|
||||
},
|
||||
"eidSystem": "http://company.io/fhir/NamingSystem/custom-eid-system"
|
||||
}
|
||||
```
|
||||
|
||||
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. 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).
|
||||
```json
|
||||
[ {
|
||||
"resourceType" : "Patient",
|
||||
"searchParam" : "birthdate"
|
||||
}, {
|
||||
"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. Another way to think of these filters is all of them are "AND"ed with each candidateSearchParam above.
|
||||
```json
|
||||
[ {
|
||||
"resourceType" : "Patient",
|
||||
"searchParam" : "active",
|
||||
"fixedValue" : "true"
|
||||
} ]
|
||||
```
|
||||
|
||||
* **matchFields** Once the match candidates have been found, they are then each compared to the incoming Patient resource. This comparison is made across a list of `matchField`s. Each matchField returns `true` or `false` indicating whether the candidate and the incoming Patient match on that field. There are two types of metrics: `Matcher` and `Similarity`. Matcher metrics return a `true` or `false` directly, whereas Similarity metrics return a score between 0.0 (no match) and 1.0 (exact match) and this score is translated to a `true/false` via a `matchThreshold`. E.g. if a `JARO_WINKLER` matchField is configured with a `matchThreshold` of 0.8 then that matchField will return `true` if the `JARO_WINKLER` similarity evaluates to a score >= 8.0.
|
||||
|
||||
By default, all matchFields have `exact=false` which means that they will have all diacritical marks removed and converted to upper case before matching. `exact=true` can be added to any matchField to compare the strings as they are originally capitalized and accented.
|
||||
|
||||
Here is a matcher matchField that uses the SOUNDEX matcher to determine whether two family names match.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "family-name-double-metaphone",
|
||||
"resourceType": "*",
|
||||
"resourcePath": "name.family",
|
||||
"metric": "SOUNDEX"
|
||||
}
|
||||
```
|
||||
|
||||
Here is a matcher matchField that only matches when two family names are identical.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "family-name-exact",
|
||||
"resourceType": "*",
|
||||
"resourcePath": "name.family",
|
||||
"metric": "STRING",
|
||||
"exact": true
|
||||
}
|
||||
```
|
||||
|
||||
Here is a similarity matchField that matches when two given names match with a JARO_WINKLER threshold >0 0.8.
|
||||
|
||||
```json
|
||||
{
|
||||
"name" : "given-name-jaro",
|
||||
"resourceType" : "Patient",
|
||||
"resourcePath" : "name.given",
|
||||
"metric" : "JARO_WINKLER",
|
||||
"matchThreshold" : 0.8
|
||||
}
|
||||
```
|
||||
|
||||
The following metrics are currently supported:
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Example</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>STRING</td>
|
||||
<td>matcher</td>
|
||||
<td>
|
||||
Match the values as strings. This matcher should be used with tokens (e.g. gender).
|
||||
</td>
|
||||
<td>MCTAVISH = McTavish when exact = false, MCTAVISH != McTavish when exact = true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SUBSTRING</td>
|
||||
<td>matcher</td>
|
||||
<td>
|
||||
True if one string starts with the other.
|
||||
</td>
|
||||
<td>Bill = Billy, Egbert = Bert</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>METAPHONE</td>
|
||||
<td>matcher</td>
|
||||
<td>
|
||||
<a href="https://commons.apache.org/proper/commons-codec/apidocs/org/apache/commons/codec/language/Metaphone.html">Apache Metaphone</a>
|
||||
</td>
|
||||
<td>Dury = Durie, Allsop != Allsob, Smith != Schmidt</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DOUBLE_METAPHONE</td>
|
||||
<td>matcher</td>
|
||||
<td>
|
||||
<a href="https://commons.apache.org/proper/commons-codec/apidocs/org/apache/commons/codec/language/DoubleMetaphone.html">Apache Double Metaphone</a>
|
||||
</td>
|
||||
<td>Dury = Durie, Allsop = Allsob, Smith != Schmidt</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SOUNDEX</td>
|
||||
<td>matcher</td>
|
||||
<td>
|
||||
<a href="https://commons.apache.org/proper/commons-codec/apidocs/org/apache/commons/codec/language/Soundex.html">Apache Soundex</a>
|
||||
</td>
|
||||
<td>Jon = John, Thomas != Tom</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CAVERPHONE1</td>
|
||||
<td>matcher</td>
|
||||
<td>
|
||||
<a href="https://commons.apache.org/proper/commons-codec/apidocs/org/apache/commons/codec/language/Caverphone1.html">Apache Caverphone1</a>
|
||||
</td>
|
||||
<td>Gail = Gael, Gail != Gale, Thomas != Tom</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CAVERPHONE1</td>
|
||||
<td>matcher</td>
|
||||
<td>
|
||||
<a href="https://commons.apache.org/proper/commons-codec/apidocs/org/apache/commons/codec/language/Caverphone1.html">Apache Caverphone1</a>
|
||||
</td>
|
||||
<td>Gail = Gael, Gail = Gale, Thomas != Tom</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DATE</td>
|
||||
<td>matcher</td>
|
||||
<td>
|
||||
Reduce the precision of the dates to the lowest precision of the two, then compare them as strings.
|
||||
</td>
|
||||
<td>2019-12,Month = 2019-12-19,Day</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NAME_ANY_ORDER</td>
|
||||
<td>matcher</td>
|
||||
<td>
|
||||
Match names as strings in any order
|
||||
</td>
|
||||
<td>John Henry = Henry JOHN when exact = false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NAME_FIRST_AND_LAST</td>
|
||||
<td>matcher</td>
|
||||
<td>
|
||||
Match names as strings in any order
|
||||
</td>
|
||||
<td>John Henry = John HENRY when exact=false, John Henry != Henry John</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>JARO_WINKLER</td>
|
||||
<td>similarity</td>
|
||||
<td>
|
||||
<a href="https://github.com/tdebatty/java-string-similarity#jaro-winkler">tdebatty Jaro Winkler</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>COSINE</td>
|
||||
<td>similarity</td>
|
||||
<td>
|
||||
<a href="https://github.com/tdebatty/java-string-similarity#cosine-similarity">tdebatty Cosine Similarity</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>JACCARD</td>
|
||||
<td>similarity</td>
|
||||
<td>
|
||||
<a href="https://github.com/tdebatty/java-string-similarity#jaccard-index">tdebatty Jaccard Index</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LEVENSCHTEIN</td>
|
||||
<td>similarity</td>
|
||||
<td>
|
||||
<a href="https://github.com/tdebatty/java-string-similarity#normalized-levenshtein">tdebatty Normalized Levenshtein</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SORENSEN_DICE</td>
|
||||
<td>similarity</td>
|
||||
<td>
|
||||
<a href="https://github.com/tdebatty/java-string-similarity#sorensen-dice-coefficient">tdebatty Sorensen-Dice coefficient</a>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
* **matchResultMap** converts 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.
|
||||
|
||||
```json
|
||||
{
|
||||
"matchResultMap": {
|
||||
"cosine-given-name" : "POSSIBLE_MATCH",
|
||||
"cosine-given-name,jaro-last-name" : "MATCH"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* **eidSystem**: The external EID system that the HAPI EMPI system should expect to see on incoming Patient resources. Must be a valid URI. See [EMPI EID](/hapi-fhir/docs/server_jpa_empi/empi_eid.html) for details on how EIDs are managed by HAPI EMPI.
|
|
@ -1,11 +0,0 @@
|
|||
# Enabling EMPI in HAPI FHIR
|
||||
|
||||
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.
|
||||
|
||||
The following settings 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.
|
|
@ -30,7 +30,7 @@ import ca.uhn.fhir.empi.api.IEmpiSettings;
|
|||
import ca.uhn.fhir.empi.log.Logs;
|
||||
import ca.uhn.fhir.empi.provider.EmpiProviderLoader;
|
||||
import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator;
|
||||
import ca.uhn.fhir.empi.rules.svc.EmpiResourceComparatorSvc;
|
||||
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.empi.broker.EmpiMessageHandler;
|
||||
|
@ -47,6 +47,7 @@ import ca.uhn.fhir.jpa.empi.svc.EmpiMatchLinkSvc;
|
|||
import ca.uhn.fhir.jpa.empi.svc.EmpiPersonFindingSvc;
|
||||
import ca.uhn.fhir.jpa.empi.svc.EmpiPersonMergerSvcImpl;
|
||||
import ca.uhn.fhir.jpa.empi.svc.EmpiResourceDaoSvc;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRetriever;
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
@ -64,8 +65,6 @@ public class EmpiConsumerConfig {
|
|||
@Autowired
|
||||
IEmpiSettings myEmpiProperties;
|
||||
@Autowired
|
||||
EmpiRuleValidator myEmpiRuleValidator;
|
||||
@Autowired
|
||||
EmpiProviderLoader myEmpiProviderLoader;
|
||||
@Autowired
|
||||
EmpiSubscriptionLoader myEmpiSubscriptionLoader;
|
||||
|
@ -133,8 +132,8 @@ public class EmpiConsumerConfig {
|
|||
}
|
||||
|
||||
@Bean
|
||||
EmpiRuleValidator empiRuleValidator() {
|
||||
return new EmpiRuleValidator();
|
||||
EmpiRuleValidator empiRuleValidator(FhirContext theFhirContext, ISearchParamRetriever theSearchParamRetriever) {
|
||||
return new EmpiRuleValidator(theFhirContext, theSearchParamRetriever);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
@ -159,8 +158,8 @@ public class EmpiConsumerConfig {
|
|||
}
|
||||
|
||||
@Bean
|
||||
EmpiResourceComparatorSvc empiResourceComparatorSvc(FhirContext theFhirContext, IEmpiSettings theEmpiConfig) {
|
||||
return new EmpiResourceComparatorSvc(theFhirContext, theEmpiConfig);
|
||||
EmpiResourceMatcherSvc empiResourceComparatorSvc(FhirContext theFhirContext, IEmpiSettings theEmpiConfig) {
|
||||
return new EmpiResourceMatcherSvc(theFhirContext, theEmpiConfig);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
@ -178,8 +177,6 @@ public class EmpiConsumerConfig {
|
|||
if (!myEmpiProperties.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
myEmpiRuleValidator.validate(myEmpiProperties.getEmpiRules());
|
||||
}
|
||||
|
||||
@EventListener(classes = {ContextRefreshedEvent.class})
|
||||
|
|
|
@ -20,6 +20,8 @@ package ca.uhn.fhir.jpa.empi.config;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator;
|
||||
import ca.uhn.fhir.jpa.empi.interceptor.EmpiSubmitterInterceptorLoader;
|
||||
import ca.uhn.fhir.jpa.empi.svc.EmpiSearchParamSvc;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
@ -38,4 +40,8 @@ public class EmpiSubmitterConfig {
|
|||
return new EmpiSearchParamSvc();
|
||||
}
|
||||
|
||||
@Bean
|
||||
EmpiRuleValidator empiRuleValidator(FhirContext theFhirContext) {
|
||||
return new EmpiRuleValidator(theFhirContext, empiSearchParamSvc());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,11 +71,11 @@ public class EmpiCandidateSearchSvc {
|
|||
public Collection<IAnyResource> findCandidates(String theResourceType, IAnyResource theResource) {
|
||||
Map<Long, IAnyResource> matchedPidsToResources = new HashMap<>();
|
||||
|
||||
List<EmpiFilterSearchParamJson> filterSearchParams = myEmpiConfig.getEmpiRules().getFilterSearchParams();
|
||||
List<EmpiFilterSearchParamJson> filterSearchParams = myEmpiConfig.getEmpiRules().getCandidateFilterSearchParams();
|
||||
|
||||
List<String> filterCriteria = buildFilterQuery(filterSearchParams, theResourceType);
|
||||
|
||||
for (EmpiResourceSearchParamJson resourceSearchParam : myEmpiConfig.getEmpiRules().getResourceSearchParams()) {
|
||||
for (EmpiResourceSearchParamJson resourceSearchParam : myEmpiConfig.getEmpiRules().getCandidateSearchParams()) {
|
||||
|
||||
if (!isSearchParamForResource(theResourceType, resourceSearchParam)) {
|
||||
continue;
|
||||
|
|
|
@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.empi.svc;
|
|||
|
||||
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
|
||||
import ca.uhn.fhir.empi.api.MatchedTarget;
|
||||
import ca.uhn.fhir.empi.rules.svc.EmpiResourceComparatorSvc;
|
||||
import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc;
|
||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
@ -37,7 +37,7 @@ public class EmpiMatchFinderSvcImpl implements IEmpiMatchFinderSvc {
|
|||
@Autowired
|
||||
private EmpiCandidateSearchSvc myEmpiCandidateSearchSvc;
|
||||
@Autowired
|
||||
private EmpiResourceComparatorSvc myEmpiResourceComparatorSvc;
|
||||
private EmpiResourceMatcherSvc myEmpiResourceMatcherSvc;
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
|
@ -45,7 +45,7 @@ public class EmpiMatchFinderSvcImpl implements IEmpiMatchFinderSvc {
|
|||
Collection<IAnyResource> targetCandidates = myEmpiCandidateSearchSvc.findCandidates(theResourceType, theResource);
|
||||
|
||||
return targetCandidates.stream()
|
||||
.map(candidate -> new MatchedTarget(candidate, myEmpiResourceComparatorSvc.getMatchResult(theResource, candidate)))
|
||||
.map(candidate -> new MatchedTarget(candidate, myEmpiResourceMatcherSvc.getMatchResult(theResource, candidate)))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
|||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService;
|
||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRetriever;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
@ -35,7 +36,7 @@ import org.springframework.stereotype.Service;
|
|||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class EmpiSearchParamSvc {
|
||||
public class EmpiSearchParamSvc implements ISearchParamRetriever {
|
||||
@Autowired
|
||||
FhirContext myFhirContext;
|
||||
@Autowired
|
||||
|
@ -55,4 +56,9 @@ public class EmpiSearchParamSvc {
|
|||
RuntimeSearchParam activeSearchParam = mySearchParamRegistry.getActiveSearchParam(resourceType, theFilterSearchParam.getSearchParam());
|
||||
return mySearchParamExtractorService.extractParamValuesAsStrings(activeSearchParam, theResource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) {
|
||||
return mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ 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.EmpiResourceComparatorSvc;
|
||||
import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc;
|
||||
import ca.uhn.fhir.empi.util.EIDHelper;
|
||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
|
||||
|
@ -82,7 +82,7 @@ abstract public class BaseEmpiR4Test extends BaseJpaR4Test {
|
|||
@Autowired
|
||||
protected IFhirResourceDao<Practitioner> myPractitionerDao;
|
||||
@Autowired
|
||||
protected EmpiResourceComparatorSvc myEmpiResourceComparatorSvc;
|
||||
protected EmpiResourceMatcherSvc myEmpiResourceMatcherSvc;
|
||||
@Autowired
|
||||
protected IEmpiLinkDao myEmpiLinkDao;
|
||||
@Autowired
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package ca.uhn.fhir.jpa.empi.config;
|
||||
|
||||
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;
|
||||
|
@ -23,11 +24,11 @@ public abstract class BaseTestEmpiConfig {
|
|||
boolean myPreventMultipleEids;
|
||||
|
||||
@Bean
|
||||
IEmpiSettings empiProperties() throws IOException {
|
||||
IEmpiSettings empiSettings(EmpiRuleValidator theEmpiRuleValidator) throws IOException {
|
||||
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
Resource resource = resourceLoader.getResource("empi/empi-rules.json");
|
||||
String json = IOUtils.toString(resource.getInputStream(), Charsets.UTF_8);
|
||||
return new EmpiSettings()
|
||||
return new EmpiSettings(theEmpiRuleValidator)
|
||||
.setEnabled(false)
|
||||
.setScriptText(json)
|
||||
.setPreventEidUpdates(myPreventEidUpdates)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package ca.uhn.fhir.jpa.empi.helper;
|
||||
|
||||
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;
|
||||
|
@ -26,13 +27,13 @@ public class EmpiHelperConfig {
|
|||
|
||||
@Primary
|
||||
@Bean
|
||||
IEmpiSettings empiProperties() throws IOException {
|
||||
IEmpiSettings empiSettings(EmpiRuleValidator theEmpiRuleValidator) throws IOException {
|
||||
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
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 EmpiSettings()
|
||||
return new EmpiSettings(theEmpiRuleValidator)
|
||||
.setEnabled(true)
|
||||
.setScriptText(json)
|
||||
.setPreventEidUpdates(myPreventEidUpdates)
|
||||
|
|
|
@ -36,7 +36,7 @@ public class EmpiLinkSvcTest extends BaseEmpiR4Test {
|
|||
public void compareEmptyPatients() {
|
||||
Patient patient = new Patient();
|
||||
patient.setId("Patient/1");
|
||||
EmpiMatchResultEnum result = myEmpiResourceComparatorSvc.getMatchResult(patient, patient);
|
||||
EmpiMatchResultEnum result = myEmpiResourceMatcherSvc.getMatchResult(patient, patient);
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, result);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,35 +1,46 @@
|
|||
{
|
||||
"candidateSearchParams" : [ {
|
||||
"resourceType" : "Patient",
|
||||
"searchParam" : "birthdate"
|
||||
}, {
|
||||
"resourceType" : "*",
|
||||
"searchParam" : "identifier"
|
||||
},{
|
||||
"resourceType" : "Patient",
|
||||
"searchParam" : "general-practitioner"
|
||||
} ],
|
||||
"candidateFilterSearchParams" : [ {
|
||||
"resourceType" : "*",
|
||||
"searchParam" : "active",
|
||||
"fixedValue" : "true"
|
||||
} ],
|
||||
"matchFields" : [ {
|
||||
"name" : "given-name",
|
||||
"resourceType" : "*",
|
||||
"resourcePath" : "name.given",
|
||||
"metric" : "COSINE",
|
||||
"matchThreshold" : 0.8
|
||||
}, {
|
||||
"name" : "last-name",
|
||||
"resourceType" : "*",
|
||||
"resourcePath" : "name.family",
|
||||
"metric" : "JARO_WINKLER",
|
||||
"matchThreshold" : 0.8
|
||||
}],
|
||||
"matchResultMap" : {
|
||||
"given-name" : "POSSIBLE_MATCH",
|
||||
"given-name,last-name" : "MATCH"
|
||||
"candidateSearchParams": [
|
||||
{
|
||||
"resourceType": "Patient",
|
||||
"searchParam": "birthdate"
|
||||
},
|
||||
{
|
||||
"resourceType": "*",
|
||||
"searchParam": "identifier"
|
||||
},
|
||||
{
|
||||
"resourceType": "Patient",
|
||||
"searchParam": "general-practitioner"
|
||||
}
|
||||
],
|
||||
"candidateFilterSearchParams": [
|
||||
{
|
||||
"resourceType": "*",
|
||||
"searchParam": "active",
|
||||
"fixedValue": "true"
|
||||
}
|
||||
],
|
||||
"matchFields": [
|
||||
{
|
||||
"name": "cosine-given-name",
|
||||
"resourceType": "*",
|
||||
"resourcePath": "name.given",
|
||||
"metric": "COSINE",
|
||||
"matchThreshold": 0.8,
|
||||
"exact": true
|
||||
},
|
||||
{
|
||||
"name": "jaro-last-name",
|
||||
"resourceType": "*",
|
||||
"resourcePath": "name.family",
|
||||
"metric": "JARO_WINKLER",
|
||||
"matchThreshold": 0.8,
|
||||
"exact": true
|
||||
}
|
||||
],
|
||||
"matchResultMap": {
|
||||
"cosine-given-name" : "POSSIBLE_MATCH",
|
||||
"cosine-given-name,jaro-last-name" : "MATCH"
|
||||
},
|
||||
"eidSystem": "http://company.io/fhir/NamingSystem/custom-eid-system"
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
<maxFileSize>5MB</maxFileSize>
|
||||
</triggeringPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n${log.stackfilter.pattern}</pattern>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<logger name="ca.uhn.fhir.log.empi_troubleshooting" level="TRACE">
|
||||
|
|
|
@ -69,6 +69,10 @@
|
|||
<groupId>javax.annotation</groupId>
|
||||
<artifactId>javax.annotation-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
</dependency>
|
||||
<!-- test -->
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
|
|
|
@ -21,20 +21,142 @@ package ca.uhn.fhir.empi.rules.config;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.empi.api.EmpiConstants;
|
||||
import ca.uhn.fhir.empi.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.parser.DataFormatException;
|
||||
import ca.uhn.fhir.rest.server.util.ISearchParamRetriever;
|
||||
import ca.uhn.fhir.util.FhirTerser;
|
||||
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.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class EmpiRuleValidator {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(EmpiRuleValidator.class);
|
||||
|
||||
private final FhirContext myFhirContext;
|
||||
private final ISearchParamRetriever mySearchParamRetriever;
|
||||
private final Class<? extends IBaseResource> myPatientClass;
|
||||
private final Class<? extends IBaseResource> myPractitionerClass;
|
||||
private final FhirTerser myTerser;
|
||||
|
||||
@Autowired
|
||||
public EmpiRuleValidator(FhirContext theFhirContext, ISearchParamRetriever theSearchParamRetriever) {
|
||||
myFhirContext = theFhirContext;
|
||||
myPatientClass = theFhirContext.getResourceDefinition("Patient").getImplementingClass();
|
||||
myPractitionerClass = theFhirContext.getResourceDefinition("Practitioner").getImplementingClass();
|
||||
myTerser = myFhirContext.newTerser();
|
||||
mySearchParamRetriever = theSearchParamRetriever;
|
||||
}
|
||||
|
||||
public void validate(EmpiRulesJson theEmpiRulesJson) {
|
||||
validateSearchParams(theEmpiRulesJson);
|
||||
validateMatchFields(theEmpiRulesJson);
|
||||
validateSystemIsUri(theEmpiRulesJson);
|
||||
}
|
||||
|
||||
private void validateSearchParams(EmpiRulesJson theEmpiRulesJson) {
|
||||
for (EmpiResourceSearchParamJson searchParam : theEmpiRulesJson.getCandidateSearchParams()) {
|
||||
validateSearchParam("candidateSearchParams", searchParam.getResourceType(), searchParam.getSearchParam());
|
||||
}
|
||||
for (EmpiFilterSearchParamJson filter : theEmpiRulesJson.getCandidateFilterSearchParams()) {
|
||||
validateSearchParam("candidateFilterSearchParams", filter.getResourceType(), filter.getSearchParam());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSearchParam(String theFieldName, String theTheResourceType, String theTheSearchParam) {
|
||||
if (EmpiConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE.equals(theTheResourceType)) {
|
||||
validateResourceSearchParam(theFieldName, "Patient", theTheSearchParam);
|
||||
validateResourceSearchParam(theFieldName, "Practitioner", theTheSearchParam);
|
||||
} else {
|
||||
validateResourceSearchParam(theFieldName, theTheResourceType, theTheSearchParam);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateResourceSearchParam(String theFieldName, String theResourceType, String theSearchParam) {
|
||||
if (mySearchParamRetriever.getActiveSearchParam(theResourceType, theSearchParam) == null) {
|
||||
throw new ConfigurationException("Error in " + theFieldName + ": " + theResourceType + " does not have a search parameter called '" + theSearchParam + "'");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateMatchFields(EmpiRulesJson theEmpiRulesJson) {
|
||||
Set<String> names = new HashSet<>();
|
||||
for (EmpiFieldMatchJson fieldMatch : theEmpiRulesJson.getMatchFields()) {
|
||||
if (names.contains(fieldMatch.getName())) {
|
||||
throw new ConfigurationException("Two MatchFields have the same name '" + fieldMatch.getName() + "'");
|
||||
}
|
||||
names.add(fieldMatch.getName());
|
||||
validateThreshold(fieldMatch);
|
||||
validatePath(fieldMatch);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateThreshold(EmpiFieldMatchJson theFieldMatch) {
|
||||
if (theFieldMatch.getMetric().isSimilarity()) {
|
||||
if (theFieldMatch.getMatchThreshold() == null) {
|
||||
throw new ConfigurationException("MatchField " + theFieldMatch.getName() + " metric " + theFieldMatch.getMetric() + " requires a matchThreshold");
|
||||
}
|
||||
} else if (theFieldMatch.getMatchThreshold() != null) {
|
||||
throw new ConfigurationException("MatchField " + theFieldMatch.getName() + " metric " + theFieldMatch.getMetric() + " should not have a matchThreshold");
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePath(EmpiFieldMatchJson theFieldMatch) {
|
||||
String resourceType = theFieldMatch.getResourceType();
|
||||
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 {
|
||||
throw new ConfigurationException("MatchField " + theFieldMatch.getName() + " has unknown resourceType " + resourceType);
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePatientPath(EmpiFieldMatchJson theFieldMatch) {
|
||||
try {
|
||||
myTerser.getDefinition(myPatientClass, "Patient." + theFieldMatch.getResourcePath());
|
||||
} catch (DataFormatException|ConfigurationException e) {
|
||||
throw new ConfigurationException("MatchField " +
|
||||
theFieldMatch.getName() +
|
||||
" resourceType " +
|
||||
theFieldMatch.getResourceType() +
|
||||
" has invalid path '" + theFieldMatch.getResourcePath() + "'. " +
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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(EmpiRulesJson theEmpiRulesJson) {
|
||||
if (theEmpiRulesJson.getEnterpriseEIDSystem() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
new URI(theEmpiRulesJson.getEnterpriseEIDSystem());
|
||||
} catch (URISyntaxException e) {
|
||||
|
|
|
@ -23,10 +23,15 @@ package ca.uhn.fhir.empi.rules.config;
|
|||
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;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class EmpiSettings implements IEmpiSettings {
|
||||
private final EmpiRuleValidator myEmpiRuleValidator;
|
||||
|
||||
private boolean myEnabled;
|
||||
private int myConcurrentConsumers = EMPI_DEFAULT_CONCURRENT_CONSUMERS;
|
||||
private String myScriptText;
|
||||
|
@ -42,6 +47,11 @@ public class EmpiSettings implements IEmpiSettings {
|
|||
*/
|
||||
private boolean myPreventMultipleEids;
|
||||
|
||||
@Autowired
|
||||
public EmpiSettings(EmpiRuleValidator theEmpiRuleValidator) {
|
||||
myEmpiRuleValidator = theEmpiRuleValidator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return myEnabled;
|
||||
|
@ -68,7 +78,7 @@ public class EmpiSettings implements IEmpiSettings {
|
|||
|
||||
public EmpiSettings setScriptText(String theScriptText) throws IOException {
|
||||
myScriptText = theScriptText;
|
||||
myEmpiRules = JsonUtil.deserialize(theScriptText, EmpiRulesJson.class);
|
||||
setEmpiRules(JsonUtil.deserialize(theScriptText, EmpiRulesJson.class));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -88,6 +98,7 @@ public class EmpiSettings implements IEmpiSettings {
|
|||
}
|
||||
|
||||
public EmpiSettings setEmpiRules(EmpiRulesJson theEmpiRules) {
|
||||
myEmpiRuleValidator.validate(theEmpiRules);
|
||||
myEmpiRules = theEmpiRules;
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
package ca.uhn.fhir.empi.rules.json;
|
||||
|
||||
/*-
|
||||
* #%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.rules.similarity.EmpiPersonNameMatchModeEnum;
|
||||
import ca.uhn.fhir.empi.rules.similarity.HapiStringSimilarity;
|
||||
import ca.uhn.fhir.empi.rules.similarity.IEmpiFieldSimilarity;
|
||||
import ca.uhn.fhir.empi.rules.similarity.NameSimilarity;
|
||||
import info.debatty.java.stringsimilarity.Cosine;
|
||||
import info.debatty.java.stringsimilarity.Jaccard;
|
||||
import info.debatty.java.stringsimilarity.JaroWinkler;
|
||||
import info.debatty.java.stringsimilarity.NormalizedLevenshtein;
|
||||
import info.debatty.java.stringsimilarity.SorensenDice;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
|
||||
/**
|
||||
* Enum for holding all the known distance metrics that we support in HAPI for
|
||||
* calculating differences between strings (https://en.wikipedia.org/wiki/String_metric)
|
||||
*/
|
||||
public enum DistanceMetricEnum implements IEmpiFieldSimilarity {
|
||||
JARO_WINKLER("Jaro Winkler", new HapiStringSimilarity(new JaroWinkler())),
|
||||
COSINE("Cosine", new HapiStringSimilarity(new Cosine())),
|
||||
JACCARD("Jaccard", new HapiStringSimilarity(new Jaccard())),
|
||||
NORMALIZED_LEVENSCHTEIN("Normalized Levenschtein", new HapiStringSimilarity(new NormalizedLevenshtein())),
|
||||
SORENSEN_DICE("Sorensen Dice", new HapiStringSimilarity(new SorensenDice())),
|
||||
STANDARD_NAME_ANY_ORDER("Standard name Any Order", new NameSimilarity(EmpiPersonNameMatchModeEnum.STANDARD_ANY_ORDER)),
|
||||
EXACT_NAME_ANY_ORDER("Exact name Any Order", new NameSimilarity(EmpiPersonNameMatchModeEnum.EXACT_ANY_ORDER)),
|
||||
STANDARD_NAME_FIRST_AND_LAST("Standard name First and Last", new NameSimilarity(EmpiPersonNameMatchModeEnum.STANDARD_FIRST_AND_LAST)),
|
||||
EXACT_NAME_FIRST_AND_LAST("Exact name First and Last", new NameSimilarity(EmpiPersonNameMatchModeEnum.EXACT_FIRST_AND_LAST));
|
||||
|
||||
private final String myCode;
|
||||
private final IEmpiFieldSimilarity myEmpiFieldSimilarity;
|
||||
|
||||
DistanceMetricEnum(String theCode, IEmpiFieldSimilarity theEmpiFieldSimilarity) {
|
||||
myCode = theCode;
|
||||
myEmpiFieldSimilarity = theEmpiFieldSimilarity;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return myCode;
|
||||
}
|
||||
|
||||
public IEmpiFieldSimilarity getEmpiFieldSimilarity() {
|
||||
return myEmpiFieldSimilarity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase) {
|
||||
return myEmpiFieldSimilarity.similarity(theFhirContext ,theLeftBase, theRightBase);
|
||||
}
|
||||
|
||||
}
|
|
@ -20,35 +20,42 @@ package ca.uhn.fhir.empi.rules.json;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum;
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Contains all business data for determining if a match exists on a particular field, given:
|
||||
*
|
||||
* 1. A {@link DistanceMetricEnum} which determines the actual similarity values.
|
||||
* 1. A {@link EmpiMetricEnum} 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 EmpiFieldMatchJson implements IModelJson {
|
||||
@JsonProperty("name")
|
||||
@JsonProperty(value = "name", required = true)
|
||||
String myName;
|
||||
@JsonProperty("resourceType")
|
||||
@JsonProperty(value = "resourceType", required = true)
|
||||
String myResourceType;
|
||||
@JsonProperty("resourcePath")
|
||||
@JsonProperty(value = "resourcePath", required = true)
|
||||
String myResourcePath;
|
||||
@JsonProperty("metric")
|
||||
DistanceMetricEnum myMetric;
|
||||
@JsonProperty(value = "metric", required = true)
|
||||
EmpiMetricEnum myMetric;
|
||||
@JsonProperty("matchThreshold")
|
||||
double myMatchThreshold;
|
||||
Double myMatchThreshold;
|
||||
/**
|
||||
* For String value types, should the values be normalized (case, accents) before they are compared
|
||||
*/
|
||||
@JsonProperty(value = "exact")
|
||||
boolean myExact;
|
||||
|
||||
public DistanceMetricEnum getMetric() {
|
||||
public EmpiMetricEnum getMetric() {
|
||||
return myMetric;
|
||||
}
|
||||
|
||||
public EmpiFieldMatchJson setMetric(DistanceMetricEnum theMetric) {
|
||||
public EmpiFieldMatchJson setMetric(EmpiMetricEnum theMetric) {
|
||||
myMetric = theMetric;
|
||||
return this;
|
||||
}
|
||||
|
@ -71,7 +78,8 @@ public class EmpiFieldMatchJson implements IModelJson {
|
|||
return this;
|
||||
}
|
||||
|
||||
public double getMatchThreshold() {
|
||||
@Nullable
|
||||
public Double getMatchThreshold() {
|
||||
return myMatchThreshold;
|
||||
}
|
||||
|
||||
|
@ -88,4 +96,13 @@ public class EmpiFieldMatchJson implements IModelJson {
|
|||
myName = theName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean getExact() {
|
||||
return myExact;
|
||||
}
|
||||
|
||||
public EmpiFieldMatchJson setExact(boolean theExact) {
|
||||
myExact = theExact;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,13 +29,13 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
* candidate searching. e.g. When doing candidate matching, only consider candidates that match all EmpiFilterSearchParams.
|
||||
*/
|
||||
public class EmpiFilterSearchParamJson implements IModelJson {
|
||||
@JsonProperty("resourceType")
|
||||
@JsonProperty(value = "resourceType", required = true)
|
||||
String myResourceType;
|
||||
@JsonProperty("searchParam")
|
||||
@JsonProperty(value = "searchParam", required = true)
|
||||
String mySearchParam;
|
||||
@JsonProperty("qualifier")
|
||||
@JsonProperty(value = "qualifier", required = true)
|
||||
TokenParamModifier myTokenParamModifier;
|
||||
@JsonProperty("fixedValue")
|
||||
@JsonProperty(value = "fixedValue", required = true)
|
||||
String myFixedValue;
|
||||
|
||||
public String getResourceType() {
|
||||
|
|
|
@ -27,9 +27,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
*
|
||||
*/
|
||||
public class EmpiResourceSearchParamJson implements IModelJson {
|
||||
@JsonProperty("resourceType")
|
||||
@JsonProperty(value = "resourceType", required = true)
|
||||
String myResourceType;
|
||||
@JsonProperty("searchParam")
|
||||
@JsonProperty(value = "searchParam", required = true)
|
||||
String mySearchParam;
|
||||
|
||||
public String getResourceType() {
|
||||
|
|
|
@ -35,13 +35,13 @@ import java.util.Map;
|
|||
|
||||
@JsonDeserialize(converter = EmpiRulesJson.EmpiRulesJsonConverter.class)
|
||||
public class EmpiRulesJson implements IModelJson {
|
||||
@JsonProperty("candidateSearchParams")
|
||||
List<EmpiResourceSearchParamJson> myResourceSearchParams = new ArrayList<>();
|
||||
@JsonProperty("candidateFilterSearchParams")
|
||||
List<EmpiFilterSearchParamJson> myFilterSearchParams = new ArrayList<>();
|
||||
@JsonProperty("matchFields")
|
||||
@JsonProperty(value = "candidateSearchParams", required = true)
|
||||
List<EmpiResourceSearchParamJson> myCandidateSearchParams = new ArrayList<>();
|
||||
@JsonProperty(value = "candidateFilterSearchParams", required = true)
|
||||
List<EmpiFilterSearchParamJson> myCandidateFilterSearchParams = new ArrayList<>();
|
||||
@JsonProperty(value = "matchFields", required = true)
|
||||
List<EmpiFieldMatchJson> myMatchFieldJsonList = new ArrayList<>();
|
||||
@JsonProperty("matchResultMap")
|
||||
@JsonProperty(value = "matchResultMap", required = true)
|
||||
Map<String, EmpiMatchResultEnum> myMatchResultMap = new HashMap<>();
|
||||
@JsonProperty(value = "eidSystem")
|
||||
String myEnterpriseEIDSystem;
|
||||
|
@ -53,11 +53,11 @@ public class EmpiRulesJson implements IModelJson {
|
|||
}
|
||||
|
||||
public void addResourceSearchParam(EmpiResourceSearchParamJson theSearchParam) {
|
||||
myResourceSearchParams.add(theSearchParam);
|
||||
myCandidateSearchParams.add(theSearchParam);
|
||||
}
|
||||
|
||||
public void addFilterSearchParam(EmpiFilterSearchParamJson theSearchParam) {
|
||||
myFilterSearchParams.add(theSearchParam);
|
||||
myCandidateFilterSearchParams.add(theSearchParam);
|
||||
}
|
||||
|
||||
int size() {
|
||||
|
@ -73,8 +73,7 @@ public class EmpiRulesJson implements IModelJson {
|
|||
}
|
||||
|
||||
public EmpiMatchResultEnum getMatchResult(Long theMatchVector) {
|
||||
EmpiMatchResultEnum result = myVectorMatchResultMap.get(theMatchVector);
|
||||
return (result == null) ? EmpiMatchResultEnum.NO_MATCH : result;
|
||||
return myVectorMatchResultMap.get(theMatchVector);
|
||||
}
|
||||
|
||||
public void putMatchResult(String theFieldMatchNames, EmpiMatchResultEnum theMatchResult) {
|
||||
|
@ -97,12 +96,12 @@ public class EmpiRulesJson implements IModelJson {
|
|||
return Collections.unmodifiableList(myMatchFieldJsonList);
|
||||
}
|
||||
|
||||
public List<EmpiResourceSearchParamJson> getResourceSearchParams() {
|
||||
return Collections.unmodifiableList(myResourceSearchParams);
|
||||
public List<EmpiResourceSearchParamJson> getCandidateSearchParams() {
|
||||
return Collections.unmodifiableList(myCandidateSearchParams);
|
||||
}
|
||||
|
||||
public List<EmpiFilterSearchParamJson> getFilterSearchParams() {
|
||||
return Collections.unmodifiableList(myFilterSearchParams);
|
||||
public List<EmpiFilterSearchParamJson> getCandidateFilterSearchParams() {
|
||||
return Collections.unmodifiableList(myCandidateFilterSearchParams);
|
||||
}
|
||||
|
||||
public String getEnterpriseEIDSystem() {
|
||||
|
@ -132,8 +131,8 @@ public class EmpiRulesJson implements IModelJson {
|
|||
}
|
||||
|
||||
public String getSummary() {
|
||||
return myResourceSearchParams.size() + " Candidate Search Params, " +
|
||||
myFilterSearchParams.size() + " Filter Search Params, " +
|
||||
return myCandidateSearchParams.size() + " Candidate Search Params, " +
|
||||
myCandidateFilterSearchParams.size() + " Filter Search Params, " +
|
||||
myMatchFieldJsonList.size() + " Match Fields, " +
|
||||
myMatchResultMap.size() + " Match Result Entries";
|
||||
}
|
||||
|
@ -142,6 +141,18 @@ public class EmpiRulesJson implements IModelJson {
|
|||
return myVectorMatchResultMap.getFieldMatchNames(theVector);
|
||||
}
|
||||
|
||||
public String getDetailedFieldMatchResultForUnmatchedVector(long theVector) {
|
||||
List<String> fieldMatchResult = new ArrayList<>();
|
||||
for (int i = 0; i < myMatchFieldJsonList.size(); ++i) {
|
||||
if ((theVector & (1 << i)) == 0) {
|
||||
fieldMatchResult.add(myMatchFieldJsonList.get(i).getName() + ": NO");
|
||||
} else {
|
||||
fieldMatchResult.add(myMatchFieldJsonList.get(i).getName() + ": YES");
|
||||
}
|
||||
}
|
||||
return String.join("\n" ,fieldMatchResult);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
VectorMatchResultMap getVectorMatchResultMapForUnitTest() {
|
||||
return myVectorMatchResultMap;
|
||||
|
|
|
@ -20,15 +20,20 @@ package ca.uhn.fhir.empi.rules.json;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class VectorMatchResultMap {
|
||||
private final EmpiRulesJson myEmpiRulesJson;
|
||||
private Map<Long, EmpiMatchResultEnum> myVectorToMatchResultMap = new HashMap<>();
|
||||
private Set<Long> myMatchVectors = new HashSet<>();
|
||||
private Set<Long> myPossibleMatchVectors = new HashSet<>();
|
||||
private Map<Long, String> myVectorToFieldMatchNamesMap = new HashMap<>();
|
||||
|
||||
VectorMatchResultMap(EmpiRulesJson theEmpiRulesJson) {
|
||||
|
@ -43,14 +48,30 @@ public class VectorMatchResultMap {
|
|||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public EmpiMatchResultEnum get(Long theMatchVector) {
|
||||
return myVectorToMatchResultMap.get(theMatchVector);
|
||||
return myVectorToMatchResultMap.computeIfAbsent(theMatchVector, this::computeMatchResult);
|
||||
}
|
||||
|
||||
private EmpiMatchResultEnum computeMatchResult(Long theVector) {
|
||||
if (myMatchVectors.stream().anyMatch(v -> (v & theVector) != 0)) {
|
||||
return EmpiMatchResultEnum.MATCH;
|
||||
}
|
||||
if (myPossibleMatchVectors.stream().anyMatch(v -> (v & theVector) != 0)) {
|
||||
return EmpiMatchResultEnum.POSSIBLE_MATCH;
|
||||
}
|
||||
return EmpiMatchResultEnum.NO_MATCH;
|
||||
}
|
||||
|
||||
private void put(String theFieldMatchNames, EmpiMatchResultEnum theMatchResult) {
|
||||
long vector = getVector(theFieldMatchNames);
|
||||
myVectorToFieldMatchNamesMap.put(vector, theFieldMatchNames);
|
||||
myVectorToMatchResultMap.put(vector, theMatchResult);
|
||||
if (theMatchResult == EmpiMatchResultEnum.MATCH) {
|
||||
myMatchVectors.add(vector);
|
||||
} else if (theMatchResult == EmpiMatchResultEnum.POSSIBLE_MATCH) {
|
||||
myPossibleMatchVectors.add(vector);
|
||||
}
|
||||
}
|
||||
|
||||
public long getVector(String theFieldMatchNames) {
|
||||
|
@ -58,7 +79,7 @@ public class VectorMatchResultMap {
|
|||
for (String fieldMatchName : splitFieldMatchNames(theFieldMatchNames)) {
|
||||
int index = getFieldMatchIndex(fieldMatchName);
|
||||
if (index == -1) {
|
||||
throw new IllegalArgumentException("There is no matchField with name " + fieldMatchName);
|
||||
throw new ConfigurationException("There is no matchField with name " + fieldMatchName);
|
||||
}
|
||||
retval |= (1 << index);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
package ca.uhn.fhir.empi.rules.metric;
|
||||
|
||||
/*-
|
||||
* #%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.rules.metric.matcher.DoubleMetaphoneStringMatcher;
|
||||
import ca.uhn.fhir.empi.rules.metric.matcher.EmpiPersonNameMatchModeEnum;
|
||||
import ca.uhn.fhir.empi.rules.metric.matcher.HapiDateMatcher;
|
||||
import ca.uhn.fhir.empi.rules.metric.matcher.HapiStringMatcher;
|
||||
import ca.uhn.fhir.empi.rules.metric.matcher.IEmpiFieldMatcher;
|
||||
import ca.uhn.fhir.empi.rules.metric.matcher.MetaphoneStringMatcher;
|
||||
import ca.uhn.fhir.empi.rules.metric.matcher.NameMatcher;
|
||||
import ca.uhn.fhir.empi.rules.metric.matcher.StringEncoderMatcher;
|
||||
import ca.uhn.fhir.empi.rules.metric.matcher.SubstringStringMatcher;
|
||||
import ca.uhn.fhir.empi.rules.metric.similarity.HapiStringSimilarity;
|
||||
import ca.uhn.fhir.empi.rules.metric.similarity.IEmpiFieldSimilarity;
|
||||
import info.debatty.java.stringsimilarity.Cosine;
|
||||
import info.debatty.java.stringsimilarity.Jaccard;
|
||||
import info.debatty.java.stringsimilarity.JaroWinkler;
|
||||
import info.debatty.java.stringsimilarity.NormalizedLevenshtein;
|
||||
import info.debatty.java.stringsimilarity.SorensenDice;
|
||||
import org.apache.commons.codec.language.Caverphone1;
|
||||
import org.apache.commons.codec.language.Caverphone2;
|
||||
import org.apache.commons.codec.language.Soundex;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Enum for holding all the known distance metrics that we support in HAPI for
|
||||
* calculating differences between strings (https://en.wikipedia.org/wiki/String_metric)
|
||||
*/
|
||||
public enum EmpiMetricEnum {
|
||||
STRING(new HapiStringMatcher()),
|
||||
SUBSTRING(new HapiStringMatcher(new SubstringStringMatcher())),
|
||||
METAPHONE(new HapiStringMatcher(new MetaphoneStringMatcher())),
|
||||
DOUBLE_METAPHONE(new HapiStringMatcher(new DoubleMetaphoneStringMatcher())),
|
||||
SOUNDEX(new HapiStringMatcher(new StringEncoderMatcher(new Soundex()))),
|
||||
CAVERPHONE1(new HapiStringMatcher(new StringEncoderMatcher(new Caverphone1()))),
|
||||
CAVERPHONE2(new HapiStringMatcher(new StringEncoderMatcher(new Caverphone2()))),
|
||||
DATE(new HapiDateMatcher()),
|
||||
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())),
|
||||
NAME_ANY_ORDER(new NameMatcher(EmpiPersonNameMatchModeEnum.ANY_ORDER)),
|
||||
NAME_FIRST_AND_LAST(new NameMatcher(EmpiPersonNameMatchModeEnum.FIRST_AND_LAST));
|
||||
|
||||
private final IEmpiFieldMetric myEmpiFieldMetric;
|
||||
|
||||
EmpiMetricEnum(IEmpiFieldMetric theEmpiFieldMetric) {
|
||||
myEmpiFieldMetric = theEmpiFieldMetric;
|
||||
}
|
||||
|
||||
public boolean match(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) {
|
||||
return ((IEmpiFieldMatcher) myEmpiFieldMetric).matches(theFhirContext, theLeftBase, theRightBase, theExact);
|
||||
}
|
||||
|
||||
public boolean match(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, @Nullable Double theThreshold) {
|
||||
if (isSimilarity()) {
|
||||
return ((IEmpiFieldSimilarity) myEmpiFieldMetric).similarity(theFhirContext, theLeftBase, theRightBase, theExact) >= theThreshold;
|
||||
} else {
|
||||
return ((IEmpiFieldMatcher) myEmpiFieldMetric).matches(theFhirContext, theLeftBase, theRightBase, theExact);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSimilarity() {
|
||||
return myEmpiFieldMetric instanceof IEmpiFieldSimilarity;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package ca.uhn.fhir.empi.rules.metric;
|
||||
|
||||
public interface IEmpiFieldMetric {
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
import ca.uhn.fhir.util.StringUtil;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
|
||||
public abstract class BaseHapiStringMetric {
|
||||
protected String extractString(IPrimitiveType<?> thePrimitive, boolean theExact) {
|
||||
String theString = thePrimitive.getValueAsString();
|
||||
if (theExact) {
|
||||
return theString;
|
||||
}
|
||||
return StringUtil.normalizeStringForSearchIndexing(theString);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
import org.apache.commons.codec.language.DoubleMetaphone;
|
||||
|
||||
public class DoubleMetaphoneStringMatcher implements IEmpiStringMatcher {
|
||||
@Override
|
||||
public boolean matches(String theLeftString, String theRightString) {
|
||||
return new DoubleMetaphone().isDoubleMetaphoneEqual(theLeftString, theRightString);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package ca.uhn.fhir.empi.rules.similarity;
|
||||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
|
@ -21,8 +21,6 @@ package ca.uhn.fhir.empi.rules.similarity;
|
|||
*/
|
||||
|
||||
public enum EmpiPersonNameMatchModeEnum {
|
||||
STANDARD_ANY_ORDER,
|
||||
EXACT_ANY_ORDER,
|
||||
STANDARD_FIRST_AND_LAST,
|
||||
EXACT_FIRST_AND_LAST
|
||||
ANY_ORDER,
|
||||
FIRST_AND_LAST
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
|
||||
public class HapiDateMatcher implements IEmpiFieldMatcher {
|
||||
private final HapiDateMatcherDstu3 myHapiDateMatcherDstu3 = new HapiDateMatcherDstu3();
|
||||
private final HapiDateMatcherR4 myHapiDateMatcherR4 = new HapiDateMatcherR4();
|
||||
|
||||
@Override
|
||||
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) {
|
||||
switch (theFhirContext.getVersion().getVersion()) {
|
||||
case DSTU3:
|
||||
return myHapiDateMatcherDstu3.match(theLeftBase, theRightBase);
|
||||
case R4:
|
||||
return myHapiDateMatcherR4.match(theLeftBase, theRightBase);
|
||||
default:
|
||||
throw new UnsupportedOperationException("Version not supported: " + theFhirContext.getVersion().getVersion());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
import org.hl7.fhir.dstu3.model.BaseDateTimeType;
|
||||
import org.hl7.fhir.dstu3.model.DateTimeType;
|
||||
import org.hl7.fhir.dstu3.model.DateType;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
|
||||
public class HapiDateMatcherDstu3 {
|
||||
// TODO KHS code duplication (tried generalizing it with generics, but it got too convoluted)
|
||||
public boolean match(IBase theLeftBase, IBase theRightBase) {
|
||||
if (theLeftBase instanceof BaseDateTimeType && theRightBase instanceof BaseDateTimeType) {
|
||||
BaseDateTimeType leftDate = (BaseDateTimeType) theLeftBase;
|
||||
BaseDateTimeType rightDate = (BaseDateTimeType) theRightBase;
|
||||
int comparison = leftDate.getPrecision().compareTo(rightDate.getPrecision());
|
||||
if (comparison == 0) {
|
||||
return leftDate.getValueAsString().equals(rightDate.getValueAsString());
|
||||
}
|
||||
BaseDateTimeType leftPDate;
|
||||
BaseDateTimeType rightPDate;
|
||||
if (comparison > 0) {
|
||||
leftPDate = leftDate;
|
||||
if (rightDate instanceof DateType) {
|
||||
rightPDate = new DateType(rightDate.getValue(), leftDate.getPrecision());
|
||||
} else {
|
||||
rightPDate = new DateTimeType(rightDate.getValue(), leftDate.getPrecision());
|
||||
}
|
||||
} else {
|
||||
rightPDate = rightDate;
|
||||
if (leftDate instanceof DateType) {
|
||||
leftPDate = new DateType(leftDate.getValue(), rightDate.getPrecision());
|
||||
} else {
|
||||
leftPDate = new DateTimeType(leftDate.getValue(), rightDate.getPrecision());
|
||||
}
|
||||
}
|
||||
return leftPDate.getValueAsString().equals(rightPDate.getValueAsString());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
import org.hl7.fhir.r4.model.BaseDateTimeType;
|
||||
import org.hl7.fhir.r4.model.DateTimeType;
|
||||
import org.hl7.fhir.r4.model.DateType;
|
||||
|
||||
public class HapiDateMatcherR4 {
|
||||
// TODO KHS code duplication (tried generalizing it with generics, but it got too convoluted)
|
||||
public boolean match(IBase theLeftBase, IBase theRightBase) {
|
||||
if (theLeftBase instanceof BaseDateTimeType && theRightBase instanceof BaseDateTimeType) {
|
||||
BaseDateTimeType leftDate = (BaseDateTimeType) theLeftBase;
|
||||
BaseDateTimeType rightDate = (BaseDateTimeType) theRightBase;
|
||||
int comparison = leftDate.getPrecision().compareTo(rightDate.getPrecision());
|
||||
if (comparison == 0) {
|
||||
return leftDate.getValueAsString().equals(rightDate.getValueAsString());
|
||||
}
|
||||
BaseDateTimeType leftPDate;
|
||||
BaseDateTimeType rightPDate;
|
||||
if (comparison > 0) {
|
||||
leftPDate = leftDate;
|
||||
if (rightDate instanceof DateType) {
|
||||
rightPDate = new DateType(rightDate.getValue(), leftDate.getPrecision());
|
||||
} else {
|
||||
rightPDate = new DateTimeType(rightDate.getValue(), leftDate.getPrecision());
|
||||
}
|
||||
} else {
|
||||
rightPDate = rightDate;
|
||||
if (leftDate instanceof DateType) {
|
||||
leftPDate = new DateType(leftDate.getValue(), rightDate.getPrecision());
|
||||
} else {
|
||||
leftPDate = new DateTimeType(leftDate.getValue(), rightDate.getPrecision());
|
||||
}
|
||||
}
|
||||
return leftPDate.getValueAsString().equals(rightPDate.getValueAsString());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
/*-
|
||||
* #%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 org.hl7.fhir.instance.model.api.IBase;
|
||||
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 IEmpiFieldMatcher {
|
||||
private final IEmpiStringMatcher myStringMatcher;
|
||||
|
||||
public HapiStringMatcher(IEmpiStringMatcher theStringMatcher) {
|
||||
myStringMatcher = theStringMatcher;
|
||||
}
|
||||
|
||||
public HapiStringMatcher() {
|
||||
myStringMatcher = String::equals;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) {
|
||||
if (theLeftBase instanceof IPrimitiveType && theRightBase instanceof IPrimitiveType) {
|
||||
String leftString = extractString((IPrimitiveType<?>) theLeftBase, theExact);
|
||||
String rightString = extractString((IPrimitiveType<?>) theRightBase, theExact);
|
||||
|
||||
return myStringMatcher.matches(leftString, rightString);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package ca.uhn.fhir.empi.rules.similarity;
|
||||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
|
@ -21,12 +21,12 @@ package ca.uhn.fhir.empi.rules.similarity;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.empi.rules.metric.IEmpiFieldMetric;
|
||||
import org.hl7.fhir.instance.model.api.IBase;
|
||||
|
||||
public class ReferenceMatchSimilarity implements IEmpiFieldSimilarity {
|
||||
@Override
|
||||
public double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase) {
|
||||
System.out.println("wip!");
|
||||
return 1;
|
||||
}
|
||||
/**
|
||||
* Measure how similar two IBase (resource fields) are to one another. 1.0 means identical. 0.0 means completely different.
|
||||
*/
|
||||
public interface IEmpiFieldMatcher extends IEmpiFieldMetric {
|
||||
boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
public interface IEmpiStringMatcher {
|
||||
boolean matches(String theLeftString, String theRightString);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
import org.apache.commons.codec.language.Metaphone;
|
||||
|
||||
public class MetaphoneStringMatcher implements IEmpiStringMatcher {
|
||||
@Override
|
||||
public boolean matches(String theLeftString, String theRightString) {
|
||||
return new Metaphone().isMetaphoneEqual(theLeftString, theRightString);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package ca.uhn.fhir.empi.rules.similarity;
|
||||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
|
@ -32,30 +32,28 @@ import java.util.stream.Collectors;
|
|||
/**
|
||||
* Similarity measure for two IBase name fields
|
||||
*/
|
||||
public class NameSimilarity implements IEmpiFieldSimilarity {
|
||||
public class NameMatcher implements IEmpiFieldMatcher {
|
||||
|
||||
private final EmpiPersonNameMatchModeEnum myMatchMode;
|
||||
|
||||
public NameSimilarity(EmpiPersonNameMatchModeEnum theMatchMode) {
|
||||
public NameMatcher(EmpiPersonNameMatchModeEnum theMatchMode) {
|
||||
myMatchMode = theMatchMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase) {
|
||||
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) {
|
||||
String leftFamilyName = NameUtil.extractFamilyName(theFhirContext, theLeftBase);
|
||||
String rightFamilyName = NameUtil.extractFamilyName(theFhirContext, theRightBase);
|
||||
if (StringUtils.isEmpty(leftFamilyName) || StringUtils.isEmpty(rightFamilyName)) {
|
||||
return 0.0;
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean match = false;
|
||||
boolean exact =
|
||||
myMatchMode == EmpiPersonNameMatchModeEnum.EXACT_ANY_ORDER ||
|
||||
myMatchMode == EmpiPersonNameMatchModeEnum.STANDARD_FIRST_AND_LAST;
|
||||
|
||||
List<String> leftGivenNames = NameUtil.extractGivenNames(theFhirContext, theLeftBase);
|
||||
List<String> rightGivenNames = NameUtil.extractGivenNames(theFhirContext, theRightBase);
|
||||
|
||||
if (!exact) {
|
||||
if (!theExact) {
|
||||
leftFamilyName = StringUtil.normalizeStringForSearchIndexing(leftFamilyName);
|
||||
rightFamilyName = StringUtil.normalizeStringForSearchIndexing(rightFamilyName);
|
||||
leftGivenNames = leftGivenNames.stream().map(StringUtil::normalizeStringForSearchIndexing).collect(Collectors.toList());
|
||||
|
@ -65,12 +63,12 @@ public class NameSimilarity implements IEmpiFieldSimilarity {
|
|||
for (String leftGivenName : leftGivenNames) {
|
||||
for (String rightGivenName : rightGivenNames) {
|
||||
match |= leftGivenName.equals(rightGivenName) && leftFamilyName.equals(rightFamilyName);
|
||||
if (myMatchMode == EmpiPersonNameMatchModeEnum.STANDARD_ANY_ORDER || myMatchMode == EmpiPersonNameMatchModeEnum.EXACT_ANY_ORDER) {
|
||||
if (myMatchMode == EmpiPersonNameMatchModeEnum.ANY_ORDER) {
|
||||
match |= leftGivenName.equals(rightFamilyName) && leftFamilyName.equals(rightGivenName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return match ? 1.0 : 0.0;
|
||||
return match;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
import org.apache.commons.codec.EncoderException;
|
||||
import org.apache.commons.codec.StringEncoder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class StringEncoderMatcher implements IEmpiStringMatcher {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(StringEncoderMatcher.class);
|
||||
|
||||
private final StringEncoder myStringEncoder;
|
||||
|
||||
public StringEncoderMatcher(StringEncoder theStringEncoder) {
|
||||
myStringEncoder = theStringEncoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String theLeftString, String theRightString) {
|
||||
try {
|
||||
return myStringEncoder.encode(theLeftString).equals(myStringEncoder.encode(theRightString));
|
||||
} catch (EncoderException e) {
|
||||
ourLog.error("Failed to match strings '{}' and '{}' using encoder {}", theLeftString, theRightString, myStringEncoder.getClass().getName(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
public class SubstringStringMatcher implements IEmpiStringMatcher {
|
||||
@Override
|
||||
public boolean matches(String theLeftString, String theRightString) {
|
||||
return theLeftString.startsWith(theRightString) || theRightString.startsWith(theLeftString);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package ca.uhn.fhir.empi.rules.similarity;
|
||||
package ca.uhn.fhir.empi.rules.metric.similarity;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
|
@ -21,6 +21,7 @@ package ca.uhn.fhir.empi.rules.similarity;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.empi.rules.metric.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;
|
||||
|
@ -28,18 +29,20 @@ 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 implements IEmpiFieldSimilarity {
|
||||
public class HapiStringSimilarity extends BaseHapiStringMetric implements IEmpiFieldSimilarity {
|
||||
private final NormalizedStringSimilarity myStringSimilarity;
|
||||
|
||||
public HapiStringSimilarity(NormalizedStringSimilarity theStringSimilarity) {
|
||||
myStringSimilarity = theStringSimilarity;
|
||||
}
|
||||
|
||||
public double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase) {
|
||||
@Override
|
||||
public double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) {
|
||||
if (theLeftBase instanceof IPrimitiveType && theRightBase instanceof IPrimitiveType) {
|
||||
IPrimitiveType<?> leftString = (IPrimitiveType<?>) theLeftBase;
|
||||
IPrimitiveType<?> rightString = (IPrimitiveType<?>) theRightBase;
|
||||
return myStringSimilarity.similarity(leftString.getValueAsString(), rightString.getValueAsString());
|
||||
String leftString = extractString((IPrimitiveType<?>) theLeftBase, theExact);
|
||||
String rightString = extractString((IPrimitiveType<?>) theRightBase, theExact);
|
||||
|
||||
return myStringSimilarity.similarity(leftString, rightString);
|
||||
}
|
||||
return 0.0;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package ca.uhn.fhir.empi.rules.similarity;
|
||||
package ca.uhn.fhir.empi.rules.metric.similarity;
|
||||
|
||||
/*-
|
||||
* #%L
|
||||
|
@ -21,11 +21,12 @@ package ca.uhn.fhir.empi.rules.similarity;
|
|||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.empi.rules.metric.IEmpiFieldMetric;
|
||||
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 IEmpiFieldSimilarity {
|
||||
double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase);
|
||||
public interface IEmpiFieldSimilarity extends IEmpiFieldMetric {
|
||||
double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact);
|
||||
}
|
|
@ -34,13 +34,13 @@ 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 EmpiResourceFieldComparator {
|
||||
public class EmpiResourceFieldMatcher {
|
||||
private final FhirContext myFhirContext;
|
||||
private final EmpiFieldMatchJson myEmpiFieldMatchJson;
|
||||
private final String myResourceType;
|
||||
private final String myResourcePath;
|
||||
|
||||
public EmpiResourceFieldComparator(FhirContext theFhirContext, EmpiFieldMatchJson theEmpiFieldMatchJson) {
|
||||
public EmpiResourceFieldMatcher(FhirContext theFhirContext, EmpiFieldMatchJson theEmpiFieldMatchJson) {
|
||||
myFhirContext = theFhirContext;
|
||||
myEmpiFieldMatchJson = theEmpiFieldMatchJson;
|
||||
myResourceType = theEmpiFieldMatchJson.getResourceType();
|
||||
|
@ -80,7 +80,7 @@ public class EmpiResourceFieldComparator {
|
|||
}
|
||||
|
||||
private boolean match(IBase theLeftValue, IBase theRightValue) {
|
||||
return myEmpiFieldMatchJson.getMetric().similarity(myFhirContext, theLeftValue, theRightValue) >= myEmpiFieldMatchJson.getMatchThreshold();
|
||||
return myEmpiFieldMatchJson.getMetric().match(myFhirContext, theLeftValue, theRightValue, myEmpiFieldMatchJson.getExact(), myEmpiFieldMatchJson.getMatchThreshold());
|
||||
}
|
||||
|
||||
private void validate(IBaseResource theResource) {
|
|
@ -43,16 +43,16 @@ import java.util.List;
|
|||
*/
|
||||
|
||||
@Service
|
||||
public class EmpiResourceComparatorSvc {
|
||||
public class EmpiResourceMatcherSvc {
|
||||
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
|
||||
|
||||
private final FhirContext myFhirContext;
|
||||
private final IEmpiSettings myEmpiConfig;
|
||||
private EmpiRulesJson myEmpiRulesJson;
|
||||
private final List<EmpiResourceFieldComparator> myFieldComparators = new ArrayList<>();
|
||||
private final List<EmpiResourceFieldMatcher> myFieldMatchers = new ArrayList<>();
|
||||
|
||||
@Autowired
|
||||
public EmpiResourceComparatorSvc(FhirContext theFhirContext, IEmpiSettings theEmpiConfig) {
|
||||
public EmpiResourceMatcherSvc(FhirContext theFhirContext, IEmpiSettings theEmpiConfig) {
|
||||
myFhirContext = theFhirContext;
|
||||
myEmpiConfig = theEmpiConfig;
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ public class EmpiResourceComparatorSvc {
|
|||
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()) {
|
||||
myFieldComparators.add(new EmpiResourceFieldComparator(myFhirContext, matchFieldJson));
|
||||
myFieldMatchers.add(new EmpiResourceFieldMatcher(myFhirContext, matchFieldJson));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -79,14 +79,18 @@ public class EmpiResourceComparatorSvc {
|
|||
* @return an {@link EmpiMatchResultEnum} indicating the result of the comparison.
|
||||
*/
|
||||
public EmpiMatchResultEnum getMatchResult(IBaseResource theLeftResource, IBaseResource theRightResource) {
|
||||
return compare(theLeftResource, theRightResource);
|
||||
return match(theLeftResource, theRightResource);
|
||||
}
|
||||
|
||||
EmpiMatchResultEnum compare(IBaseResource theLeftResource, IBaseResource theRightResource) {
|
||||
EmpiMatchResultEnum match(IBaseResource theLeftResource, IBaseResource theRightResource) {
|
||||
long matchVector = getMatchVector(theLeftResource, theRightResource);
|
||||
EmpiMatchResultEnum matchResult = myEmpiRulesJson.getMatchResult(matchVector);
|
||||
if (ourLog.isTraceEnabled() && matchResult == EmpiMatchResultEnum.MATCH || matchResult == EmpiMatchResultEnum.POSSIBLE_MATCH) {
|
||||
ourLog.trace("{} {} with field matchers {}", matchResult, theRightResource.getIdElement().toUnqualifiedVersionless(), myEmpiRulesJson.getFieldMatchNamesForVector(matchVector));
|
||||
if (ourLog.isDebugEnabled()) {
|
||||
if (matchResult == EmpiMatchResultEnum.MATCH || matchResult == EmpiMatchResultEnum.POSSIBLE_MATCH) {
|
||||
ourLog.debug("{} {} with field matchers {}", matchResult, theRightResource.getIdElement().toUnqualifiedVersionless(), myEmpiRulesJson.getFieldMatchNamesForVector(matchVector));
|
||||
} else if (ourLog.isTraceEnabled()) {
|
||||
ourLog.trace("{} {}. Field matcher results: {}", matchResult, theRightResource.getIdElement().toUnqualifiedVersionless(), myEmpiRulesJson.getDetailedFieldMatchResultForUnmatchedVector(matchVector));
|
||||
}
|
||||
}
|
||||
return matchResult;
|
||||
}
|
||||
|
@ -108,9 +112,9 @@ public class EmpiResourceComparatorSvc {
|
|||
*/
|
||||
private long getMatchVector(IBaseResource theLeftResource, IBaseResource theRightResource) {
|
||||
long retval = 0;
|
||||
for (int i = 0; i < myFieldComparators.size(); ++i) {
|
||||
for (int i = 0; i < myFieldMatchers.size(); ++i) {
|
||||
//any that are not for the resourceType in question.
|
||||
EmpiResourceFieldComparator fieldComparator = myFieldComparators.get(i);
|
||||
EmpiResourceFieldMatcher fieldComparator = myFieldMatchers.get(i);
|
||||
if (fieldComparator.match(theLeftResource, theRightResource)) {
|
||||
retval |= (1 << i);
|
||||
}
|
|
@ -1,39 +1,21 @@
|
|||
package ca.uhn.fhir.empi;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
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.DistanceMetricEnum;
|
||||
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.svc.EmpiResourceComparatorSvc;
|
||||
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.Before;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public abstract class BaseR4Test {
|
||||
protected static final FhirContext ourFhirContext = FhirContext.forR4();
|
||||
public static final String PATIENT_GIVEN = "patient-given";
|
||||
public static final String PATIENT_LAST = "patient-last";
|
||||
public static final String PATIENT_GENERAL_PRACTITIONER= "patient-practitioner";
|
||||
|
||||
|
||||
public static final double NAME_THRESHOLD = 0.8;
|
||||
protected EmpiFieldMatchJson myGivenNameMatchField;
|
||||
protected EmpiFieldMatchJson myParentMatchField;
|
||||
protected String myBothNameFields;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
myGivenNameMatchField = new EmpiFieldMatchJson()
|
||||
.setName(PATIENT_GIVEN)
|
||||
.setResourceType("Patient")
|
||||
.setResourcePath("name.given")
|
||||
.setMetric(DistanceMetricEnum.COSINE)
|
||||
.setMatchThreshold(NAME_THRESHOLD);
|
||||
myBothNameFields = String.join(",", PATIENT_GIVEN, PATIENT_LAST);
|
||||
}
|
||||
protected ISearchParamRetriever mySearchParamRetriever = mock(ISearchParamRetriever.class);
|
||||
|
||||
protected Patient buildJohn() {
|
||||
Patient patient = new Patient();
|
||||
|
@ -49,40 +31,8 @@ public abstract class BaseR4Test {
|
|||
return patient;
|
||||
}
|
||||
|
||||
protected EmpiRulesJson buildActiveBirthdateIdRules() {
|
||||
EmpiFilterSearchParamJson activePatientsBlockingFilter = new EmpiFilterSearchParamJson()
|
||||
.setResourceType("Patient")
|
||||
.setSearchParam(Patient.SP_ACTIVE)
|
||||
.setFixedValue("true");
|
||||
|
||||
EmpiResourceSearchParamJson patientBirthdayBlocking = new EmpiResourceSearchParamJson()
|
||||
.setResourceType("Patient")
|
||||
.setSearchParam(Patient.SP_BIRTHDATE);
|
||||
EmpiResourceSearchParamJson patientIdentifierBlocking = new EmpiResourceSearchParamJson()
|
||||
.setResourceType("Patient")
|
||||
.setSearchParam(Patient.SP_IDENTIFIER);
|
||||
|
||||
|
||||
EmpiFieldMatchJson lastNameMatchField = new EmpiFieldMatchJson()
|
||||
.setName(PATIENT_LAST)
|
||||
.setResourceType("Patient")
|
||||
.setResourcePath("name.family")
|
||||
.setMetric(DistanceMetricEnum.JARO_WINKLER)
|
||||
.setMatchThreshold(NAME_THRESHOLD);
|
||||
|
||||
EmpiRulesJson retval = new EmpiRulesJson();
|
||||
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;
|
||||
}
|
||||
|
||||
protected EmpiResourceComparatorSvc buildComparator(EmpiRulesJson theEmpiRulesJson) {
|
||||
EmpiResourceComparatorSvc retval = new EmpiResourceComparatorSvc(ourFhirContext, new EmpiSettings().setEmpiRules(theEmpiRulesJson));
|
||||
protected EmpiResourceMatcherSvc buildMatcher(EmpiRulesJson theEmpiRulesJson) {
|
||||
EmpiResourceMatcherSvc retval = new EmpiResourceMatcherSvc(ourFhirContext, new EmpiSettings(new EmpiRuleValidator(ourFhirContext, mySearchParamRetriever)).setEmpiRules(theEmpiRulesJson));
|
||||
retval.init();
|
||||
return retval;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package ca.uhn.fhir.empi.rules.config;
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.empi.rules.json.EmpiRulesJson;
|
||||
import ca.uhn.fhir.empi.BaseR4Test;
|
||||
import com.google.common.base.Charsets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.junit.Test;
|
||||
|
@ -11,37 +11,99 @@ import org.springframework.core.io.Resource;
|
|||
import java.io.IOException;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.startsWith;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class EmpiRuleValidatorTest {
|
||||
private EmpiRuleValidator myEmpiRuleValidator = new EmpiRuleValidator();
|
||||
|
||||
public class EmpiRuleValidatorTest extends BaseR4Test {
|
||||
@Test
|
||||
public void testValidate() {
|
||||
String invalidUri = "invalid uri";
|
||||
EmpiRulesJson sampleEmpiRulesJson = new EmpiRulesJson();
|
||||
sampleEmpiRulesJson.setEnterpriseEIDSystem(invalidUri);
|
||||
|
||||
public void testValidate() throws IOException {
|
||||
try {
|
||||
myEmpiRuleValidator.validate(sampleEmpiRulesJson);
|
||||
setEmpiRuleJson("bad-rules-bad-url.json");
|
||||
fail();
|
||||
} catch (ConfigurationException e){
|
||||
assertThat(e.getMessage(), is("Enterprise Identifier System (eidSystem) must be a valid URI"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@Test
|
||||
public void testNonExistentMatchField() throws IOException {
|
||||
EmpiSettings empiSettings = new EmpiSettings();
|
||||
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
Resource resource = resourceLoader.getResource("bad-rules.json");
|
||||
String json = IOUtils.toString(resource.getInputStream(), Charsets.UTF_8);
|
||||
try {
|
||||
empiSettings.setScriptText(json);
|
||||
setEmpiRuleJson("bad-rules-missing-name.json");
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
} catch (ConfigurationException e) {
|
||||
assertThat(e.getMessage(), is("There is no matchField with name foo"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimilarityHasThreshold() throws IOException {
|
||||
try {
|
||||
setEmpiRuleJson("bad-rules-missing-threshold.json");
|
||||
fail();
|
||||
} catch (ConfigurationException e) {
|
||||
assertThat(e.getMessage(), is("MatchField given-name metric COSINE requires a matchThreshold"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatcherUnusedThreshold() throws IOException {
|
||||
try {
|
||||
setEmpiRuleJson("bad-rules-unused-threshold.json");
|
||||
fail();
|
||||
} catch (ConfigurationException e) {
|
||||
assertThat(e.getMessage(), is("MatchField given-name metric STRING should not have a matchThreshold"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatcherBadPath() throws IOException {
|
||||
try {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatcherBadSearchParam() throws IOException {
|
||||
try {
|
||||
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'"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatcherBadFilter() throws IOException {
|
||||
try {
|
||||
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 testMatcherduplicateName() throws IOException {
|
||||
try {
|
||||
setEmpiRuleJson("bad-rules-duplicate-name.json");
|
||||
fail();
|
||||
} catch (ConfigurationException e) {
|
||||
assertThat(e.getMessage(), startsWith("Two MatchFields have the same name 'foo'"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
empiSettings.setScriptText(json);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package ca.uhn.fhir.empi.rules.json;
|
||||
|
||||
import ca.uhn.fhir.empi.BaseR4Test;
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
|
||||
import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum;
|
||||
import ca.uhn.fhir.empi.rules.svc.BaseEmpiRulesR4Test;
|
||||
import ca.uhn.fhir.util.JsonUtil;
|
||||
import junit.framework.TestCase;
|
||||
import org.junit.Before;
|
||||
|
@ -14,7 +16,7 @@ import java.io.IOException;
|
|||
import static junit.framework.TestCase.assertEquals;
|
||||
import static junit.framework.TestCase.fail;
|
||||
|
||||
public class EmpiRulesJsonR4Test extends BaseR4Test {
|
||||
public class EmpiRulesJsonR4Test extends BaseEmpiRulesR4Test {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(EmpiRulesJsonR4Test.class);
|
||||
private EmpiRulesJson myRules;
|
||||
|
||||
|
@ -34,7 +36,7 @@ public class EmpiRulesJsonR4Test extends BaseR4Test {
|
|||
assertEquals(EmpiMatchResultEnum.MATCH, rulesDeser.getMatchResult(myBothNameFields));
|
||||
EmpiFieldMatchJson second = rulesDeser.get(1);
|
||||
assertEquals("name.family", second.getResourcePath());
|
||||
TestCase.assertEquals(DistanceMetricEnum.JARO_WINKLER, second.getMetric());
|
||||
TestCase.assertEquals(EmpiMetricEnum.JARO_WINKLER, second.getMetric());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -54,7 +56,7 @@ public class EmpiRulesJsonR4Test extends BaseR4Test {
|
|||
try {
|
||||
vectorMatchResultMap.getVector("bad");
|
||||
fail();
|
||||
} catch (IllegalArgumentException e) {
|
||||
} catch (ConfigurationException e) {
|
||||
assertEquals("There is no matchField with name bad", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package ca.uhn.fhir.empi.rules.json;
|
||||
|
||||
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
|
||||
import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum;
|
||||
import org.junit.Test;
|
||||
|
||||
import static junit.framework.TestCase.assertEquals;
|
||||
|
@ -21,4 +23,19 @@ public class VectorMatchResultMapTest {
|
|||
assertEquals("b", result[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchBeforePossibleMatch() {
|
||||
EmpiRulesJson empiRulesJson = new EmpiRulesJson();
|
||||
empiRulesJson.addMatchField(new EmpiFieldMatchJson().setName("given").setResourceType("Patient").setResourcePath("name.given").setMetric(EmpiMetricEnum.STRING));
|
||||
empiRulesJson.addMatchField(new EmpiFieldMatchJson().setName("family").setResourceType("Patient").setResourcePath("name.family").setMetric(EmpiMetricEnum.STRING));
|
||||
empiRulesJson.addMatchField(new EmpiFieldMatchJson().setName("prefix").setResourceType("Patient").setResourcePath("name.prefix").setMetric(EmpiMetricEnum.STRING));
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
|
||||
public abstract class BaseMatcherR4Test {
|
||||
protected static final FhirContext ourFhirContext = FhirContext.forR4();
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum;
|
||||
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
|
||||
import org.hl7.fhir.r4.model.DateTimeType;
|
||||
import org.hl7.fhir.r4.model.DateType;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class DateMatcherR4Test extends BaseMatcherR4Test {
|
||||
|
||||
@Test
|
||||
public void testExactDatePrecision() {
|
||||
Calendar cal = new GregorianCalendar(2020,6,15);
|
||||
Calendar sameMonthCal = new GregorianCalendar(2020,6,22);
|
||||
Calendar sameYearCal = new GregorianCalendar(2020,11,13);
|
||||
Calendar otherYearCal = new GregorianCalendar(1965,8,9);
|
||||
|
||||
Date date = cal.getTime();
|
||||
Date sameMonth = sameMonthCal.getTime();
|
||||
Date sameYear = sameYearCal.getTime();
|
||||
Date otherYear = otherYearCal.getTime();
|
||||
|
||||
assertTrue(dateMatch(date, date, TemporalPrecisionEnum.DAY));
|
||||
assertFalse(dateMatch(date, sameMonth, TemporalPrecisionEnum.DAY));
|
||||
assertFalse(dateMatch(date, sameYear, TemporalPrecisionEnum.DAY));
|
||||
assertFalse(dateMatch(date, otherYear, TemporalPrecisionEnum.DAY));
|
||||
|
||||
assertTrue(dateMatch(date, date, TemporalPrecisionEnum.MONTH));
|
||||
assertTrue(dateMatch(date, sameMonth, TemporalPrecisionEnum.MONTH));
|
||||
assertFalse(dateMatch(date, sameYear, TemporalPrecisionEnum.MONTH));
|
||||
assertFalse(dateMatch(date, otherYear, TemporalPrecisionEnum.MONTH));
|
||||
|
||||
assertTrue(dateMatch(date, date, TemporalPrecisionEnum.YEAR));
|
||||
assertTrue(dateMatch(date, sameMonth, TemporalPrecisionEnum.YEAR));
|
||||
assertTrue(dateMatch(date, sameYear, TemporalPrecisionEnum.YEAR));
|
||||
assertFalse(dateMatch(date, otherYear, TemporalPrecisionEnum.YEAR));
|
||||
}
|
||||
|
||||
private boolean dateMatch(Date theDate, Date theSameMonth, TemporalPrecisionEnum theTheDay) {
|
||||
return EmpiMetricEnum.DATE.match(ourFhirContext, new DateType(theDate, theTheDay), new DateType(theSameMonth, theTheDay), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactDateTimePrecision() {
|
||||
Calendar cal = new GregorianCalendar(2020,6,15, 11, 12, 13);
|
||||
Calendar sameSecondCal = new GregorianCalendar(2020,6,15, 11, 12, 13);
|
||||
sameSecondCal.add(Calendar.MILLISECOND, 123);
|
||||
|
||||
Calendar sameDayCal = new GregorianCalendar(2020,6,15, 12, 34, 56);
|
||||
|
||||
Date date = cal.getTime();
|
||||
Date sameSecond = sameSecondCal.getTime();
|
||||
Date sameDay = sameDayCal.getTime();
|
||||
|
||||
// Same precision
|
||||
|
||||
assertTrue(dateTimeMatch(date, date, TemporalPrecisionEnum.DAY, TemporalPrecisionEnum.DAY));
|
||||
assertTrue(dateTimeMatch(date, sameSecond, TemporalPrecisionEnum.DAY, TemporalPrecisionEnum.DAY));
|
||||
assertTrue(dateTimeMatch(date, sameDay, TemporalPrecisionEnum.DAY, TemporalPrecisionEnum.DAY));
|
||||
|
||||
assertTrue(dateTimeMatch(date, date, TemporalPrecisionEnum.SECOND, TemporalPrecisionEnum.SECOND));
|
||||
assertTrue(dateTimeMatch(date, sameSecond, TemporalPrecisionEnum.SECOND, TemporalPrecisionEnum.SECOND));
|
||||
assertFalse(dateTimeMatch(date, sameDay, TemporalPrecisionEnum.SECOND, TemporalPrecisionEnum.SECOND));
|
||||
|
||||
assertTrue(dateTimeMatch(date, date, TemporalPrecisionEnum.MILLI, TemporalPrecisionEnum.MILLI));
|
||||
assertFalse(dateTimeMatch(date, sameSecond, TemporalPrecisionEnum.MILLI, TemporalPrecisionEnum.MILLI));
|
||||
assertFalse(dateTimeMatch(date, sameDay, TemporalPrecisionEnum.MILLI, TemporalPrecisionEnum.MILLI));
|
||||
|
||||
// Different precision matches by coarser precision
|
||||
assertTrue(dateTimeMatch(date, date, TemporalPrecisionEnum.SECOND, TemporalPrecisionEnum.DAY));
|
||||
assertTrue(dateTimeMatch(date, sameSecond, TemporalPrecisionEnum.SECOND, TemporalPrecisionEnum.DAY));
|
||||
assertFalse(dateTimeMatch(date, sameDay, TemporalPrecisionEnum.SECOND, TemporalPrecisionEnum.DAY));
|
||||
|
||||
assertTrue(dateTimeMatch(date, date, TemporalPrecisionEnum.DAY, TemporalPrecisionEnum.SECOND));
|
||||
assertTrue(dateTimeMatch(date, sameSecond, TemporalPrecisionEnum.DAY, TemporalPrecisionEnum.SECOND));
|
||||
assertFalse(dateTimeMatch(date, sameDay, TemporalPrecisionEnum.DAY, TemporalPrecisionEnum.SECOND));
|
||||
}
|
||||
|
||||
private boolean dateTimeMatch(Date theDate, Date theSameSecond, TemporalPrecisionEnum theTheDay, TemporalPrecisionEnum theTheDay2) {
|
||||
return EmpiMetricEnum.DATE.match(ourFhirContext, new DateTimeType(theDate, theTheDay), new DateTimeType(theSameSecond, theTheDay2), true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package ca.uhn.fhir.empi.rules.metric.matcher;
|
||||
|
||||
import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum;
|
||||
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.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class StringMatcherR4Test extends BaseMatcherR4Test {
|
||||
@Test
|
||||
public void testMetaphone() {
|
||||
assertTrue(match(EmpiMetricEnum.METAPHONE, new StringType("Durie"), new StringType("dury")));
|
||||
assertTrue(match(EmpiMetricEnum.METAPHONE, new StringType("Balo"), new StringType("ballo")));
|
||||
assertTrue(match(EmpiMetricEnum.METAPHONE, new StringType("Hans Peter"), new StringType("Hanspeter")));
|
||||
assertTrue(match(EmpiMetricEnum.METAPHONE, new StringType("Lawson"), new StringType("Law son")));
|
||||
|
||||
assertFalse(match(EmpiMetricEnum.METAPHONE, new StringType("Allsop"), new StringType("Allsob")));
|
||||
assertFalse(match(EmpiMetricEnum.METAPHONE, new StringType("Gevne"), new StringType("Geve")));
|
||||
assertFalse(match(EmpiMetricEnum.METAPHONE, new StringType("Bruce"), new StringType("Bruch")));
|
||||
assertFalse(match(EmpiMetricEnum.METAPHONE, new StringType("Smith"), new StringType("Schmidt")));
|
||||
assertFalse(match(EmpiMetricEnum.METAPHONE, new StringType("Jyothi"), new StringType("Jyoti")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoubleMetaphone() {
|
||||
assertTrue(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Durie"), new StringType("dury")));
|
||||
assertTrue(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Balo"), new StringType("ballo")));
|
||||
assertTrue(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Hans Peter"), new StringType("Hanspeter")));
|
||||
assertTrue(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Lawson"), new StringType("Law son")));
|
||||
assertTrue(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Allsop"), new StringType("Allsob")));
|
||||
|
||||
assertFalse(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Gevne"), new StringType("Geve")));
|
||||
assertFalse(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Bruce"), new StringType("Bruch")));
|
||||
assertFalse(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Smith"), new StringType("Schmidt")));
|
||||
assertFalse(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Jyothi"), new StringType("Jyoti")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalizeCase() {
|
||||
assertTrue(match(EmpiMetricEnum.STRING, new StringType("joe"), new StringType("JoE")));
|
||||
assertTrue(match(EmpiMetricEnum.STRING, new StringType("MCTAVISH"), new StringType("McTavish")));
|
||||
|
||||
assertFalse(match(EmpiMetricEnum.STRING, new StringType("joey"), new StringType("joe")));
|
||||
assertFalse(match(EmpiMetricEnum.STRING, new StringType("joe"), new StringType("joey")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactString() {
|
||||
assertTrue(EmpiMetricEnum.STRING.match(ourFhirContext, new StringType("Jilly"), new StringType("Jilly"), true));
|
||||
|
||||
assertFalse(EmpiMetricEnum.STRING.match(ourFhirContext, new StringType("MCTAVISH"), new StringType("McTavish"), true));
|
||||
assertFalse(EmpiMetricEnum.STRING.match(ourFhirContext, new StringType("Durie"), new StringType("dury"), true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactBoolean() {
|
||||
assertTrue(EmpiMetricEnum.STRING.match(ourFhirContext, new BooleanType(true), new BooleanType(true), true));
|
||||
|
||||
assertFalse(EmpiMetricEnum.STRING.match(ourFhirContext, new BooleanType(true), new BooleanType(false), true));
|
||||
assertFalse(EmpiMetricEnum.STRING.match(ourFhirContext, new BooleanType(false), new BooleanType(true), true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactDateString() {
|
||||
assertTrue(EmpiMetricEnum.STRING.match(ourFhirContext, new DateType("1965-08-09"), new DateType("1965-08-09"), true));
|
||||
|
||||
assertFalse(EmpiMetricEnum.STRING.match(ourFhirContext, new DateType("1965-08-09"), new DateType("1965-09-08"), true));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testExactGender() {
|
||||
Enumeration<Enumerations.AdministrativeGender> male = new Enumeration<Enumerations.AdministrativeGender>(new Enumerations.AdministrativeGenderEnumFactory());
|
||||
male.setValue(Enumerations.AdministrativeGender.MALE);
|
||||
|
||||
Enumeration<Enumerations.AdministrativeGender> female = new Enumeration<Enumerations.AdministrativeGender>(new Enumerations.AdministrativeGenderEnumFactory());
|
||||
female.setValue(Enumerations.AdministrativeGender.FEMALE);
|
||||
|
||||
assertTrue(EmpiMetricEnum.STRING.match(ourFhirContext, male, male, true));
|
||||
|
||||
assertFalse(EmpiMetricEnum.STRING.match(ourFhirContext, male, female, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSoundex() {
|
||||
assertTrue(match(EmpiMetricEnum.SOUNDEX, new StringType("Gail"), new StringType("Gale")));
|
||||
assertTrue(match(EmpiMetricEnum.SOUNDEX, new StringType("John"), new StringType("Jon")));
|
||||
assertTrue(match(EmpiMetricEnum.SOUNDEX, new StringType("Thom"), new StringType("Tom")));
|
||||
|
||||
assertFalse(match(EmpiMetricEnum.SOUNDEX, new StringType("Fred"), new StringType("Frank")));
|
||||
assertFalse(match(EmpiMetricEnum.SOUNDEX, new StringType("Thomas"), new StringType("Tom")));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testCaverphone1() {
|
||||
assertTrue(match(EmpiMetricEnum.CAVERPHONE1, new StringType("Gail"), new StringType("Gael")));
|
||||
assertTrue(match(EmpiMetricEnum.CAVERPHONE1, new StringType("John"), new StringType("Jon")));
|
||||
|
||||
assertFalse(match(EmpiMetricEnum.CAVERPHONE1, new StringType("Gail"), new StringType("Gale")));
|
||||
assertFalse(match(EmpiMetricEnum.CAVERPHONE1, new StringType("Fred"), new StringType("Frank")));
|
||||
assertFalse(match(EmpiMetricEnum.CAVERPHONE1, new StringType("Thomas"), new StringType("Tom")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCaverphone2() {
|
||||
assertTrue(match(EmpiMetricEnum.CAVERPHONE2, new StringType("Gail"), new StringType("Gael")));
|
||||
assertTrue(match(EmpiMetricEnum.CAVERPHONE2, new StringType("John"), new StringType("Jon")));
|
||||
assertTrue(match(EmpiMetricEnum.CAVERPHONE2, new StringType("Gail"), new StringType("Gale")));
|
||||
|
||||
assertFalse(match(EmpiMetricEnum.CAVERPHONE2, new StringType("Fred"), new StringType("Frank")));
|
||||
assertFalse(match(EmpiMetricEnum.CAVERPHONE2, new StringType("Thomas"), new StringType("Tom")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalizeSubstring() {
|
||||
assertTrue(match(EmpiMetricEnum.SUBSTRING, new StringType("BILLY"), new StringType("Bill")));
|
||||
assertTrue(match(EmpiMetricEnum.SUBSTRING, new StringType("Bill"), new StringType("Billy")));
|
||||
assertTrue(match(EmpiMetricEnum.SUBSTRING, new StringType("FRED"), new StringType("Frederik")));
|
||||
|
||||
assertFalse(match(EmpiMetricEnum.SUBSTRING, new StringType("Fred"), new StringType("Friederik")));
|
||||
}
|
||||
|
||||
private boolean match(EmpiMetricEnum theMetric, StringType theLeft, StringType theRight) {
|
||||
return theMetric.match(ourFhirContext, theLeft, theRight, false);
|
||||
}
|
||||
}
|
|
@ -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.metric.EmpiMetricEnum;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.Before;
|
||||
|
||||
public abstract class BaseEmpiRulesR4Test extends BaseR4Test {
|
||||
public static final String PATIENT_GIVEN = "patient-given";
|
||||
public static final String PATIENT_LAST = "patient-last";
|
||||
|
||||
public static final double NAME_THRESHOLD = 0.8;
|
||||
protected EmpiFieldMatchJson myGivenNameMatchField;
|
||||
protected String myBothNameFields;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
myGivenNameMatchField = new EmpiFieldMatchJson()
|
||||
.setName(PATIENT_GIVEN)
|
||||
.setResourceType("Patient")
|
||||
.setResourcePath("name.given")
|
||||
.setMetric(EmpiMetricEnum.COSINE)
|
||||
.setMatchThreshold(NAME_THRESHOLD);
|
||||
myBothNameFields = String.join(",", PATIENT_GIVEN, PATIENT_LAST);
|
||||
}
|
||||
|
||||
protected EmpiRulesJson buildActiveBirthdateIdRules() {
|
||||
EmpiFilterSearchParamJson activePatientsBlockingFilter = new EmpiFilterSearchParamJson()
|
||||
.setResourceType("Patient")
|
||||
.setSearchParam(Patient.SP_ACTIVE)
|
||||
.setFixedValue("true");
|
||||
|
||||
EmpiResourceSearchParamJson patientBirthdayBlocking = new EmpiResourceSearchParamJson()
|
||||
.setResourceType("Patient")
|
||||
.setSearchParam(Patient.SP_BIRTHDATE);
|
||||
EmpiResourceSearchParamJson patientIdentifierBlocking = new EmpiResourceSearchParamJson()
|
||||
.setResourceType("Patient")
|
||||
.setSearchParam(Patient.SP_IDENTIFIER);
|
||||
|
||||
|
||||
EmpiFieldMatchJson lastNameMatchField = new EmpiFieldMatchJson()
|
||||
.setName(PATIENT_LAST)
|
||||
.setResourceType("Patient")
|
||||
.setResourcePath("name.family")
|
||||
.setMetric(EmpiMetricEnum.JARO_WINKLER)
|
||||
.setMatchThreshold(NAME_THRESHOLD);
|
||||
|
||||
EmpiRulesJson retval = new EmpiRulesJson();
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
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.DistanceMetricEnum;
|
||||
import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson;
|
||||
import ca.uhn.fhir.empi.rules.json.EmpiRulesJson;
|
||||
import org.hl7.fhir.r4.model.HumanName;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class CustomResourceComparatorR4Test extends BaseR4Test {
|
||||
|
||||
public static final String FIELD_EXACT_MATCH_NAME = DistanceMetricEnum.EXACT_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;
|
||||
|
||||
@BeforeClass
|
||||
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");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactNameAnyOrder() {
|
||||
EmpiResourceComparatorSvc nameAnyOrderComparator = buildComparator(buildNameAnyOrderRules(DistanceMetricEnum.EXACT_NAME_ANY_ORDER));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHenry));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJohn));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJOHN));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHENRY));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJaneHenry));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnSmith));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnBillyHenry));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourBillyJohnHenry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStandardNameAnyOrder() {
|
||||
EmpiResourceComparatorSvc nameAnyOrderComparator = buildComparator(buildNameAnyOrderRules(DistanceMetricEnum.STANDARD_NAME_ANY_ORDER));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHenry));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJohn));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJOHN));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHENRY));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJaneHenry));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnSmith));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnBillyHenry));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourBillyJohnHenry));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testExactNameFirstAndLast() {
|
||||
EmpiResourceComparatorSvc nameAnyOrderComparator = buildComparator(buildNameAnyOrderRules(DistanceMetricEnum.EXACT_NAME_FIRST_AND_LAST));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHenry));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJohn));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJOHN));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHENRY));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJaneHenry));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnSmith));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnBillyHenry));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourBillyJohnHenry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStandardNameFirstAndLast() {
|
||||
EmpiResourceComparatorSvc nameAnyOrderComparator = buildComparator(buildNameAnyOrderRules(DistanceMetricEnum.STANDARD_NAME_FIRST_AND_LAST));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHenry));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJohn));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJOHN));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHENRY));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJaneHenry));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnSmith));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnBillyHenry));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourBillyJohnHenry));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private EmpiRulesJson buildNameAnyOrderRules(DistanceMetricEnum theExactNameAnyOrder) {
|
||||
EmpiFieldMatchJson nameAnyOrderFieldMatch = new EmpiFieldMatchJson()
|
||||
.setName(FIELD_EXACT_MATCH_NAME)
|
||||
.setResourceType("Patient")
|
||||
.setResourcePath("name")
|
||||
.setMetric(theExactNameAnyOrder)
|
||||
.setMatchThreshold(NAME_THRESHOLD);
|
||||
|
||||
EmpiRulesJson retval = new EmpiRulesJson();
|
||||
retval.addMatchField(nameAnyOrderFieldMatch);
|
||||
retval.putMatchResult(FIELD_EXACT_MATCH_NAME, EmpiMatchResultEnum.MATCH);
|
||||
|
||||
return retval;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
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.EmpiRulesJson;
|
||||
import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum;
|
||||
import org.hl7.fhir.r4.model.HumanName;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class CustomResourceMatcherR4Test extends BaseR4Test {
|
||||
|
||||
public static final String FIELD_EXACT_MATCH_NAME = EmpiMetricEnum.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;
|
||||
|
||||
@BeforeClass
|
||||
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");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExactNameAnyOrder() {
|
||||
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_ANY_ORDER, true));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalizedNameAnyOrder() {
|
||||
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_ANY_ORDER, false));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testExactNameFirstAndLast() {
|
||||
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_FIRST_AND_LAST, true));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalizedNameFirstAndLast() {
|
||||
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_FIRST_AND_LAST, false));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry));
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry));
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private EmpiRulesJson buildNameRules(EmpiMetricEnum theExactNameAnyOrder, boolean theExact) {
|
||||
EmpiFieldMatchJson nameAnyOrderFieldMatch = new EmpiFieldMatchJson()
|
||||
.setName(FIELD_EXACT_MATCH_NAME)
|
||||
.setResourceType("Patient")
|
||||
.setResourcePath("name")
|
||||
.setMetric(theExactNameAnyOrder)
|
||||
.setExact(theExact);
|
||||
|
||||
EmpiRulesJson retval = new EmpiRulesJson();
|
||||
retval.addMatchField(nameAnyOrderFieldMatch);
|
||||
retval.putMatchResult(FIELD_EXACT_MATCH_NAME, EmpiMatchResultEnum.MATCH);
|
||||
|
||||
return retval;
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package ca.uhn.fhir.empi.rules.svc;
|
||||
|
||||
import ca.uhn.fhir.empi.BaseR4Test;
|
||||
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class EmpiResourceComparatorSvcR4Test extends BaseR4Test {
|
||||
private EmpiResourceComparatorSvc myEmpiResourceComparatorSvc;
|
||||
public static final double NAME_DELTA = 0.0001;
|
||||
|
||||
private Patient myJohn;
|
||||
private Patient myJohny;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
super.before();
|
||||
|
||||
myEmpiResourceComparatorSvc = buildComparator(buildActiveBirthdateIdRules());
|
||||
|
||||
myJohn = buildJohn();
|
||||
myJohny = buildJohny();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompareFirstNameMatch() {
|
||||
EmpiMatchResultEnum result = myEmpiResourceComparatorSvc.compare(myJohn, myJohny);
|
||||
assertEquals(EmpiMatchResultEnum.POSSIBLE_MATCH, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompareBothNamesMatch() {
|
||||
myJohn.addName().setFamily("Smith");
|
||||
myJohny.addName().setFamily("Smith");
|
||||
EmpiMatchResultEnum result = myEmpiResourceComparatorSvc.compare(myJohn, myJohny);
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchResult() {
|
||||
assertEquals(EmpiMatchResultEnum.POSSIBLE_MATCH, myEmpiResourceComparatorSvc.getMatchResult(myJohn, myJohny));
|
||||
myJohn.addName().setFamily("Smith");
|
||||
myJohny.addName().setFamily("Smith");
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, myEmpiResourceComparatorSvc.getMatchResult(myJohn, myJohny));
|
||||
Patient patient3 = new Patient();
|
||||
patient3.setId("Patient/3");
|
||||
patient3.addName().addGiven("Henry");
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, myEmpiResourceComparatorSvc.getMatchResult(myJohn, patient3));
|
||||
}
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
package ca.uhn.fhir.empi.rules.svc;
|
||||
|
||||
import ca.uhn.fhir.empi.BaseR4Test;
|
||||
import ca.uhn.fhir.empi.rules.json.DistanceMetricEnum;
|
||||
import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson;
|
||||
import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum;
|
||||
import ca.uhn.fhir.parser.DataFormatException;
|
||||
import org.hl7.fhir.r4.model.Encounter;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
|
@ -16,8 +15,8 @@ import static junit.framework.TestCase.fail;
|
|||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.core.StringStartsWith.startsWith;
|
||||
|
||||
public class EmpiResourceFieldComparatorR4Test extends BaseR4Test {
|
||||
protected EmpiResourceFieldComparator myComparator;
|
||||
public class EmpiResourceFieldMatcherR4Test extends BaseEmpiRulesR4Test {
|
||||
protected EmpiResourceFieldMatcher myComparator;
|
||||
private Patient myJohn;
|
||||
private Patient myJohny;
|
||||
|
||||
|
@ -25,7 +24,8 @@ public class EmpiResourceFieldComparatorR4Test extends BaseR4Test {
|
|||
public void before() {
|
||||
super.before();
|
||||
|
||||
myComparator = new EmpiResourceFieldComparator(ourFhirContext, myGivenNameMatchField);
|
||||
|
||||
myComparator = new EmpiResourceFieldMatcher(ourFhirContext, myGivenNameMatchField);
|
||||
myJohn = buildJohn();
|
||||
myJohny = buildJohny();
|
||||
}
|
||||
|
@ -64,9 +64,9 @@ public class EmpiResourceFieldComparatorR4Test extends BaseR4Test {
|
|||
.setName("patient-foo")
|
||||
.setResourceType("Patient")
|
||||
.setResourcePath("foo")
|
||||
.setMetric(DistanceMetricEnum.COSINE)
|
||||
.setMetric(EmpiMetricEnum.COSINE)
|
||||
.setMatchThreshold(NAME_THRESHOLD);
|
||||
EmpiResourceFieldComparator comparator = new EmpiResourceFieldComparator(ourFhirContext, matchField);
|
||||
EmpiResourceFieldMatcher comparator = new EmpiResourceFieldMatcher(ourFhirContext, matchField);
|
||||
comparator.match(myJohn, myJohny);
|
||||
fail();
|
||||
} catch (DataFormatException e) {
|
|
@ -0,0 +1,59 @@
|
|||
package ca.uhn.fhir.empi.rules.svc;
|
||||
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class EmpiResourceMatcherSvcR4Test extends BaseEmpiRulesR4Test {
|
||||
private EmpiResourceMatcherSvc myEmpiResourceMatcherSvc;
|
||||
public static final double NAME_DELTA = 0.0001;
|
||||
|
||||
private Patient myJohn;
|
||||
private Patient myJohny;
|
||||
|
||||
@Before
|
||||
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() {
|
||||
EmpiMatchResultEnum result = myEmpiResourceMatcherSvc.match(myJohn, myJohny);
|
||||
assertEquals(EmpiMatchResultEnum.POSSIBLE_MATCH, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompareBothNamesMatch() {
|
||||
myJohn.addName().setFamily("Smith");
|
||||
myJohny.addName().setFamily("Smith");
|
||||
EmpiMatchResultEnum result = myEmpiResourceMatcherSvc.match(myJohn, myJohny);
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMatchResult() {
|
||||
assertEquals(EmpiMatchResultEnum.POSSIBLE_MATCH, myEmpiResourceMatcherSvc.getMatchResult(myJohn, myJohny));
|
||||
myJohn.addName().setFamily("Smith");
|
||||
myJohny.addName().setFamily("Smith");
|
||||
assertEquals(EmpiMatchResultEnum.MATCH, myEmpiResourceMatcherSvc.getMatchResult(myJohn, myJohny));
|
||||
Patient patient3 = new Patient();
|
||||
patient3.setId("Patient/3");
|
||||
patient3.addName().addGiven("Henry");
|
||||
assertEquals(EmpiMatchResultEnum.NO_MATCH, myEmpiResourceMatcherSvc.getMatchResult(myJohn, patient3));
|
||||
}
|
||||
}
|
|
@ -1,12 +1,15 @@
|
|||
package ca.uhn.fhir.empi.svc;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
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.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -18,21 +21,26 @@ import static org.hamcrest.CoreMatchers.nullValue;
|
|||
import static org.junit.Assert.assertThat;
|
||||
|
||||
|
||||
public class EIDHelperR4Test {
|
||||
public class EIDHelperR4Test extends BaseR4Test {
|
||||
|
||||
private static final FhirContext myFhirContext = FhirContext.forR4();
|
||||
private static final FhirContext ourFhirContext = FhirContext.forR4();
|
||||
private static final String EXTERNAL_ID_SYSTEM_FOR_TEST = "http://testsystem.io/naming-system/empi";
|
||||
|
||||
private static final EmpiRulesJson myRules = new EmpiRulesJson() {{
|
||||
private static final EmpiRulesJson ourRules = new EmpiRulesJson() {{
|
||||
setEnterpriseEIDSystem(EXTERNAL_ID_SYSTEM_FOR_TEST);
|
||||
}};
|
||||
|
||||
private static final EmpiSettings mySettings = new EmpiSettings() {{
|
||||
setEmpiRules(myRules);
|
||||
}};
|
||||
private EmpiSettings myEmpiSettings;
|
||||
|
||||
private static final EIDHelper EID_HELPER = new EIDHelper(myFhirContext, mySettings);
|
||||
private EIDHelper myEidHelper;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
myEmpiSettings = new EmpiSettings(new EmpiRuleValidator(ourFhirContext, mySearchParamRetriever)) {{
|
||||
setEmpiRules(ourRules);
|
||||
}};
|
||||
myEidHelper = new EIDHelper(ourFhirContext, myEmpiSettings);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtractionOfInternalEID() {
|
||||
|
@ -42,7 +50,7 @@ public class EIDHelperR4Test {
|
|||
.setValue("simpletest")
|
||||
.setUse(Identifier.IdentifierUse.SECONDARY);
|
||||
|
||||
List<CanonicalEID> externalEid = EID_HELPER.getHapiEid(patient);
|
||||
List<CanonicalEID> externalEid = myEidHelper.getHapiEid(patient);
|
||||
|
||||
assertThat(externalEid.isEmpty(), is(false));
|
||||
assertThat(externalEid.get(0).getValue(), is(equalTo("simpletest")));
|
||||
|
@ -59,7 +67,7 @@ public class EIDHelperR4Test {
|
|||
.setSystem(EXTERNAL_ID_SYSTEM_FOR_TEST)
|
||||
.setValue(uniqueID);
|
||||
|
||||
List<CanonicalEID> externalEid = EID_HELPER.getExternalEid(patient);
|
||||
List<CanonicalEID> externalEid = myEidHelper.getExternalEid(patient);
|
||||
|
||||
assertThat(externalEid.isEmpty(), is(false));
|
||||
assertThat(externalEid.get(0).getValue(), is(equalTo(uniqueID)));
|
||||
|
@ -69,7 +77,7 @@ public class EIDHelperR4Test {
|
|||
@Test
|
||||
public void testCreationOfInternalEIDGeneratesUuidEID() {
|
||||
|
||||
CanonicalEID internalEid = EID_HELPER.createHapiEid();
|
||||
CanonicalEID internalEid = myEidHelper.createHapiEid();
|
||||
|
||||
assertThat(internalEid.getSystem(), is(equalTo(HAPI_ENTERPRISE_IDENTIFIER_SYSTEM)));
|
||||
assertThat(internalEid.getValue().length(), is(equalTo(36)));
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"candidateSearchParams" : [],
|
||||
"candidateFilterSearchParams" : [{
|
||||
"resourceType" : "Patient",
|
||||
"searchParam" : "foo"
|
||||
}],
|
||||
"matchFields" : [],
|
||||
"matchResultMap" : {}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"candidateSearchParams" : [],
|
||||
"candidateFilterSearchParams" : [],
|
||||
"matchFields" : [ {
|
||||
"name" : "given-name",
|
||||
"resourceType" : "Patient",
|
||||
"resourcePath" : "name.first",
|
||||
"metric" : "STRING",
|
||||
"exact" : true
|
||||
}],
|
||||
"matchResultMap" : {
|
||||
"given-name" : "POSSIBLE_MATCH"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"candidateSearchParams" : [{
|
||||
"resourceType" : "Patient",
|
||||
"searchParam" : "foo"
|
||||
}],
|
||||
"candidateFilterSearchParams" : [],
|
||||
"matchFields" : [],
|
||||
"matchResultMap" : {}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"candidateSearchParams" : [],
|
||||
"candidateFilterSearchParams" : [],
|
||||
"matchFields" : [],
|
||||
"matchResultMap" : {},
|
||||
"eidSystem": "invalid url"
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"candidateSearchParams": [],
|
||||
"candidateFilterSearchParams": [],
|
||||
"matchFields": [
|
||||
{
|
||||
"name": "foo",
|
||||
"resourceType": "Patient",
|
||||
"resourcePath": "name.family",
|
||||
"metric": "STRING",
|
||||
"exact": true
|
||||
},
|
||||
{
|
||||
"name": "foo",
|
||||
"resourceType": "Patient",
|
||||
"resourcePath": "name.given",
|
||||
"metric": "STRING"
|
||||
}
|
||||
],
|
||||
"matchResultMap": {
|
||||
"foo": "POSSIBLE_MATCH"
|
||||
}
|
||||
}
|
|
@ -11,6 +11,5 @@
|
|||
"matchResultMap" : {
|
||||
"given-name" : "POSSIBLE_MATCH",
|
||||
"foo" : "MATCH"
|
||||
},
|
||||
"eidSystem": "http://company.io/fhir/NamingSystem/custom-eid-system"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"candidateSearchParams" : [],
|
||||
"candidateFilterSearchParams" : [],
|
||||
"matchFields" : [ {
|
||||
"name" : "given-name",
|
||||
"resourceType" : "*",
|
||||
"resourcePath" : "name.given",
|
||||
"metric" : "COSINE"
|
||||
}],
|
||||
"matchResultMap" : {
|
||||
"given-name" : "POSSIBLE_MATCH"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"candidateSearchParams" : [],
|
||||
"candidateFilterSearchParams" : [],
|
||||
"matchFields" : [ {
|
||||
"name" : "given-name",
|
||||
"resourceType" : "*",
|
||||
"resourcePath" : "name.given",
|
||||
"metric" : "STRING",
|
||||
"matchThreshold" : 0.8
|
||||
}],
|
||||
"matchResultMap" : {
|
||||
"given-name" : "POSSIBLE_MATCH"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package ca.uhn.fhir.rest.server.util;
|
||||
|
||||
import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||
|
||||
public interface ISearchParamRetriever {
|
||||
/**
|
||||
* @return Returns {@literal null} if no match
|
||||
*/
|
||||
RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName);
|
||||
}
|