Merge branch 'master' into spring-batch-integration

This commit is contained in:
Tadgh 2020-06-15 09:10:41 -07:00
commit 798fdb9cc2
86 changed files with 2075 additions and 646 deletions

6
\
View File

@ -1,6 +0,0 @@
---
type: fix
issue: 1856
title: The subscription delivery queue in the JPA server was erroniously keeping both a copy of the serialized and the
deserialized payload in memory for each entry in the queue, doubling the memory requirements. This also caused failures
when delivering XML payloads in some configurations. This has been corrected.

View File

@ -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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 55 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 43 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 60 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 56 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 66 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 76 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -213,9 +213,15 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc {
myCodeSystemDao.flush();
});
List<TermCodeSystemVersion> codeSystemVersions = myCodeSystemVersionDao.findByCodeSystemPid(theCodeSystem.getPid());
for (TermCodeSystemVersion next : codeSystemVersions) {
deleteCodeSystemVersion(next.getPid());
List<Long> codeSystemVersionPids = txTemplate.execute(t -> {
List<TermCodeSystemVersion> codeSystemVersions = myCodeSystemVersionDao.findByCodeSystemPid(theCodeSystem.getPid());
return codeSystemVersions
.stream()
.map(v -> v.getPid())
.collect(Collectors.toList());
});
for (Long next : codeSystemVersionPids) {
deleteCodeSystemVersion(next);
}
txTemplate.executeWithoutResult(t -> {

View File

@ -64,7 +64,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
@Autowired
protected PlatformTransactionManager myTransactionMgr;
private boolean myProcessDeferred = true;
private List<TermCodeSystem> myCodeSystemsToDelete = Collections.synchronizedList(new ArrayList<>());
private List<TermCodeSystem> myDefferedCodeSystemsDeletions = Collections.synchronizedList(new ArrayList<>());
private List<TermConcept> myDeferredConcepts = Collections.synchronizedList(new ArrayList<>());
private List<ValueSet> myDeferredValueSets = Collections.synchronizedList(new ArrayList<>());
private List<ConceptMap> myDeferredConceptMaps = Collections.synchronizedList(new ArrayList<>());
@ -109,7 +109,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
public void deleteCodeSystem(TermCodeSystem theCodeSystem) {
theCodeSystem.setCodeSystemUri("urn:uuid:" + UUID.randomUUID().toString());
myCodeSystemDao.save(theCodeSystem);
myCodeSystemsToDelete.add(theCodeSystem);
myDefferedCodeSystemsDeletions.add(theCodeSystem);
}
@Override
@ -155,8 +155,11 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
ourLog.info("Saving {} deferred concept relationships...", count);
while (relCount < count && myConceptLinksToSaveLater.size() > 0) {
TermConceptParentChildLink next = myConceptLinksToSaveLater.remove(0);
assert next.getChild() != null;
assert next.getParent() != null;
if (!myConceptDao.findById(next.getChild().getId()).isPresent() || !myConceptDao.findById(next.getParent().getId()).isPresent()) {
if ((next.getChild().getId() == null || !myConceptDao.findById(next.getChild().getId()).isPresent())
|| (next.getParent().getId() == null || !myConceptDao.findById(next.getParent().getId()).isPresent())) {
ourLog.warn("Not inserting link from child {} to parent {} because it appears to have been deleted", next.getParent().getCode(), next.getChild().getCode());
continue;
}
@ -194,6 +197,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
myDeferredValueSets.clear();
myDeferredConceptMaps.clear();
myDeferredConcepts.clear();
myDefferedCodeSystemsDeletions.clear();
}
@Transactional(propagation = Propagation.NEVER)
@ -243,10 +247,10 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
}
private void processDeferredCodeSystemDeletions() {
for (TermCodeSystem next : myCodeSystemsToDelete) {
for (TermCodeSystem next : myDefferedCodeSystemsDeletions) {
myCodeSystemStorageSvc.deleteCodeSystem(next);
}
myCodeSystemsToDelete.clear();
myDefferedCodeSystemsDeletions.clear();
}
@Override
@ -277,7 +281,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
}
private boolean isDeferredCodeSystemDeletions() {
return !myCodeSystemsToDelete.isEmpty();
return !myDefferedCodeSystemsDeletions.isEmpty();
}
private boolean isDeferredConcepts() {

View File

@ -68,7 +68,10 @@ public class FhirResourceDaoDstu3CodeSystemTest extends BaseJpaDstu3Test {
});
// Delete the code system
myCodeSystemDao.delete(id);
runInTransaction(()->{
myCodeSystemDao.delete(id);
});
myTerminologyDeferredStorageSvc.saveDeferred();
runInTransaction(()->{
assertEquals(0L, myConceptDao.count());
});

View File

@ -114,10 +114,13 @@ public class ResourceProviderDstu3CodeSystemTest extends BaseResourceProviderDst
// Create the code system
CodeSystem cs = (CodeSystem) myFhirCtx.newJsonParser().parseResource(input);
ourClient.update().resource(cs).execute();
runInTransaction(() -> assertEquals(26, myConceptDao.count()));
runInTransaction(() -> assertEquals(26L, myConceptDao.count()));
// Delete the code system
ourClient.delete().resource(cs).execute();
runInTransaction(() -> assertEquals(26L, myConceptDao.count()));
myTerminologyDeferredStorageSvc.saveDeferred();
runInTransaction(() -> assertEquals(24L, myConceptDao.count()));
}

View File

@ -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})

View File

@ -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());
}
}

View File

@ -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;

View File

@ -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());
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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);
}

View File

@ -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"
}

View File

@ -64,7 +64,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">

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.migrate.taskdef;
/*-
* #%L
* HAPI FHIR JPA Server - Migration
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
public enum ColumnTypeEnum {
LONG,

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.jpa.migrate.taskdef;
/*-
* #%L
* HAPI FHIR JPA Server - Migration
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
import java.util.HashMap;

View File

@ -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>

View File

@ -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) {

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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() {

View File

@ -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() {

View File

@ -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;

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -0,0 +1,24 @@
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%
*/
public interface IEmpiFieldMetric {
}

View File

@ -0,0 +1,34 @@
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.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);
}
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.empi.rules.similarity;
package ca.uhn.fhir.empi.rules.metric.matcher;
/*-
* #%L
@ -20,13 +20,11 @@ package ca.uhn.fhir.empi.rules.similarity;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.instance.model.api.IBase;
import org.apache.commons.codec.language.DoubleMetaphone;
public class ReferenceMatchSimilarity implements IEmpiFieldSimilarity {
public class DoubleMetaphoneStringMatcher implements IEmpiStringMatcher {
@Override
public double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase) {
System.out.println("wip!");
return 1;
public boolean matches(String theLeftString, String theRightString) {
return new DoubleMetaphone().isDoubleMetaphoneEqual(theLeftString, theRightString);
}
}

View File

@ -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
}

View File

@ -0,0 +1,41 @@
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;
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());
}
}
}

View File

@ -0,0 +1,60 @@
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 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;
}
}

View File

@ -0,0 +1,60 @@
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 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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,32 @@
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 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 IEmpiFieldMatcher extends IEmpiFieldMetric {
boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact);
}

View File

@ -0,0 +1,25 @@
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%
*/
public interface IEmpiStringMatcher {
boolean matches(String theLeftString, String theRightString);
}

View File

@ -0,0 +1,30 @@
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 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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,46 @@
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 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;
}
}

View File

@ -0,0 +1,28 @@
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%
*/
public class SubstringStringMatcher implements IEmpiStringMatcher {
@Override
public boolean matches(String theLeftString, String theRightString) {
return theLeftString.startsWith(theRightString) || theRightString.startsWith(theLeftString);
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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) {

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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));
}
}

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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) {

View File

@ -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));
}
}

View File

@ -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)));

View File

@ -0,0 +1,9 @@
{
"candidateSearchParams" : [],
"candidateFilterSearchParams" : [{
"resourceType" : "Patient",
"searchParam" : "foo"
}],
"matchFields" : [],
"matchResultMap" : {}
}

View File

@ -0,0 +1,14 @@
{
"candidateSearchParams" : [],
"candidateFilterSearchParams" : [],
"matchFields" : [ {
"name" : "given-name",
"resourceType" : "Patient",
"resourcePath" : "name.first",
"metric" : "STRING",
"exact" : true
}],
"matchResultMap" : {
"given-name" : "POSSIBLE_MATCH"
}
}

View File

@ -0,0 +1,9 @@
{
"candidateSearchParams" : [{
"resourceType" : "Patient",
"searchParam" : "foo"
}],
"candidateFilterSearchParams" : [],
"matchFields" : [],
"matchResultMap" : {}
}

View File

@ -0,0 +1,7 @@
{
"candidateSearchParams" : [],
"candidateFilterSearchParams" : [],
"matchFields" : [],
"matchResultMap" : {},
"eidSystem": "invalid url"
}

View File

@ -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"
}
}

View File

@ -11,6 +11,5 @@
"matchResultMap" : {
"given-name" : "POSSIBLE_MATCH",
"foo" : "MATCH"
},
"eidSystem": "http://company.io/fhir/NamingSystem/custom-eid-system"
}
}

View File

@ -0,0 +1,13 @@
{
"candidateSearchParams" : [],
"candidateFilterSearchParams" : [],
"matchFields" : [ {
"name" : "given-name",
"resourceType" : "*",
"resourcePath" : "name.given",
"metric" : "COSINE"
}],
"matchResultMap" : {
"given-name" : "POSSIBLE_MATCH"
}
}

View File

@ -0,0 +1,14 @@
{
"candidateSearchParams" : [],
"candidateFilterSearchParams" : [],
"matchFields" : [ {
"name" : "given-name",
"resourceType" : "*",
"resourcePath" : "name.given",
"metric" : "STRING",
"matchThreshold" : 0.8
}],
"matchResultMap" : {
"given-name" : "POSSIBLE_MATCH"
}
}

View File

@ -0,0 +1,30 @@
package ca.uhn.fhir.rest.server.util;
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* 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.RuntimeSearchParam;
public interface ISearchParamRetriever {
/**
* @return Returns {@literal null} if no match
*/
RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName);
}