empi bugfixes and enhancements (#2104)

This commit is contained in:
Ken Stevens 2020-09-29 17:27:43 -04:00 committed by GitHub
parent ecc09889cb
commit 09d09233ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 1941 additions and 920 deletions

View File

@ -0,0 +1,9 @@
\---
type: fix
issue: 2104
title: "EMPI bug fixes. Special characters in search param values (e.g. space)
led to missed matches; these are now properly escaped so matches are accurately found.
Candidates were failing to match when candidateSearchParams was empty; matches now
work properly with empty candidateSearchParams. matchResultMap entries were
incorrectly resolving if any named matchFields matched; they now correctly
resolve only when ALL matchFields match."

View File

@ -0,0 +1,9 @@
---
type: add
issue: 2104
title: "EMPI enhancements. Added new IDENTIFIER matcher rule type that defines
a matcher where two resources share the same value for a particular identifier system,
or if no system is indicated, defines a matcher where two resources share any matching system,values
identifier. Improved channel and batch troubleshooting logging. Added new
controller layer to support non-fhir apis. Changed rule json format; replaced 'metric'
with either matcher or simiarity that now have their own distinct subkeys."

View File

@ -2,11 +2,9 @@
## Introduction
HAPI FHIR 5.1.0 introduces preliminary support for **EMPI**.
An Enterprise Master Patient Index (EMPI) allows for links to be created and maintained between different Patient and/or Practitioner resources. These links are used to indicate the fact that different Patient/Practitioner resources are known or believed to refer to the same actual (real world) person.
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 and manual linking.
These links are created and updated using different combinations of automatic linking and manual linking.
Note: This documentation describes EMPI for Patient resources. The same information applies for Practitioner resources. You can substitute "Practitioner" for "Patient" anywhere it appears in this documentation.

View File

@ -15,25 +15,25 @@ There are several resources that are used:
# 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).
With EMPI enabled, the default behavior of the EMPI is to create a new Person record for every Patient that is created such that there is a 1:1 relationship between them. Any relinking is then expected to be done manually via the [EMPI Operations](/hapi-fhir/docs/server_jpa_empi/empi_operations.html).
In a typical configuration it is often desirable to have links be created automatically using matching rules. For example, you might decide that if a Patient shares the same name, gender, and date of birth as another Patient, you have at least a little confidence that they are the same Person.
This automatic linking is done via configurable matching rules that create links between Patients and Persons. Based on the strength of the match configured in these rules, the link will be set to either POSSIBLE_MATCH or MATCH.
It is important to note that before a resource is to be processed by EMPI, it is first checked to ensure that it has at least one attribute that the EMPI system cares about, as defined in the `empi-rules.json` file. If the incoming resource has no attributes that the EMPI system cares about, EMPI processing does not occur on it. In this case, no Person is created for them. If in the future that Patient is updated to contain attributes the EMPI system does concern itself with, it will be processed at that time.
It is important to note that before a resource is processed by EMPI, it is first checked to ensure that it has at least one attribute that the EMPI system cares about, as defined in the `empi-rules.json` file. If the incoming resource has no such attributes, then EMPI processing does not occur on it. In this case, no Person is created for them. If in the future that Patient is updated to contain attributes the EMPI system does concern itself with, it will be processed at that time.
## Design
Below are some simplifying principles HAPI EMPI enforces to reduce complexity and ensure data integrity.
Below are some simplifying principles HAPI EMPI follows to reduce complexity and ensure data integrity.
1. When EMPI is enabled on a HAPI FHIR server, any Person resource in the repository that has the "hapi-empi" tag is considered read-only 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. When EMPI is enabled on a HAPI FHIR server, any Person resource in the repository that has the "hapi-empi" tag is considered read-only by the FHIR endpoint. These Person resources are managed exclusively by HAPI EMPI. Users can only change them via [EMPI Operations](/hapi-fhir/docs/server_jpa_empi/empi_operations.html). In most cases, users will indirectly change them by creating and updating Patient and Practitioner ("Patient") resources. For the rest of this document, assume "Person" refers to a "hapi-empi" tagged Person resource.
1. Every Patient in the system has a MATCH link to at most one Person resource.
1. Every Patient resource in the system that has been processed by EMPI 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 only Patient resources in the system that do not have a MATCH link are those that have the 'no-empi' tag or those that have POSSIBLE_MATCH links pending review.
1. The HAPI EMPI rules define a single identifier system that holds the external enterprise id ("EID"). If a Patient has an external EID, then the Person it links to always has the same EID. If a patient has no EID when it arrives, the person created from this patient is given an internal EID.
1. The HAPI EMPI rules define a single identifier system that holds the external enterprise id ("EID"). If a Patient has an external EID, then the Person it links to always has the same EID. If a patient has no EID when it arrives, a unique UUID will be assigned as that Person's EID.
1. A Person can have both an internal EID(auto-created by HAPI), and an external EID (provided by an external system).
@ -61,15 +61,15 @@ Below are some simplifying principles HAPI EMPI enforces to reduce complexity an
### 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:
When a new Patient resource is compared with all other resources of that type in the repository, there are four possible outcomes:
* CASE 1: No MATCH and no POSSIBLE_MATCH outcomes -> a new Person resource is created and linked to that Patient as MATCH. All fields are copied from the Patient to the Person. If the incoming resource has an EID, it is copied to the Person. Otherwise a new UUID is created and used as the internal EID.
* CASE 1: No MATCH and no POSSIBLE_MATCH outcomes -> a new Person resource is created and linked to that Patient as MATCH. All fields are copied from the Patient to the Person. If the incoming resource has an EID, it is copied to the Person. Otherwise a new UUID is generated and used as the internal EID.
* CASE 2: All of the MATCH Patient resources are already linked to the same Person -> a new Link is created between the new Patient and that Person and is set to MATCH.
* CASE 3: The MATCH Patient resources link to more than one Person -> Mark all links as POSSIBLE_MATCH. All other Person resources are marked as POSSIBLE_DUPLICATE of this first Person. These duplicates are manually reviewed later and either merged or marked as NO_MATCH and the system will no longer consider them as a POSSIBLE_DUPLICATE going forward. POSSIBLE_DUPLICATE is the only link type that can have a Person as both the source and target of the link.
* CASE 4: Only POSSIBLE_MATCH outcomes -> In this case, new POSSIBLE_MATCH links are created and await manual assignment to either NO_MATCH or MATCH.
* CASE 4: Only POSSIBLE_MATCH outcomes -> In this case, new POSSIBLE_MATCH links are created and await manual reassignment to either NO_MATCH or MATCH.
# HAPI EMPI Technical Details
@ -77,5 +77,5 @@ When EMPI is enabled, the HAPI FHIR JPA Server does the following things on star
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. The [EMPI Operations](/hapi-fhir/docs/server_jpa_empi/empi_operations.html) are registered with the server.
1. It registers a new dao interceptor that restricts access to EMPI managed Person records.

View File

@ -1,8 +1,8 @@
# EMPI Operations
Several operations exist that can be used to manage EMPI links. These operations are supplied by a [plain provider](/docs/server_plain/resource_providers.html#plain-providers) called [EmpiProvider](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/provider/EmpiProviderR4.html).
EMPI links are managed by EMPI Operations. These operations are supplied by a [plain provider](/docs/server_plain/resource_providers.html#plain-providers) called [EmpiProvider](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/provider/EmpiProviderR4.html).
In cases where the operation changes data, if a resource id parameter contains a version (e.g. `Person/123/_history/1`), then the operation will fail with a 409 CONFLICT if that is not the latest version of that resource. This could be used to prevent update conflicts in an environment where multiple users are working on the same set of empi links.
In cases where the operation changes data, if a resource id parameter contains a version (e.g. `Person/123/_history/1`), then the operation will fail with a 409 CONFLICT if that is not the latest version of that resource. This feature can be used to prevent update conflicts in an environment where multiple users are working on the same set of empi links.
## Query links
@ -55,10 +55,10 @@ Ue the `$empi-query-links` operation to view empi links. The results returned a
### Example
Use an HTTP GET like `http://example.com/$empi-query-link?matchResult=POSSIBLE_MATCH` or an HTTP POST to the following URL to invoke this operation:
Use an HTTP GET like `http://example.com/$empi-query-links?matchResult=POSSIBLE_MATCH` or an HTTP POST to the following URL to invoke this operation:
```url
http://example.com/$empi-query-link
http://example.com/$empi-query-links
```
The following request body could be used to find all POSSIBLE_MATCH links in the system:
@ -188,7 +188,7 @@ This operation returns `Parameters` similar to `$empi-query-links`:
## Unduplicate Persons
Use the `$empi-not-duplicate` operation to mark duplicate persons as not duplicates. This operation takes the following parameters:
<table class="table table-striped table-condensed">
<thead>
<tr>
@ -450,12 +450,12 @@ This might result in a response such as the following:
## Clearing EMPI Links
The `$empi-clear` operation is used to batch-delete EMPI links and related persons from the database. This operation is meant to
The `$empi-clear` operation is used to batch-delete EMPI links and related persons from the database. This operation is meant to
be used during the rules-tuning phase of the EMPI implementation so that you can quickly test your ruleset.
It permits the user to reset the state of their EMPI system without manual deletion of all related links and Persons.
It permits the user to reset the state of their EMPI system without manual deletion of all related links and Persons.
After the operation is complete, all targeted EMPI links are removed from the system, and their related Person resources are deleted and expunged
from the server.
After the operation is complete, all targeted EMPI links are removed from the system, and their related Person resources are deleted and expunged
from the server.
This operation takes a single optional Parameter.
@ -500,7 +500,7 @@ The following request body could be used:
}
```
This operation returns the number of EMPI links that were cleared. The following is a sample response:
This operation returns the number of EMPI links that were cleared. The following is a sample response:
```json
{
@ -515,10 +515,10 @@ This operation returns the number of EMPI links that were cleared. The following
## Batch-creating EMPI Links
Call the `$empi-submit` operation to submit patients and practitioners for EMPI processing. In the rules-tuning phase of your setup, you can use `$empi-submit` to apply EMPI rules across multiple Resources.
An important thing to note is that this operation only submits the resources for processing. Actual EMPI processing is run asynchronously, and depending on the size
An important thing to note is that this operation only submits the resources for processing. Actual EMPI processing is run asynchronously, and depending on the size
of the affected bundle of resources, may take some time to complete.
After the operation is complete, all resources that matched the criteria will now have at least one EMPI link attached to them.
After the operation is complete, all resources that matched the criteria will now have at least one empi link attached to them.
This operation takes a single optional criteria parameter unless it is called on a specific instance.
@ -537,7 +537,7 @@ This operation takes a single optional criteria parameter unless it is called on
<td>String</td>
<td>0..1</td>
<td>
The search critiera used to filter resources.
The search criteria used to filter resources. An empty criteria will submit all resources.
</td>
</tr>
</tbody>
@ -560,7 +560,7 @@ The following request body could be used:
{
"resourceType": "Parameters",
"parameter": [ {
"criteria": "",
"name": "criteria",
"valueString": "birthDate=2020-07-28"
} ]
}
@ -577,11 +577,10 @@ This operation returns the number of resources that were submitted for EMPI proc
}
```
This operation can also be done at the Instance level. When this is the case, the operations accepts no parameters.
This operation can also be done at the Instance level. When this is the case, the operations accepts no parameters.
The following are examples of Instance level POSTs, which require no parameters.
```url
http://example.com/Patient/123/$empi-submit
http://example.com/Practitioner/456/$empi-submit
```

View File

@ -1,66 +1,109 @@
# Rules
HAPI EMPI rules are managed via a single json document.
HAPI EMPI rules are defined in a single json document.
Note that in all the following configuration, valid options for `resourceType` 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.
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
{
"version": "1",
"candidateSearchParams": [
{
"resourceType": "Patient",
"searchParams": ["given", "family"]
},
{
"resourceType": "*",
"searchParams": ["identifier"]
"searchParams": [
"phone"
]
},
{
"resourceType": "Patient",
"searchParams": ["general-practitioner"]
}
],
"candidateFilterSearchParams": [
{
"resourceType": "*",
"searchParam": "active",
"fixedValue": "true"
}
],
"matchFields": [
{
"name": "cosine-given-name",
"resourceType": "*",
"resourcePath": "name.given",
"metric": "COSINE",
"matchThreshold": 0.8,
"exact": true
"searchParams": [
"birthdate"
]
},
{
"name": "jaro-last-name",
"resourceType": "*",
"searchParams": [
"identifier"
]
}
],
"candidateFilterSearchParams": [],
"matchFields": [
{
"name": "birthday",
"resourceType": "Patient",
"resourcePath": "birthDate",
"matcher": {
"algorithm": "STRING"
}
},
{
"name": "phone",
"resourceType": "Patient",
"resourcePath": "telecom.value",
"matcher": {
"algorithm": "STRING"
}
},
{
"name": "firstname-meta",
"resourceType": "Patient",
"resourcePath": "name.given",
"matcher": {
"algorithm": "METAPHONE"
}
},
{
"name": "lastname-meta",
"resourceType": "Patient",
"resourcePath": "name.family",
"metric": "JARO_WINKLER",
"matchThreshold": 0.8
"matcher": {
"algorithm": "METAPHONE"
}
},
{
"name": "firstname-jaro",
"resourceType": "Patient",
"resourcePath": "name.given",
"similarity": {
"algorithm": "JARO_WINKLER",
"matchThreshold": 0.80
}
},
{
"name": "lastname-jaro",
"resourceType": "Patient",
"resourcePath": "name.family",
"similarity": {
"algorithm": "JARO_WINKLER",
"matchThreshold": 0.80
}
}
],
"matchResultMap": {
"cosine-given-name" : "POSSIBLE_MATCH",
"cosine-given-name,jaro-last-name" : "MATCH"
},
"eidSystem": "http://company.io/fhir/NamingSystem/custom-eid-system"
"firstname-meta,lastname-meta,birthday": "MATCH",
"firstname-meta,lastname-meta,phone": "MATCH",
"firstname-jaro,lastname-jaro,birthday": "POSSIBLE_MATCH",
"firstname-jaro,lastname-jaro,phone": "POSSIBLE_MATCH",
"lastname-jaro,phone,birthday": "POSSIBLE_MATCH",
"firstname-jaro,phone,birthday": "POSSIBLE_MATCH"
}
}
```
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).
These define fields which must have at least one exact match before two resources are considered for matching. This is like a list of "pre-searches" that find potential candidates for matches, to avoid the expensive operation of running a match score calculation on all resources in the system. E.g. you may only wish to consider matching two Patients if they either share at least one identifier in common or have the same birthday or the same phone number. The HAPI FHIR server executes each of these searches separately and then takes the union of the results, so you can think of these as `OR` criteria that cast a wide net for potential candidates. In some EMPI systems, these "pre-searches" are called "blocking" searches (since they identify "blocks" of candidates that will be searched for matches).
If a list of searchParams is specified in a given candidateSearchParams item, then these search parameters are treated as `AND` parameters. In the following candidateSearchParams definition, hapi-fhir
will extract given name, family name and identifiers from the incoming Patient and perform two separate
searches, first for all Patient resources that have the same given `AND` the same family name as the incoming Patient, and second for all Patient resources that share at least one identifier as the incoming Patient. Note that if the incoming Patient was missing any of these searchParam values, then that search would be skipped. E.g. if the incoming Patient had a given name but no family name, then only a search for matching identifiers would be performed.
```json
[ {
"candidateSearchParams": [ {
"resourceType" : "Patient",
"searchParams" : ["given", "family"]
}, {
@ -70,7 +113,7 @@ These define fields which must have at least one exact match before two resource
```
### 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.
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",
@ -102,23 +145,25 @@ For example, if the incoming patient looked like this:
then the above `candidateSearchParams` and `candidateFilterSearchParams` would result in the following two consecutive searches for candidates:
* `Patient?given=Peter,James&family=Chalmers&active=true`
* `Patient?identifier=urn:oid:1.2.36.146.595.217.0.1|12345&active=true`
* `Patient?identifier=urn:oid:1.2.36.146.595.217.0.1|12345&active=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.
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 matchFields: `matcher` and `similarity`. `matcher` matchFields return a `true` or `false` directly, whereas `similarity` matchFields 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 only 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.
By default, all matchFields have `exact=false` which means that they will have all diacritical marks removed and all letters will be 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": "*",
"name": "familyname-soundex",
"resourceType": "*",
"resourcePath": "name.family",
"metric": "SOUNDEX"
"matcher": {
"algorithm": "SOUNDEX"
}
}
```
@ -126,32 +171,50 @@ Here is a matcher matchField that only matches when two family names are identic
```json
{
"name": "family-name-exact",
"resourceType": "*",
"name": "familyname-exact",
"resourceType": "*",
"resourcePath": "name.family",
"metric": "STRING",
"exact": true
"matcher": {
"algorithm": "STRING",
"exact": true
}
}
```
Here is a similarity matchField that matches when two given names match with a JARO_WINKLER threshold >0 0.8.
Special identifier matching is also available if you need to match on a particular identifier system:
```json
{
"name": "identifier-ssn",
"resourceType": "*",
"resourcePath": "identifier",
"matcher": {
"algorithm": "IDENTIFIER",
"identifierSystem": "http://hl7.org/fhir/sid/us-ssn"
}
}
```
Here is a similarity matchField that matches when two given names match with a JARO_WINKLER threshold >= 0.8.
```json
{
"name" : "given-name-jaro",
"resourceType" : "Patient",
"resourcePath" : "name.given",
"metric" : "JARO_WINKLER",
"matchThreshold" : 0.8
"name": "firstname-jaro",
"resourceType": "*",
"resourcePath": "name.given",
"similarity": {
"algorithm": "JARO_WINKLER",
"matchThreshold": 0.80
}
}
```
The following metrics are currently supported:
The following algorithms are currently supported:
<table class="table table-striped table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Algorithm</th>
<th>Type</th>
<th>Description</th>
<th>Example</th>
@ -269,6 +332,14 @@ The following metrics are currently supported:
Match names as strings in any order
</td>
<td>John Henry = John HENRY when exact=false, John Henry != Henry John</td>
</tr>
<tr>
<td>IDENTIFIER</td>
<td>matcher</td>
<td>
Matches when the system and value of the identifier are identical.
</td>
<td>If an optional "identifierSystem" is provided, then the identifiers only match when they belong to that system</td>
</tr>
<tr>
<td>JARO_WINKLER</td>
@ -315,13 +386,13 @@ The following metrics are currently supported:
### matchResultMap
These entries convert combinations of successful matchFields into an EMPI Match Result for overall matching of a given pair of resources. MATCH results are evaluated take precedence over POSSIBLE_MATCH results.
These entries convert combinations of successful matchFields into an EMPI Match Result for overall matching of a given pair of resources. MATCH results are evaluated take precedence over POSSIBLE_MATCH results. If the incoming resource matches ALL of the named matchFields listed, then a new match link is created with the assigned matchResult (`MATCH` or `POSSIBLE_MATCH`).
```json
{
"matchResultMap": {
"cosine-given-name" : "POSSIBLE_MATCH",
"cosine-given-name,jaro-last-name" : "MATCH"
"firstname-meta,lastname-meta,birthday": "MATCH",
"firstname-jaro,lastname-jaro,birthday": "POSSIBLE_MATCH",
}
}
```

View File

@ -26,11 +26,9 @@ import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.EncodingEnum;
@ -39,6 +37,8 @@ import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.ValidationModeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.instance.model.api.IBaseMetaType;
@ -47,6 +47,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
@ -254,6 +255,15 @@ public interface IFhirResourceDao<T extends IBaseResource> extends IDao {
RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria);
/**
* Delete a list of resource Pids
* @param theUrl the original URL that triggered the delete
* @param theResourceIds the ids of the resources to be deleted
* @param theDeleteConflicts out parameter of conflicts preventing deletion
* @param theRequest the request that initiated the request
* @return response back to the client
*/
DeleteMethodOutcome deletePidList(String theUrl, Collection<ResourcePersistentId> theResourceIds, DeleteConflictList theDeleteConflicts, RequestDetails theRequest);
// /**
// * Invoke the everything operation

View File

@ -123,7 +123,6 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.defaultString;
@ -505,18 +504,23 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
@Nonnull
private DeleteMethodOutcome doDeleteByUrl(String theUrl, DeleteConflictList deleteConflicts, RequestDetails theRequest) {
StopWatch w = new StopWatch();
Set<ResourcePersistentId> resourceIds = myMatchResourceUrlService.processMatchUrl(theUrl, myResourceType, theRequest);
if (resourceIds.size() > 1) {
if (myDaoConfig.isAllowMultipleDelete() == false) {
if (!myDaoConfig.isAllowMultipleDelete()) {
throw new PreconditionFailedException(getContext().getLocalizer().getMessageSanitized(BaseHapiFhirDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resourceIds.size()));
}
}
return deletePidList(theUrl, resourceIds, deleteConflicts, theRequest);
}
@NotNull
@Override
public DeleteMethodOutcome deletePidList(String theUrl, Collection<ResourcePersistentId> theResourceIds, DeleteConflictList theDeleteConflicts, RequestDetails theTheRequest) {
StopWatch w = new StopWatch();
TransactionDetails transactionDetails = new TransactionDetails();
List<ResourceTable> deletedResources = new ArrayList<>();
for (ResourcePersistentId pid : resourceIds) {
for (ResourcePersistentId pid : theResourceIds) {
ResourceTable entity = myEntityManager.find(ResourceTable.class, pid.getId());
deletedResources.add(entity);
@ -525,23 +529,23 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
// Notify IServerOperationInterceptors about pre-action call
HookParams hooks = new HookParams()
.add(IBaseResource.class, resourceToDelete)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(RequestDetails.class, theTheRequest)
.addIfMatchesType(ServletRequestDetails.class, theTheRequest)
.add(TransactionDetails.class, transactionDetails);
doCallHooks(theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks);
doCallHooks(theTheRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks);
myDeleteConflictService.validateOkToDelete(deleteConflicts, entity, false, theRequest, transactionDetails);
myDeleteConflictService.validateOkToDelete(theDeleteConflicts, entity, false, theTheRequest, transactionDetails);
// Notify interceptors
IdDt idToDelete = entity.getIdDt();
if (theRequest != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theRequest, idToDelete.getResourceType(), idToDelete);
if (theTheRequest != null) {
ActionRequestDetails requestDetails = new ActionRequestDetails(theTheRequest, idToDelete.getResourceType(), idToDelete);
notifyInterceptors(RestOperationTypeEnum.DELETE, requestDetails);
}
// Perform delete
updateEntityForDelete(theRequest, transactionDetails, entity);
updateEntityForDelete(theTheRequest, transactionDetails, entity);
resourceToDelete.setId(entity.getIdDt());
// Notify JPA interceptors
@ -550,10 +554,10 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
public void beforeCommit(boolean readOnly) {
HookParams hookParams = new HookParams()
.add(IBaseResource.class, resourceToDelete)
.add(RequestDetails.class, theRequest)
.addIfMatchesType(ServletRequestDetails.class, theRequest)
.add(RequestDetails.class, theTheRequest)
.addIfMatchesType(ServletRequestDetails.class, theTheRequest)
.add(TransactionDetails.class, transactionDetails);
doCallHooks(theRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
doCallHooks(theTheRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
}
});
}
@ -1282,7 +1286,6 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends B
String uuid = UUID.randomUUID().toString();
SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid);
RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequest, getResourceName());
try (IResultIterator iter = builder.createQuery(theParams, searchRuntimeDetails, theRequest, requestPartitionId)) {
while (iter.hasNext()) {

View File

@ -438,7 +438,7 @@ public class IdHelperService {
if (retVal == null) {
IIdType id = theResource.getIdElement();
try {
retVal = this.resolveResourcePersistentIds(null, id.getResourceType(), id.getIdPart()).getIdAsLong();
retVal = this.resolveResourcePersistentIds(RequestPartitionId.allPartitions(), id.getResourceType(), id.getIdPart()).getIdAsLong();
} catch (ResourceNotFoundException e) {
return null;
}

View File

@ -64,6 +64,11 @@
<version>5.2.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -53,11 +53,11 @@ public class EmpiMessageHandler implements MessageHandler {
@Autowired
private FhirContext myFhirContext;
@Autowired
private EmpiResourceFilteringSvc myEmpiResourceFileringSvc;
private EmpiResourceFilteringSvc myEmpiResourceFilteringSvc;
@Override
public void handleMessage(Message<?> theMessage) throws MessagingException {
ourLog.info("Handling resource modified message: {}", theMessage);
ourLog.trace("Handling resource modified message: {}", theMessage);
if (!(theMessage instanceof ResourceModifiedJsonMessage)) {
ourLog.warn("Unexpected message payload type: {}", theMessage);
@ -66,7 +66,7 @@ public class EmpiMessageHandler implements MessageHandler {
ResourceModifiedMessage msg = ((ResourceModifiedJsonMessage) theMessage).getPayload();
try {
if (myEmpiResourceFileringSvc.shouldBeProcessed(getResourceFromPayload(msg))) {
if (myEmpiResourceFilteringSvc.shouldBeProcessed(getResourceFromPayload(msg))) {
matchEmpiAndUpdateLinks(msg);
}
} catch (Exception e) {
@ -92,7 +92,7 @@ public class EmpiMessageHandler implements MessageHandler {
ourLog.trace("Not processing modified message for {}", theMsg.getOperationType());
}
}catch (Exception e) {
log(empiContext, "Failure during EMPI processing: " + e.getMessage());
log(empiContext, "Failure during EMPI processing: " + e.getMessage(), e);
} finally {
// Interceptor call: EMPI_AFTER_PERSISTED_RESOURCE_CHECKED
@ -111,13 +111,13 @@ public class EmpiMessageHandler implements MessageHandler {
EmpiTransactionContext.OperationType empiOperation;
switch (theMsg.getOperationType()) {
case CREATE:
empiOperation = EmpiTransactionContext.OperationType.CREATE;
empiOperation = EmpiTransactionContext.OperationType.CREATE_RESOURCE;
break;
case UPDATE:
empiOperation = EmpiTransactionContext.OperationType.UPDATE;
empiOperation = EmpiTransactionContext.OperationType.UPDATE_RESOURCE;
break;
case MANUALLY_TRIGGERED:
empiOperation = EmpiTransactionContext.OperationType.BATCH;
empiOperation = EmpiTransactionContext.OperationType.SUBMIT_RESOURCE_TO_EMPI;
break;
case DELETE:
default:
@ -145,8 +145,13 @@ public class EmpiMessageHandler implements MessageHandler {
myEmpiMatchLinkSvc.updateEmpiLinksForEmpiTarget(getResourceFromPayload(theMsg), theEmpiTransactionContext);
}
private void log(EmpiTransactionContext theMessages, String theMessage) {
theMessages.addTransactionLogMessage(theMessage);
private void log(EmpiTransactionContext theEmpiContext, String theMessage) {
theEmpiContext.addTransactionLogMessage(theMessage);
ourLog.debug(theMessage);
}
private void log(EmpiTransactionContext theEmpiContext, String theMessage, Exception theException) {
theEmpiContext.addTransactionLogMessage(theMessage);
ourLog.error(theMessage, theException);
}
}

View File

@ -53,11 +53,12 @@ public class EmpiQueueConsumerLoader {
ChannelConsumerSettings config = new ChannelConsumerSettings();
config.setConcurrentConsumers(myEmpiSettings.getConcurrentConsumers());
myEmpiChannel = myChannelFactory.getOrCreateReceiver(IEmpiSettings.EMPI_CHANNEL_NAME, ResourceModifiedJsonMessage.class, config);
}
if (myEmpiChannel != null) {
myEmpiChannel.subscribe(myEmpiMessageHandler);
ourLog.info("EMPI Matching Consumer subscribed to Matching Channel {} with name {}", myEmpiChannel.getClass().getName(), myEmpiChannel.getName());
if (myEmpiChannel == null) {
ourLog.error("Unable to create receiver for {}", IEmpiSettings.EMPI_CHANNEL_NAME);
} else {
myEmpiChannel.subscribe(myEmpiMessageHandler);
ourLog.info("EMPI Matching Consumer subscribed to Matching Channel {} with name {}", myEmpiChannel.getClass().getName(), myEmpiChannel.getName());
}
}
}
@ -66,6 +67,7 @@ public class EmpiQueueConsumerLoader {
public void stop() {
if (myEmpiChannel != null) {
myEmpiChannel.unsubscribe(myEmpiMessageHandler);
ourLog.info("EMPI Matching Consumer unsubscribed from Matching Channel {} with name {}", myEmpiChannel.getClass().getName(), myEmpiChannel.getName());
}
}

View File

@ -21,14 +21,16 @@ package ca.uhn.fhir.jpa.empi.config;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.IEmpiControllerSvc;
import ca.uhn.fhir.empi.api.IEmpiExpungeSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
import ca.uhn.fhir.empi.api.IEmpiLinkSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.api.IEmpiResetSvc;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.provider.EmpiControllerHelper;
import ca.uhn.fhir.empi.provider.EmpiProviderLoader;
import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator;
import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc;
@ -41,17 +43,18 @@ import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkFactory;
import ca.uhn.fhir.jpa.empi.interceptor.EmpiStorageInterceptor;
import ca.uhn.fhir.jpa.empi.interceptor.IEmpiStorageInterceptor;
import ca.uhn.fhir.jpa.empi.svc.EmpiClearSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiControllerSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiEidUpdateService;
import ca.uhn.fhir.jpa.empi.svc.EmpiLinkQuerySvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiLinkSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiLinkUpdaterSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiMatchFinderSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiMatchLinkSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiResourceFilteringSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiPersonDeletingSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiPersonMergerSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiResetSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiResourceDaoSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiResourceFilteringSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiCandidateSearchCriteriaBuilderSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiCandidateSearchSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.EmpiPersonFindingSvc;
@ -59,6 +62,7 @@ import ca.uhn.fhir.jpa.empi.svc.candidate.FindCandidateByEidSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.FindCandidateByLinkSvc;
import ca.uhn.fhir.jpa.empi.svc.candidate.FindCandidateByScoreSvc;
import ca.uhn.fhir.rest.server.util.ISearchParamRetriever;
import ca.uhn.fhir.validation.IResourceLoader;
import org.slf4j.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -164,8 +168,8 @@ public class EmpiConsumerConfig {
}
@Bean
IEmpiResetSvc empiResetSvc(EmpiLinkDaoSvc theEmpiLinkDaoSvc, EmpiPersonDeletingSvc theEmpiPersonDeletingSvcImpl ) {
return new EmpiResetSvcImpl(theEmpiLinkDaoSvc, theEmpiPersonDeletingSvcImpl);
IEmpiExpungeSvc empiResetSvc(EmpiLinkDaoSvc theEmpiLinkDaoSvc, EmpiPersonDeletingSvc theEmpiPersonDeletingSvcImpl ) {
return new EmpiClearSvcImpl(theEmpiLinkDaoSvc, theEmpiPersonDeletingSvcImpl);
}
@Bean
@ -217,4 +221,10 @@ public class EmpiConsumerConfig {
EmpiResourceFilteringSvc empiResourceFilteringSvc() {
return new EmpiResourceFilteringSvc();
}
@Bean
EmpiControllerHelper empiProviderHelper(FhirContext theFhirContext, IResourceLoader theResourceLoader) { return new EmpiControllerHelper(theFhirContext, theResourceLoader); }
@Bean
IEmpiControllerSvc empiControllerSvc() {return new EmpiControllerSvcImpl(); }
}

View File

@ -21,15 +21,15 @@ package ca.uhn.fhir.jpa.empi.config;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiChannelSubmitterSvc;
import ca.uhn.fhir.empi.api.IEmpiSubmitSvc;
import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator;
import ca.uhn.fhir.jpa.dao.empi.EmpiLinkDeleteSvc;
import ca.uhn.fhir.jpa.empi.interceptor.EmpiSubmitterInterceptorLoader;
import ca.uhn.fhir.jpa.empi.svc.EmpiBatchSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiChannelSubmitterSvcImpl;
import ca.uhn.fhir.jpa.empi.svc.EmpiPersonDeletingSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiSearchParamSvc;
import ca.uhn.fhir.jpa.empi.svc.EmpiSubmitSvcImpl;
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -70,7 +70,7 @@ public class EmpiSubmitterConfig {
}
@Bean
IEmpiBatchSvc empiBatchService() {
return new EmpiBatchSvcImpl();
IEmpiSubmitSvc empiBatchService() {
return new EmpiSubmitSvcImpl();
}
}

View File

@ -238,7 +238,9 @@ public class EmpiLinkDaoSvc {
private List<Long> deleteEmpiLinksAndReturnPersonPids(List<EmpiLink> theLinks) {
List<Long> collect = theLinks.stream().map(EmpiLink::getPersonPid).distinct().collect(Collectors.toList());
ourLog.info("Deleting {} EMPI link records...", theLinks.size());
myEmpiLinkDao.deleteAll(theLinks);
ourLog.info("{} EMPI link records deleted", theLinks.size());
return collect;
}

View File

@ -22,12 +22,14 @@ package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.IEmpiChannelSubmitterSvc;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.jpa.subscription.channel.api.ChannelProducerSettings;
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelFactory;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.MessageChannel;
@ -37,6 +39,8 @@ import static ca.uhn.fhir.empi.api.IEmpiSettings.EMPI_CHANNEL_NAME;
* This class is responsible for manual submissions of {@link IAnyResource} resources onto the Empi Channel.
*/
public class EmpiChannelSubmitterSvcImpl implements IEmpiChannelSubmitterSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
private MessageChannel myEmpiChannelProducer;
private FhirContext myFhirContext;
@ -49,7 +53,10 @@ public class EmpiChannelSubmitterSvcImpl implements IEmpiChannelSubmitterSvc {
ResourceModifiedMessage resourceModifiedMessage = new ResourceModifiedMessage(myFhirContext, theResource, ResourceModifiedMessage.OperationTypeEnum.MANUALLY_TRIGGERED);
resourceModifiedMessage.setOperationType(ResourceModifiedMessage.OperationTypeEnum.MANUALLY_TRIGGERED);
resourceModifiedJsonMessage.setPayload(resourceModifiedMessage);
getEmpiChannelProducer().send(resourceModifiedJsonMessage);
boolean success = getEmpiChannelProducer().send(resourceModifiedJsonMessage);
if (!success) {
ourLog.error("Failed to submit {} to EMPI Channel.", resourceModifiedMessage.getPayloadId());
}
}
@Autowired

View File

@ -20,13 +20,13 @@ package ca.uhn.fhir.jpa.empi.svc;
* #L%
*/
import ca.uhn.fhir.empi.api.IEmpiResetSvc;
import ca.uhn.fhir.empi.api.IEmpiExpungeSvc;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
@ -35,14 +35,14 @@ import java.util.List;
* This class is responsible for clearing out existing EMPI links, as well as deleting all persons related to those EMPI Links.
*
*/
public class EmpiResetSvcImpl implements IEmpiResetSvc {
private static final Logger ourLog = LoggerFactory.getLogger(EmpiResetSvcImpl.class);
public class EmpiClearSvcImpl implements IEmpiExpungeSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
final EmpiLinkDaoSvc myEmpiLinkDaoSvc;
final EmpiPersonDeletingSvc myEmpiPersonDeletingSvcImpl;
@Autowired
public EmpiResetSvcImpl(EmpiLinkDaoSvc theEmpiLinkDaoSvc, EmpiPersonDeletingSvc theEmpiPersonDeletingSvcImpl) {
public EmpiClearSvcImpl(EmpiLinkDaoSvc theEmpiLinkDaoSvc, EmpiPersonDeletingSvc theEmpiPersonDeletingSvcImpl) {
myEmpiLinkDaoSvc = theEmpiLinkDaoSvc;
myEmpiPersonDeletingSvcImpl = theEmpiPersonDeletingSvcImpl;
}
@ -50,9 +50,11 @@ public class EmpiResetSvcImpl implements IEmpiResetSvc {
@Override
public long expungeAllEmpiLinksOfTargetType(String theResourceType) {
throwExceptionIfInvalidTargetType(theResourceType);
ourLog.info("Clearing all EMPI Links for resource type {}...", theResourceType);
List<Long> longs = myEmpiLinkDaoSvc.deleteAllEmpiLinksOfTypeAndReturnPersonPids(theResourceType);
myEmpiPersonDeletingSvcImpl.deleteResourcesAndHandleConflicts(longs);
myEmpiPersonDeletingSvcImpl.deletePersonResourcesAndHandleConflicts(longs);
myEmpiPersonDeletingSvcImpl.expungeHistoricalAndCurrentVersionsOfIds(longs);
ourLog.info("EMPI clear operation complete. Removed {} EMPI links.", longs.size());
return longs.size();
}
@ -64,9 +66,11 @@ public class EmpiResetSvcImpl implements IEmpiResetSvc {
@Override
public long removeAllEmpiLinks() {
ourLog.info("Clearing all EMPI Links...");
List<Long> personPids = myEmpiLinkDaoSvc.deleteAllEmpiLinksAndReturnPersonPids();
myEmpiPersonDeletingSvcImpl.deleteResourcesAndHandleConflicts(personPids);
myEmpiPersonDeletingSvcImpl.deletePersonResourcesAndHandleConflicts(personPids);
myEmpiPersonDeletingSvcImpl.expungeHistoricalAndCurrentVersionsOfIds(personPids);
ourLog.info("EMPI clear operation complete. Removed {} EMPI links.", personPids.size());
return personPids.size();
}
}

View File

@ -0,0 +1,80 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.empi.api.EmpiLinkJson;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiControllerSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.provider.EmpiControllerHelper;
import ca.uhn.fhir.empi.provider.EmpiControllerUtil;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.stream.Stream;
/**
* This class acts as a layer between EmpiProviders and EMPI services to support a REST API that's not a FHIR Operation API.
*/
@Service
public class EmpiControllerSvcImpl implements IEmpiControllerSvc {
@Autowired
EmpiControllerHelper myEmpiControllerHelper;
@Autowired
IEmpiPersonMergerSvc myEmpiPersonMergerSvc;
@Autowired
IEmpiLinkQuerySvc myEmpiLinkQuerySvc;
@Autowired
IEmpiLinkUpdaterSvc myIEmpiLinkUpdaterSvc;
@Override
public IAnyResource mergePersons(String theFromPersonId, String theToPersonId, EmpiTransactionContext theEmpiTransactionContext) {
IAnyResource fromPerson = myEmpiControllerHelper.getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_MERGE_PERSONS_FROM_PERSON_ID, theFromPersonId);
IAnyResource toPerson = myEmpiControllerHelper.getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, theToPersonId);
myEmpiControllerHelper.validateMergeResources(fromPerson, toPerson);
myEmpiControllerHelper.validateSameVersion(fromPerson, theFromPersonId);
myEmpiControllerHelper.validateSameVersion(toPerson, theToPersonId);
return myEmpiPersonMergerSvc.mergePersons(fromPerson, toPerson, theEmpiTransactionContext);
}
@Override
public Stream<EmpiLinkJson> queryLinks(@Nullable String thePersonId, @Nullable String theTargetId, @Nullable String theMatchResult, @Nullable String theLinkSource, EmpiTransactionContext theEmpiContext) {
IIdType personId = EmpiControllerUtil.extractPersonIdDtOrNull(ProviderConstants.EMPI_QUERY_LINKS_PERSON_ID, thePersonId);
IIdType targetId = EmpiControllerUtil.extractTargetIdDtOrNull(ProviderConstants.EMPI_QUERY_LINKS_TARGET_ID, theTargetId);
EmpiMatchResultEnum matchResult = EmpiControllerUtil.extractMatchResultOrNull(theMatchResult);
EmpiLinkSourceEnum linkSource = EmpiControllerUtil.extractLinkSourceOrNull(theLinkSource);
return myEmpiLinkQuerySvc.queryLinks(personId, targetId, matchResult, linkSource, theEmpiContext);
}
@Override
public Stream<EmpiLinkJson> getDuplicatePersons(EmpiTransactionContext theEmpiContext) {
return myEmpiLinkQuerySvc.getDuplicatePersons(theEmpiContext);
}
@Override
public IAnyResource updateLink(String thePersonId, String theTargetId, String theMatchResult, EmpiTransactionContext theEmpiContext) {
EmpiMatchResultEnum matchResult = EmpiControllerUtil.extractMatchResultOrNull(theMatchResult);
IAnyResource person = myEmpiControllerHelper.getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_PERSON_ID, thePersonId);
IAnyResource target = myEmpiControllerHelper.getLatestTargetFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_TARGET_ID, theTargetId);
myEmpiControllerHelper.validateSameVersion(person, thePersonId);
myEmpiControllerHelper.validateSameVersion(target, theTargetId);
return myIEmpiLinkUpdaterSvc.updateLink(person, target, matchResult, theEmpiContext);
}
@Override
public void notDuplicatePerson(String thePersonId, String theTargetPersonId, EmpiTransactionContext theEmpiContext) {
IAnyResource person = myEmpiControllerHelper.getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_PERSON_ID, thePersonId);
IAnyResource target = myEmpiControllerHelper.getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_TARGET_ID, theTargetPersonId);
myIEmpiLinkUpdaterSvc.notDuplicatePerson(person, target, theEmpiContext);
}
}

View File

@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.empi.svc;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiLinkJson;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
@ -28,65 +28,53 @@ import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.util.ParametersUtil;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class EmpiLinkQuerySvcImpl implements IEmpiLinkQuerySvc {
private static final Logger ourLog = LoggerFactory.getLogger(EmpiLinkQuerySvcImpl.class);
@Autowired
FhirContext myFhirContext;
@Autowired
IdHelperService myIdHelperService;
@Autowired
EmpiLinkDaoSvc myEmpiLinkDaoSvc;
@Override
public IBaseParameters queryLinks(IIdType thePersonId, IIdType theTargetId, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiContext) {
public Stream<EmpiLinkJson> queryLinks(IIdType thePersonId, IIdType theTargetId, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiContext) {
Example<EmpiLink> exampleLink = exampleLinkFromParameters(thePersonId, theTargetId, theMatchResult, theLinkSource);
List<EmpiLink> empiLinks = myEmpiLinkDaoSvc.findEmpiLinkByExample(exampleLink).stream()
return myEmpiLinkDaoSvc.findEmpiLinkByExample(exampleLink).stream()
.filter(empiLink -> empiLink.getMatchResult() != EmpiMatchResultEnum.POSSIBLE_DUPLICATE)
.collect(Collectors.toList());
// TODO RC1 KHS page results
return parametersFromEmpiLinks(empiLinks, true);
.map(this::toJson);
}
@Override
public IBaseParameters getPossibleDuplicates(EmpiTransactionContext theEmpiContext) {
public Stream<EmpiLinkJson> getDuplicatePersons(EmpiTransactionContext theEmpiContext) {
Example<EmpiLink> exampleLink = exampleLinkFromParameters(null, null, EmpiMatchResultEnum.POSSIBLE_DUPLICATE, null);
List<EmpiLink> empiLinks = myEmpiLinkDaoSvc.findEmpiLinkByExample(exampleLink);
// TODO RC1 page results
return parametersFromEmpiLinks(empiLinks, false);
return myEmpiLinkDaoSvc.findEmpiLinkByExample(exampleLink).stream().map(this::toJson);
}
private IBaseParameters parametersFromEmpiLinks(List<EmpiLink> theEmpiLinks, boolean includeResultAndSource) {
IBaseParameters retval = ParametersUtil.newInstance(myFhirContext);
for (EmpiLink empiLink : theEmpiLinks) {
IBase resultPart = ParametersUtil.addParameterToParameters(myFhirContext, retval, "link");
String personId = myIdHelperService.resourceIdFromPidOrThrowException(empiLink.getPersonPid()).toVersionless().getValue();
ParametersUtil.addPartString(myFhirContext, resultPart, "personId", personId);
String targetId = myIdHelperService.resourceIdFromPidOrThrowException(empiLink.getTargetPid()).toVersionless().getValue();
ParametersUtil.addPartString(myFhirContext, resultPart, "targetId", targetId);
if (includeResultAndSource) {
ParametersUtil.addPartString(myFhirContext, resultPart, "matchResult", empiLink.getMatchResult().name());
ParametersUtil.addPartString(myFhirContext, resultPart, "linkSource", empiLink.getLinkSource().name());
ParametersUtil.addPartBoolean(myFhirContext, resultPart, "eidMatch", empiLink.getEidMatch());
ParametersUtil.addPartBoolean(myFhirContext, resultPart, "newPerson", empiLink.getNewPerson());
ParametersUtil.addPartDecimal(myFhirContext, resultPart, "score", empiLink.getScore());
}
}
private EmpiLinkJson toJson(EmpiLink theLink) {
EmpiLinkJson retval = new EmpiLinkJson();
String targetId = myIdHelperService.resourceIdFromPidOrThrowException(theLink.getTargetPid()).toVersionless().getValue();
retval.setTargetId(targetId);
String personId = myIdHelperService.resourceIdFromPidOrThrowException(theLink.getPersonPid()).toVersionless().getValue();
retval.setPersonId(personId);
retval.setCreated(theLink.getCreated());
retval.setEidMatch(theLink.getEidMatch());
retval.setLinkSource(theLink.getLinkSource());
retval.setMatchResult(theLink.getMatchResult());
retval.setNewPerson(theLink.getNewPerson());
retval.setScore(theLink.getScore());
retval.setUpdated(theLink.getUpdated());
retval.setVector(theLink.getVector());
retval.setVersion(theLink.getVersion());
return retval;
}

View File

@ -34,10 +34,7 @@ import ca.uhn.fhir.jpa.empi.dao.EmpiLinkDaoSvc;
import ca.uhn.fhir.jpa.entity.EmpiLink;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.util.ParametersUtil;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.r4.model.Parameters;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
@ -118,7 +115,7 @@ public class EmpiLinkUpdaterSvcImpl implements IEmpiLinkUpdaterSvc {
@Transactional
@Override
public IBaseParameters notDuplicatePerson(IAnyResource thePerson, IAnyResource theTarget, EmpiTransactionContext theEmpiContext) {
public void notDuplicatePerson(IAnyResource thePerson, IAnyResource theTarget, EmpiTransactionContext theEmpiContext) {
validateNotDuplicatePersonRequest(thePerson, theTarget);
Long personId = myIdHelperService.getPidOrThrowException(thePerson);
@ -136,10 +133,6 @@ public class EmpiLinkUpdaterSvcImpl implements IEmpiLinkUpdaterSvc {
empiLink.setMatchResult(EmpiMatchResultEnum.NO_MATCH);
empiLink.setLinkSource(EmpiLinkSourceEnum.MANUAL);
myEmpiLinkDaoSvc.save(empiLink);
Parameters retval = (Parameters) ParametersUtil.newInstance(myFhirContext);
retval.addParameter("success", true);
return retval;
}
private void validateNotDuplicatePersonRequest(IAnyResource thePerson, IAnyResource theTarget) {

View File

@ -143,7 +143,7 @@ public class EmpiMatchLinkSvc {
private void handleEmpiWithSingleCandidate(IAnyResource theResource, MatchedPersonCandidate thePersonCandidate, EmpiTransactionContext theEmpiTransactionContext) {
log(theEmpiTransactionContext, "EMPI has narrowed down to one candidate for matching.");
if (theEmpiTransactionContext.getRestOperation().equals(EmpiTransactionContext.OperationType.UPDATE)) {
if (theEmpiTransactionContext.getRestOperation().equals(EmpiTransactionContext.OperationType.UPDATE_RESOURCE)) {
myEidUpdateService.handleEmpiUpdate(theResource, thePersonCandidate, theEmpiTransactionContext);
} else {
handleEmpiCreate(theResource, thePersonCandidate, theEmpiTransactionContext);

View File

@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.empi.svc;
* #L%
*/
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.model.DeleteConflict;
@ -27,8 +28,9 @@ import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
import ca.uhn.fhir.jpa.dao.expunge.ExpungeService;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.IdType;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -36,11 +38,10 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import static org.slf4j.LoggerFactory.getLogger;
@Service
public class EmpiPersonDeletingSvc {
private static final Logger ourLog = getLogger(EmpiPersonDeletingSvc.class);
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
/**
* This is here for the case of possible infinite loops. Technically batch conflict deletion should handle this, but this is an escape hatch.
*/
@ -55,13 +56,17 @@ public class EmpiPersonDeletingSvc {
* Function which will delete all resources by their PIDs, and also delete any resources that were undeletable due to
* VersionConflictException
*
* @param theLongs
* @param theResourcePids
*/
@Transactional
public void deleteResourcesAndHandleConflicts(List<Long> theLongs) {
public void deletePersonResourcesAndHandleConflicts(List<Long> theResourcePids) {
List<ResourcePersistentId> resourceIds = ResourcePersistentId.fromLongList(theResourcePids);
ourLog.info("Deleting {} Person resources...", resourceIds.size());
DeleteConflictList
deleteConflictList = new DeleteConflictList();
theLongs.stream().forEach(pid -> deleteCascade(pid, deleteConflictList));
IFhirResourceDao<?> resourceDao = myDaoRegistry.getResourceDao("Person");
resourceDao.deletePidList(ProviderConstants.EMPI_CLEAR, resourceIds, deleteConflictList, null);
IFhirResourceDao personDao = myDaoRegistry.getResourceDao("Person");
int batchCount = 0;
@ -72,23 +77,20 @@ public class EmpiPersonDeletingSvc {
throw new IllegalStateException("Person deletion seems to have entered an infinite loop. Aborting");
}
}
ourLog.info("Deleted {} Person resources in {} batches", resourceIds.size(), batchCount);
}
/**
* Use the expunge service to expunge all historical and current versions of the resources associated to the PIDs.
*/
public void expungeHistoricalAndCurrentVersionsOfIds(List<Long> theLongs) {
ourLog.info("Expunging historical versions of {} Person resources...", theLongs.size());
ExpungeOptions options = new ExpungeOptions();
options.setExpungeDeletedResources(true);
options.setExpungeOldVersions(true);
theLongs
.forEach(personId -> myExpungeService.expunge("Person", personId, null, options, null));
}
private void deleteCascade(Long pid, DeleteConflictList theDeleteConflictList) {
ourLog.debug("About to cascade delete: {}", pid);
IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao("Person");
resourceDao.delete(new IdType("Person/" + pid), theDeleteConflictList, null, null);
ourLog.info("Expunged historical versions of {} Person resources", theLongs.size());
}
private void deleteConflictBatch(DeleteConflictList theDcl, IFhirResourceDao<IBaseResource> theDao) {

View File

@ -36,7 +36,7 @@ public class EmpiResourceFilteringSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private IEmpiSettings empiSettings;
private IEmpiSettings myEmpiSettings;
@Autowired
EmpiSearchParamSvc myEmpiSearchParamSvc;
@Autowired
@ -56,7 +56,11 @@ public class EmpiResourceFilteringSvc {
*/
public boolean shouldBeProcessed(IAnyResource theResource) {
String resourceType = myFhirContext.getResourceType(theResource);
List<EmpiResourceSearchParamJson> candidateSearchParams = empiSettings.getEmpiRules().getCandidateSearchParams();
List<EmpiResourceSearchParamJson> candidateSearchParams = myEmpiSettings.getEmpiRules().getCandidateSearchParams();
if (candidateSearchParams.isEmpty()) {
return true;
}
boolean containsValueForSomeSearchParam = candidateSearchParams.stream()
.filter(csp -> myEmpiSearchParamSvc.searchParamTypeIsValidForResourceType(csp.getResourceType(), resourceType))
@ -64,7 +68,7 @@ public class EmpiResourceFilteringSvc {
.map(searchParam -> myEmpiSearchParamSvc.getValueFromResourceForSearchParam(theResource, searchParam))
.anyMatch(valueList -> !valueList.isEmpty());
ourLog.debug("Is {} suitable for EMPI processing? : {}", theResource.getId(), containsValueForSomeSearchParam);
ourLog.trace("Is {} suitable for EMPI processing? : {}", theResource.getId(), containsValueForSomeSearchParam);
return containsValueForSomeSearchParam;
}
}

View File

@ -37,6 +37,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Nullable;
import java.util.List;
@Service
@ -79,7 +80,7 @@ public class EmpiSearchParamSvc implements ISearchParamRetriever {
*
* @return the generated SearchParameterMap, or an empty one if there is no criteria.
*/
public SearchParameterMap getSearchParameterMapFromCriteria(String theTargetType, String theCriteria) {
public SearchParameterMap getSearchParameterMapFromCriteria(String theTargetType, @Nullable String theCriteria) {
SearchParameterMap spMap;
if (StringUtils.isBlank(theCriteria)) {
spMap = new SearchParameterMap();

View File

@ -20,8 +20,9 @@ package ca.uhn.fhir.jpa.empi.svc;
* #L%
*/
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiChannelSubmitterSvc;
import ca.uhn.fhir.empi.api.IEmpiSubmitSvc;
import ca.uhn.fhir.empi.log.Logs;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
@ -36,9 +37,11 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@ -46,7 +49,8 @@ import java.util.Collections;
import java.util.List;
import java.util.UUID;
public class EmpiBatchSvcImpl implements IEmpiBatchSvc {
public class EmpiSubmitSvcImpl implements IEmpiSubmitSvc {
private static final Logger ourLog = Logs.getEmpiTroubleshootingLog();
@Autowired
private DaoRegistry myDaoRegistry;
@ -61,16 +65,21 @@ public class EmpiBatchSvcImpl implements IEmpiBatchSvc {
@Override
@Transactional
public long runEmpiOnAllTargetTypes(String theCriteria) {
public long submitAllTargetTypesToEmpi(@Nullable String theCriteria) {
long submittedCount = 0;
submittedCount += runEmpiOnPatientType(theCriteria);
submittedCount += runEmpiOnPractitionerType(theCriteria);
submittedCount += submitPatientTypeToEmpi(theCriteria);
submittedCount += submitPractitionerTypeToEmpi(theCriteria);
return submittedCount;
}
@Override
@Transactional
public long runEmpiOnTargetType(String theTargetType, String theCriteria) {
public long submitTargetTypeToEmpi(String theTargetType, @Nullable String theCriteria) {
if (theCriteria == null) {
ourLog.info("Submitting all resources of type {} to EMPI", theTargetType);
} else {
ourLog.info("Submitting resources of type {} with criteria {} to EMPI", theTargetType, theCriteria);
}
resolveTargetTypeOrThrowException(theTargetType);
SearchParameterMap spMap = myEmpiSearchParamSvc.getSearchParameterMapFromCriteria(theTargetType, theCriteria);
spMap.setLoadSynchronousUpTo(BUFFER_SIZE);
@ -88,8 +97,9 @@ public class EmpiBatchSvcImpl implements IEmpiBatchSvc {
total += loadPidsAndSubmitToEmpiChannel(theSearchBuilder, pidBatch);
} while (query.hasNext());
} catch (IOException theE) {
throw new InternalErrorException("Failure while attempting to query resources for " + ProviderConstants.OPERATION_EMPI_BATCH_RUN, theE);
throw new InternalErrorException("Failure while attempting to query resources for " + ProviderConstants.OPERATION_EMPI_SUBMIT, theE);
}
ourLog.info("EMPI Submit complete. Submitted a total of {} resources.", total);
return total;
}
@ -105,6 +115,7 @@ public class EmpiBatchSvcImpl implements IEmpiBatchSvc {
private long loadPidsAndSubmitToEmpiChannel(ISearchBuilder theSearchBuilder, Collection<ResourcePersistentId> thePidsToSubmit) {
List<IBaseResource> resourcesToSubmit = new ArrayList<>();
theSearchBuilder.loadResourcesByPid(thePidsToSubmit, Collections.emptyList(), resourcesToSubmit, false, null);
ourLog.info("Submitting {} resources to EMPI", resourcesToSubmit.size());
resourcesToSubmit
.forEach(resource -> myEmpiChannelSubmitterSvc.submitResourceToEmpiChannel(resource));
return resourcesToSubmit.size();
@ -112,19 +123,19 @@ public class EmpiBatchSvcImpl implements IEmpiBatchSvc {
@Override
@Transactional
public long runEmpiOnPractitionerType(String theCriteria) {
return runEmpiOnTargetType("Practitioner", theCriteria);
public long submitPractitionerTypeToEmpi(@Nullable String theCriteria) {
return submitTargetTypeToEmpi("Practitioner", theCriteria);
}
@Override
@Transactional
public long runEmpiOnPatientType(String theCriteria) {
return runEmpiOnTargetType("Patient", theCriteria);
public long submitPatientTypeToEmpi(@Nullable String theCriteria) {
return submitTargetTypeToEmpi("Patient", theCriteria);
}
@Override
@Transactional
public long runEmpiOnTarget(IIdType theId) {
public long submitTargetToEmpi(IIdType theId) {
resolveTargetTypeOrThrowException(theId.getResourceType());
IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(theId.getResourceType());
IBaseResource read = resourceDao.read(theId);
@ -134,7 +145,7 @@ public class EmpiBatchSvcImpl implements IEmpiBatchSvc {
private void resolveTargetTypeOrThrowException(String theResourceType) {
if (!EmpiUtil.supportedTargetType(theResourceType)) {
throw new InvalidRequestException(ProviderConstants.OPERATION_EMPI_BATCH_RUN + " does not support resource type: " + theResourceType);
throw new InvalidRequestException(ProviderConstants.OPERATION_EMPI_SUBMIT + " does not support resource type: " + theResourceType);
}
}
}

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.jpa.empi.svc.candidate;
import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson;
import ca.uhn.fhir.jpa.empi.svc.EmpiSearchParamSvc;
import ca.uhn.fhir.util.UrlUtil;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -31,6 +32,7 @@ import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Service
public class EmpiCandidateSearchCriteriaBuilderSvc {
@ -67,6 +69,9 @@ public class EmpiCandidateSearchCriteriaBuilderSvc {
}
private String buildResourceMatchQuery(String theSearchParamName, List<String> theResourceValues) {
return theSearchParamName + "=" + String.join(",", theResourceValues);
String nameValueOrList = theResourceValues.stream()
.map(UrlUtil::escapeUrlParam)
.collect(Collectors.joining(","));
return theSearchParamName + "=" + nameValueOrList;
}
}

View File

@ -50,9 +50,9 @@ public class EmpiPersonFindingSvc {
* 0. First, check the incoming Resource for an EID. If it is present, and we can find a Person with this EID, it automatically matches.
* 1. First, check link table for any entries where this baseresource is the target of a person. If found, return.
* 2. If none are found, attempt to find Person Resources which link to this theResource.
* 3. If none are found, attempt to find Persons similar to our incoming resource based on the EMPI rules and similarity metrics.
* 3. If none are found, attempt to find Person Resources similar to our incoming resource based on the EMPI rules and field matchers.
* 4. If none are found, attempt to find Persons that are linked to Patients/Practitioners that are similar to our incoming resource based on the EMPI rules and
* similarity metrics.
* field matchers.
*
* @param theResource the {@link IBaseResource} we are attempting to find matching candidate Persons for.
* @return A list of {@link MatchedPersonCandidate} indicating all potential Person matches.

View File

@ -4,7 +4,6 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc;
@ -104,8 +103,6 @@ abstract public class BaseEmpiR4Test extends BaseJpaR4Test {
EmpiSearchParameterLoader myEmpiSearchParameterLoader;
@Autowired
SearchParamRegistryImpl mySearchParamRegistry;
@Autowired
private IEmpiBatchSvc myEmpiBatchService;
protected ServletRequestDetails myRequestDetails = new ServletRequestDetails(null);
@ -299,14 +296,14 @@ abstract public class BaseEmpiR4Test extends BaseJpaR4Test {
protected EmpiTransactionContext createContextForCreate() {
EmpiTransactionContext ctx = new EmpiTransactionContext();
ctx.setRestOperation(EmpiTransactionContext.OperationType.CREATE);
ctx.setRestOperation(EmpiTransactionContext.OperationType.CREATE_RESOURCE);
ctx.setTransactionLogMessages(null);
return ctx;
}
protected EmpiTransactionContext createContextForUpdate() {
EmpiTransactionContext ctx = new EmpiTransactionContext();
ctx.setRestOperation(EmpiTransactionContext.OperationType.UPDATE);
ctx.setRestOperation(EmpiTransactionContext.OperationType.UPDATE_RESOURCE);
ctx.setTransactionLogMessages(null);
return ctx;
}

View File

@ -1,16 +1,12 @@
package ca.uhn.fhir.jpa.empi.provider;
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiResetSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.api.IEmpiControllerSvc;
import ca.uhn.fhir.empi.api.IEmpiExpungeSvc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.api.IEmpiSubmitSvc;
import ca.uhn.fhir.empi.provider.EmpiProviderR4;
import ca.uhn.fhir.empi.rules.config.EmpiSettings;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
import ca.uhn.fhir.validation.IResourceLoader;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.AfterEach;
@ -26,19 +22,13 @@ public abstract class BaseProviderR4Test extends BaseEmpiR4Test {
@Autowired
private IEmpiMatchFinderSvc myEmpiMatchFinderSvc;
@Autowired
private IEmpiPersonMergerSvc myPersonMergerSvc;
private IEmpiControllerSvc myEmpiControllerSvc;
@Autowired
private IEmpiLinkUpdaterSvc myEmpiLinkUpdaterSvc;
private IEmpiExpungeSvc myEmpiResetSvc;
@Autowired
private IEmpiLinkQuerySvc myEmpiLinkQuerySvc;
private IEmpiSubmitSvc myEmpiBatchSvc;
@Autowired
private IResourceLoader myResourceLoader;
@Autowired
private IEmpiSettings myEmpiSettings;
@Autowired
private IEmpiResetSvc myEmpiExpungeSvc;
@Autowired
private IEmpiBatchSvc myEmpiBatchSvc;
private EmpiSettings myEmpiSettings;
private String defaultScript;
@ -46,18 +36,17 @@ public abstract class BaseProviderR4Test extends BaseEmpiR4Test {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
Resource resource = resourceLoader.getResource(theString);
String json = IOUtils.toString(resource.getInputStream(), Charsets.UTF_8);
((EmpiSettings)myEmpiSettings).getScriptText();
((EmpiSettings)myEmpiSettings).setScriptText(json);
myEmpiSettings.setScriptText(json);
}
@BeforeEach
public void before() {
myEmpiProviderR4 = new EmpiProviderR4(myFhirContext, myEmpiMatchFinderSvc, myPersonMergerSvc, myEmpiLinkUpdaterSvc, myEmpiLinkQuerySvc, myResourceLoader, myEmpiExpungeSvc, myEmpiBatchSvc);
defaultScript = ((EmpiSettings)myEmpiSettings).getScriptText();
myEmpiProviderR4 = new EmpiProviderR4(myFhirContext, myEmpiControllerSvc, myEmpiMatchFinderSvc, myEmpiResetSvc, myEmpiBatchSvc);
defaultScript = myEmpiSettings.getScriptText();
}
@AfterEach
public void after() throws IOException {
super.after();
((EmpiSettings)myEmpiSettings).setScriptText(defaultScript);
myEmpiSettings.setScriptText(defaultScript);
}
}

View File

@ -13,6 +13,7 @@ import org.junit.jupiter.api.Test;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -73,7 +74,7 @@ public class EmpiProviderMergePersonsR4Test extends BaseProviderR4Test {
myEmpiProviderR4.mergePersons(patientId, otherPatientId, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("fromPersonId must have form Person/<id> where <id> is the id of the person", e.getMessage());
assertThat(e.getMessage(), endsWith("must have form Person/<id> where <id> is the id of the person"));
}
}
@ -106,13 +107,13 @@ public class EmpiProviderMergePersonsR4Test extends BaseProviderR4Test {
myEmpiProviderR4.mergePersons(new StringType("Patient/1"), new StringType("Patient/2"), myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("fromPersonId must have form Person/<id> where <id> is the id of the person", e.getMessage());
assertThat(e.getMessage(), endsWith(" must have form Person/<id> where <id> is the id of the person"));
}
try {
myEmpiProviderR4.mergePersons(myFromPersonId, new StringType("Patient/2"), myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("toPersonId must have form Person/<id> where <id> is the id of the person", e.getMessage());
assertThat(e.getMessage(), endsWith(" must have form Person/<id> where <id> is the id of the person"));
}
try {
myEmpiProviderR4.mergePersons(new StringType("Person/1"), new StringType("Person/1"), myRequestDetails);

View File

@ -102,7 +102,7 @@ public class EmpiProviderUpdateLinkR4Test extends BaseLinkR4Test {
myEmpiProviderR4.updateLink(myPatientId, myPatientId, NO_MATCH_RESULT, myRequestDetails);
fail();
} catch (InvalidRequestException e) {
assertEquals("personId must have form Person/<id> where <id> is the id of the person", e.getMessage());
assertThat(e.getMessage(), endsWith(" must have form Person/<id> where <id> is the id of the person"));
}
}

View File

@ -1,6 +1,6 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiSubmitSvc;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.empi.BaseEmpiR4Test;
@ -17,7 +17,7 @@ import java.util.Date;
class EmpiBatchSvcImplTest extends BaseEmpiR4Test {
@Autowired
IEmpiBatchSvc myEmpiBatchSvc;
IEmpiSubmitSvc myEmpiSubmitSvc;
@Autowired
IInterceptorService myInterceptorService;
@ -49,7 +49,7 @@ class EmpiBatchSvcImplTest extends BaseEmpiR4Test {
assertLinkCount(0);
//SUT
afterEmpiLatch.runWithExpectedCount(20, () -> myEmpiBatchSvc.runEmpiOnAllTargetTypes(null));
afterEmpiLatch.runWithExpectedCount(20, () -> myEmpiSubmitSvc.submitAllTargetTypesToEmpi(null));
assertLinkCount(20);
}
@ -64,7 +64,7 @@ class EmpiBatchSvcImplTest extends BaseEmpiR4Test {
assertLinkCount(0);
//SUT
afterEmpiLatch.runWithExpectedCount(10, () -> myEmpiBatchSvc.runEmpiOnTargetType("Patient", null));
afterEmpiLatch.runWithExpectedCount(10, () -> myEmpiSubmitSvc.submitTargetTypeToEmpi("Patient", null));
assertLinkCount(10);
}
@ -79,7 +79,7 @@ class EmpiBatchSvcImplTest extends BaseEmpiR4Test {
assertLinkCount(0);
//SUT
afterEmpiLatch.runWithExpectedCount(10, () -> myEmpiBatchSvc.runEmpiOnAllTargetTypes(null));
afterEmpiLatch.runWithExpectedCount(10, () -> myEmpiSubmitSvc.submitAllTargetTypesToEmpi(null));
assertLinkCount(10);
}
@ -92,7 +92,7 @@ class EmpiBatchSvcImplTest extends BaseEmpiR4Test {
assertLinkCount(0);
//SUT
afterEmpiLatch.runWithExpectedCount(1, () -> myEmpiBatchSvc.runEmpiOnAllTargetTypes("Patient?name=gary"));
afterEmpiLatch.runWithExpectedCount(1, () -> myEmpiSubmitSvc.submitAllTargetTypesToEmpi("Patient?name=gary"));
assertLinkCount(1);
}

View File

@ -67,7 +67,18 @@ public class EmpiCandidateSearchCriteriaBuilderSvcTest extends BaseEmpiR4Test {
searchParamJson.addSearchParam("identifier");
Optional<String> result = myEmpiCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson);
assertTrue(result.isPresent());
assertEquals(result.get(), "Patient?identifier=urn:oid:1.2.36.146.595.217.0.1|12345");
assertEquals(result.get(), "Patient?identifier=urn%3Aoid%3A1.2.36.146.595.217.0.1%7C12345");
}
@Test
public void testIdentifierSpaceIsEscaped() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("urn:oid:1.2.36.146.595.217.0.1").setValue("abc def");
EmpiResourceSearchParamJson searchParamJson = new EmpiResourceSearchParamJson();
searchParamJson.addSearchParam("identifier");
Optional<String> result = myEmpiCandidateSearchCriteriaBuilderSvc.buildResourceQueryString("Patient", patient, Collections.emptyList(), searchParamJson);
assertTrue(result.isPresent());
assertEquals("Patient?identifier=urn%3Aoid%3A1.2.36.146.595.217.0.1%7Cabc%20def", result.get());
}
@Test

View File

@ -144,10 +144,10 @@ public class EmpiPersonMergerSvcTest extends BaseEmpiR4Test {
public void fromLinkToNoLink() {
createEmpiLink(myFromPerson, myTargetPatient1);
mergePersons();
List<EmpiLink> links = myEmpiLinkDaoSvc.findEmpiLinksByPerson(myToPerson);
Person mergedPerson = mergePersons();
List<EmpiLink> links = myEmpiLinkDaoSvc.findEmpiLinksByPerson(mergedPerson);
assertEquals(1, links.size());
assertThat(myToPerson, is(possibleLinkedTo(myTargetPatient1)));
assertThat(mergedPerson, is(possibleLinkedTo(myTargetPatient1)));
assertEquals(1, myToPerson.getLink().size());
}
@ -155,10 +155,10 @@ public class EmpiPersonMergerSvcTest extends BaseEmpiR4Test {
public void fromNoLinkToLink() {
createEmpiLink(myToPerson, myTargetPatient1);
mergePersons();
List<EmpiLink> links = myEmpiLinkDaoSvc.findEmpiLinksByPerson(myToPerson);
Person mergedPerson = mergePersons();
List<EmpiLink> links = myEmpiLinkDaoSvc.findEmpiLinksByPerson(mergedPerson);
assertEquals(1, links.size());
assertThat(myToPerson, is(possibleLinkedTo(myTargetPatient1)));
assertThat(mergedPerson, is(possibleLinkedTo(myTargetPatient1)));
assertEquals(1, myToPerson.getLink().size());
}
@ -335,10 +335,10 @@ public class EmpiPersonMergerSvcTest extends BaseEmpiR4Test {
assertThat(myToPerson.getName(), hasSize(1));
assertThat(myToPerson.getName().get(0).getGiven(), hasSize(2));
mergePersons();
assertThat(myToPerson.getName(), hasSize(2));
assertThat(myToPerson.getName().get(0).getGiven(), hasSize(2));
assertThat(myToPerson.getName().get(1).getGiven(), hasSize(2));
Person mergedPerson = mergePersons();
assertThat(mergedPerson.getName(), hasSize(2));
assertThat(mergedPerson.getName().get(0).getGiven(), hasSize(2));
assertThat(mergedPerson.getName().get(1).getGiven(), hasSize(2));
}
@Test

View File

@ -0,0 +1,41 @@
package ca.uhn.fhir.jpa.empi.svc;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.IEmpiSettings;
import ca.uhn.fhir.empi.rules.json.EmpiRulesJson;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
@ExtendWith(SpringExtension.class)
class EmpiResourceFilteringSvcMockTest {
@MockBean
private IEmpiSettings myEmpiSettings;
@MockBean
EmpiSearchParamSvc myEmpiSearchParamSvc;
@MockBean
FhirContext myFhirContext;
@Autowired
private EmpiResourceFilteringSvc myEmpiResourceFilteringSvc;
@Configuration
static class SpringConfig {
@Bean EmpiResourceFilteringSvc empiResourceFilteringSvc() {
return new EmpiResourceFilteringSvc();
}
}
@Test
public void testEmptyCriteriaShouldBeProcessed() {
when(myEmpiSettings.getEmpiRules()).thenReturn(new EmpiRulesJson());
assertTrue(myEmpiResourceFilteringSvc.shouldBeProcessed(new Patient()));
}
}

View File

@ -3,15 +3,21 @@
"candidateSearchParams": [
{
"resourceType": "Patient",
"searchParams": ["birthdate"]
"searchParams": [
"birthdate"
]
},
{
"resourceType": "*",
"searchParams": ["identifier"]
"searchParams": [
"identifier"
]
},
{
"resourceType": "Patient",
"searchParams": ["general-practitioner"]
"searchParams": [
"general-practitioner"
]
}
],
"candidateFilterSearchParams": [
@ -26,22 +32,35 @@
"name": "cosine-given-name",
"resourceType": "*",
"resourcePath": "name.given",
"metric": "COSINE",
"matchThreshold": 0.8,
"exact": true
"similarity": {
"algorithm": "COSINE",
"matchThreshold": 0.8,
"exact": true
}
},
{
"name": "jaro-last-name",
"resourceType": "*",
"resourcePath": "name.family",
"metric": "JARO_WINKLER",
"matchThreshold": 0.8,
"exact": true
"similarity": {
"algorithm": "JARO_WINKLER",
"matchThreshold": 0.8,
"exact": true
}
},
{
"name": "medicare-id",
"resourceType": "*",
"resourcePath": "identifier",
"matcher": {
"algorithm": "IDENTIFIER",
"identifierSystem": "http://hl7.org/fhir/sid/us-medicare"
}
}
],
"matchResultMap": {
"cosine-given-name" : "POSSIBLE_MATCH",
"cosine-given-name,jaro-last-name" : "MATCH"
"cosine-given-name": "POSSIBLE_MATCH",
"cosine-given-name,jaro-last-name": "MATCH"
},
"eidSystem": "http://company.io/fhir/NamingSystem/custom-eid-system"
}

View File

@ -13,17 +13,30 @@
"name": "cosine-given-name",
"resourceType": "*",
"resourcePath": "name.given",
"metric": "COSINE",
"matchThreshold": 0.8,
"exact": true
"similarity": {
"algorithm": "COSINE",
"matchThreshold": 0.8,
"exact": true
}
},
{
"name": "jaro-last-name",
"resourceType": "*",
"resourcePath": "name.family",
"metric": "JARO_WINKLER",
"matchThreshold": 0.8,
"exact": true
"similarity": {
"algorithm": "JARO_WINKLER",
"matchThreshold": 0.8,
"exact": true
}
},
{
"name": "medicare-id",
"resourceType": "*",
"resourcePath": "identifier",
"matcher": {
"algorithm": "IDENTIFIER",
"identifierSystem": "http://hl7.org/fhir/sid/us-medicare"
}
}
],
"matchResultMap": {

View File

@ -0,0 +1,142 @@
package ca.uhn.fhir.empi.api;
import ca.uhn.fhir.model.api.IModelJson;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Date;
public class EmpiLinkJson implements IModelJson {
@JsonProperty("personId")
private String myPersonId;
@JsonProperty("targetId")
private String myTargetId;
@JsonProperty("matchResult")
private EmpiMatchResultEnum myMatchResult;
@JsonProperty("linkSource")
private EmpiLinkSourceEnum myLinkSource;
@JsonProperty("created")
private Date myCreated;
@JsonProperty("updated")
private Date myUpdated;
@JsonProperty("version")
private String myVersion;
/** This link was created as a result of an eid match **/
@JsonProperty("eidMatch")
private Boolean myEidMatch;
/** This link created a new person **/
@JsonProperty("newPerson")
private Boolean myNewPerson;
@JsonProperty("vector")
private Long myVector;
@JsonProperty("score")
private Double myScore;
public String getPersonId() {
return myPersonId;
}
public EmpiLinkJson setPersonId(String thePersonId) {
myPersonId = thePersonId;
return this;
}
public String getTargetId() {
return myTargetId;
}
public EmpiLinkJson setTargetId(String theTargetId) {
myTargetId = theTargetId;
return this;
}
public EmpiMatchResultEnum getMatchResult() {
return myMatchResult;
}
public EmpiLinkJson setMatchResult(EmpiMatchResultEnum theMatchResult) {
myMatchResult = theMatchResult;
return this;
}
public EmpiLinkSourceEnum getLinkSource() {
return myLinkSource;
}
public EmpiLinkJson setLinkSource(EmpiLinkSourceEnum theLinkSource) {
myLinkSource = theLinkSource;
return this;
}
public Date getCreated() {
return myCreated;
}
public EmpiLinkJson setCreated(Date theCreated) {
myCreated = theCreated;
return this;
}
public Date getUpdated() {
return myUpdated;
}
public EmpiLinkJson setUpdated(Date theUpdated) {
myUpdated = theUpdated;
return this;
}
public String getVersion() {
return myVersion;
}
public EmpiLinkJson setVersion(String theVersion) {
myVersion = theVersion;
return this;
}
public Boolean getEidMatch() {
return myEidMatch;
}
public EmpiLinkJson setEidMatch(Boolean theEidMatch) {
myEidMatch = theEidMatch;
return this;
}
public Boolean getNewPerson() {
return myNewPerson;
}
public EmpiLinkJson setNewPerson(Boolean theNewPerson) {
myNewPerson = theNewPerson;
return this;
}
public Long getVector() {
return myVector;
}
public EmpiLinkJson setVector(Long theVector) {
myVector = theVector;
return this;
}
public Double getScore() {
return myScore;
}
public EmpiLinkJson setScore(Double theScore) {
myScore = theScore;
return this;
}
}

View File

@ -20,6 +20,8 @@ package ca.uhn.fhir.empi.api;
* #L%
*/
import org.apache.commons.lang3.builder.ToStringBuilder;
/**
* This data object captures the final outcome of an EMPI match
*/
@ -102,4 +104,15 @@ public final class EmpiMatchOutcome {
myEidMatch = theEidMatch;
return this;
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("vector", vector)
.append("score", score)
.append("myNewPerson", myNewPerson)
.append("myEidMatch", myEidMatch)
.append("myMatchResultEnum", myMatchResultEnum)
.toString();
}
}

View File

@ -0,0 +1,15 @@
package ca.uhn.fhir.empi.api;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import org.hl7.fhir.instance.model.api.IAnyResource;
import javax.annotation.Nullable;
import java.util.stream.Stream;
public interface IEmpiControllerSvc {
Stream<EmpiLinkJson> queryLinks(@Nullable String thePersonId, @Nullable String theTargetId, @Nullable String theMatchResult, @Nullable String theLinkSource, EmpiTransactionContext theEmpiContext);
Stream<EmpiLinkJson> getDuplicatePersons(EmpiTransactionContext theEmpiContext);
void notDuplicatePerson(String thePersonId, String theTargetPersonId, EmpiTransactionContext theEmpiContext);
IAnyResource mergePersons(String theFromPersonId, String theToPersonId, EmpiTransactionContext theEmpiTransactionContext);
IAnyResource updateLink(String thePersonId, String theTargetId, String theMatchResult, EmpiTransactionContext theEmpiContext);
}

View File

@ -20,7 +20,7 @@ package ca.uhn.fhir.empi.api;
* #L%
*/
public interface IEmpiResetSvc {
public interface IEmpiExpungeSvc {
/**
* Given a resource type, delete the underlying EMPI links, and their related person objects.

View File

@ -21,12 +21,14 @@ package ca.uhn.fhir.empi.api;
*/
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.stream.Stream;
/**
* This service supports the EMPI Operation providers for those services that return multiple empi links.
*/
public interface IEmpiLinkQuerySvc {
IBaseParameters queryLinks(IIdType thePersonId, IIdType theTargetId, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiContext);
IBaseParameters getPossibleDuplicates(EmpiTransactionContext theEmpiContext);
Stream<EmpiLinkJson> queryLinks(IIdType thePersonId, IIdType theTargetId, EmpiMatchResultEnum theMatchResult, EmpiLinkSourceEnum theLinkSource, EmpiTransactionContext theEmpiContext);
Stream<EmpiLinkJson> getDuplicatePersons(EmpiTransactionContext theEmpiContext);
}

View File

@ -22,10 +22,8 @@ package ca.uhn.fhir.empi.api;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseParameters;
public interface IEmpiLinkUpdaterSvc {
IAnyResource updateLink(IAnyResource thePerson, IAnyResource theTarget, EmpiMatchResultEnum theMatchResult, EmpiTransactionContext theEmpiContext);
IBaseParameters notDuplicatePerson(IAnyResource thePerson, IAnyResource theTarget, EmpiTransactionContext theEmpiContext);
void notDuplicatePerson(IAnyResource thePerson, IAnyResource theTarget, EmpiTransactionContext theEmpiContext);
}

View File

@ -22,7 +22,9 @@ package ca.uhn.fhir.empi.api;
import org.hl7.fhir.instance.model.api.IIdType;
public interface IEmpiBatchSvc {
import javax.annotation.Nullable;
public interface IEmpiSubmitSvc {
/**
* Submit all eligible resources for EMPI processing.
@ -34,7 +36,7 @@ public interface IEmpiBatchSvc {
*
* @return
*/
long runEmpiOnAllTargetTypes(String theCriteria);
long submitAllTargetTypesToEmpi(@Nullable String theCriteria);
/**
* Given a type and a search criteria, submit all found resources for EMPI processing.
@ -43,29 +45,29 @@ public interface IEmpiBatchSvc {
* @param theCriteria The FHIR search critieria for filtering the resources to be submitted for EMPI processing..
* @return the number of resources submitted for EMPI processing.
*/
long runEmpiOnTargetType(String theTargetType, String theCriteria);
long submitTargetTypeToEmpi(String theTargetType, String theCriteria);
/**
* Convenience method that calls {@link #runEmpiOnTargetType(String, String)} with the type pre-populated.
* Convenience method that calls {@link #submitTargetTypeToEmpi(String, String)} with the type pre-populated.
*
* @param theCriteria The FHIR search critieria for filtering the resources to be submitted for EMPI processing.
* @return the number of resources submitted for EMPI processing.
*/
long runEmpiOnPractitionerType(String theCriteria);
long submitPractitionerTypeToEmpi(String theCriteria);
/**
* Convenience method that calls {@link #runEmpiOnTargetType(String, String)} with the type pre-populated.
* Convenience method that calls {@link #submitTargetTypeToEmpi(String, String)} with the type pre-populated.
*
* @param theCriteria The FHIR search critieria for filtering the resources to be submitted for EMPI processing.
* @return the number of resources submitted for EMPI processing.
*/
long runEmpiOnPatientType(String theCriteria);
long submitPatientTypeToEmpi(String theCriteria);
/**
* Given an ID and a target type valid for EMPI, manually submit the given ID for EMPI processing.
* @param theId the ID of the resource to process for EMPI.
* @return the constant `1`, as if this function returns successfully, it will have processed one resource for EMPI.
*/
long runEmpiOnTarget(IIdType theId);
long submitTargetToEmpi(IIdType theId);
}

View File

@ -33,9 +33,13 @@ public class EmpiTransactionContext {
public enum OperationType {
CREATE,
UPDATE,
BATCH,
CREATE_RESOURCE,
UPDATE_RESOURCE,
SUBMIT_RESOURCE_TO_EMPI,
QUERY_LINKS,
UPDATE_LINK,
DUPLICATE_PERSONS,
NOT_DUPLICATE,
MERGE_PERSONS
}
public TransactionLogMessages getTransactionLogMessages() {

View File

@ -21,65 +21,26 @@ package ca.uhn.fhir.empi.provider;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiLinkJson;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.TransactionLogMessages;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.validation.IResourceLoader;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.util.ParametersUtil;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseParameters;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.stream.Stream;
public abstract class BaseEmpiProvider {
protected final FhirContext myFhirContext;
private final IResourceLoader myResourceLoader;
public BaseEmpiProvider(FhirContext theFhirContext, IResourceLoader theResourceLoader) {
public BaseEmpiProvider(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
myResourceLoader = theResourceLoader;
}
protected IAnyResource getLatestPersonFromIdOrThrowException(String theParamName, String theId) {
IdDt personId = getPersonIdDtOrThrowException(theParamName, theId);
return loadResource(personId.toUnqualifiedVersionless());
}
private IdDt getPersonIdDtOrThrowException(String theParamName, String theId) {
IdDt personId = new IdDt(theId);
if (!"Person".equals(personId.getResourceType()) ||
personId.getIdPart() == null) {
throw new InvalidRequestException(theParamName + " must have form Person/<id> where <id> is the id of the person");
}
return personId;
}
protected IAnyResource getLatestTargetFromIdOrThrowException(String theParamName, String theId) {
IIdType targetId = getTargetIdDtOrThrowException(theParamName, theId);
return loadResource(targetId.toUnqualifiedVersionless());
}
protected IIdType getTargetIdDtOrThrowException(String theParamName, String theId) {
IdDt targetId = new IdDt(theId);
String resourceType = targetId.getResourceType();
if (!EmpiUtil.supportedTargetType(resourceType) ||
targetId.getIdPart() == null) {
throw new InvalidRequestException(theParamName + " must have form Patient/<id> or Practitioner/<id> where <id> is the id of the resource");
}
return targetId;
}
protected IAnyResource loadResource(IIdType theResourceId) {
Class<? extends IBaseResource> resourceClass = myFhirContext.getResourceDefinition(theResourceId.getResourceType()).getImplementingClass();
return (IAnyResource) myResourceLoader.load(resourceClass, theResourceId);
}
protected void validateMergeParameters(IPrimitiveType<String> theFromPersonId, IPrimitiveType<String> theToPersonId) {
@ -90,20 +51,6 @@ public abstract class BaseEmpiProvider {
}
}
protected void validateMergeResources(IAnyResource theFromPerson, IAnyResource theToPerson) {
validateIsEmpiManaged(ProviderConstants.EMPI_MERGE_PERSONS_FROM_PERSON_ID, theFromPerson);
validateIsEmpiManaged(ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, theToPerson);
}
private void validateIsEmpiManaged(String theName, IAnyResource thePerson) {
if (!"Person".equals(myFhirContext.getResourceType(thePerson))) {
throw new InvalidRequestException("Only Person resources can be merged. The " + theName + " points to a " + myFhirContext.getResourceType(thePerson));
}
if (!EmpiUtil.isEmpiManaged(thePerson)) {
throw new InvalidRequestException("Only EMPI managed resources can be merged. Empi managed resource have the " + EmpiConstants.CODE_HAPI_EMPI_MANAGED + " tag.");
}
}
private void validateNotNull(String theName, IPrimitiveType<String> theString) {
if (theString == null || theString.getValue() == null) {
throw new InvalidRequestException(theName + " cannot be null");
@ -130,59 +77,34 @@ public abstract class BaseEmpiProvider {
validateNotNull(ProviderConstants.EMPI_UPDATE_LINK_TARGET_ID, theTargetId);
}
protected EmpiTransactionContext createEmpiContext(RequestDetails theRequestDetails) {
protected EmpiTransactionContext createEmpiContext(RequestDetails theRequestDetails, EmpiTransactionContext.OperationType theOperationType) {
TransactionLogMessages transactionLogMessages = TransactionLogMessages.createFromTransactionGuid(theRequestDetails.getTransactionGuid());
return new EmpiTransactionContext(transactionLogMessages, EmpiTransactionContext.OperationType.MERGE_PERSONS);
return new EmpiTransactionContext(transactionLogMessages, theOperationType);
}
protected EmpiMatchResultEnum extractMatchResultOrNull(IPrimitiveType<String> theMatchResult) {
String matchResult = extractStringNull(theMatchResult);
if (matchResult == null) {
return null;
}
return EmpiMatchResultEnum.valueOf(matchResult);
}
protected EmpiLinkSourceEnum extractLinkSourceOrNull(IPrimitiveType<String> theLinkSource) {
String linkSource = extractStringNull(theLinkSource);
if (linkSource == null) {
return null;
}
return EmpiLinkSourceEnum.valueOf(linkSource);
}
private String extractStringNull(IPrimitiveType<String> theString) {
protected String extractStringOrNull(IPrimitiveType<String> theString) {
if (theString == null) {
return null;
}
return theString.getValue();
}
protected IIdType extractPersonIdDtOrNull(String theName, IPrimitiveType<String> thePersonId) {
String personId = extractStringNull(thePersonId);
if (personId == null) {
return null;
}
return getPersonIdDtOrThrowException(theName, personId);
}
protected IBaseParameters parametersFromEmpiLinks(Stream<EmpiLinkJson> theEmpiLinkStream, boolean includeResultAndSource) {
IBaseParameters retval = ParametersUtil.newInstance(myFhirContext);
protected IIdType extractTargetIdDtOrNull(String theName, IPrimitiveType<String> theTargetId) {
String targetId = extractStringNull(theTargetId);
if (targetId == null) {
return null;
}
return getTargetIdDtOrThrowException(theName, targetId);
}
theEmpiLinkStream.forEach(empiLink -> {
IBase resultPart = ParametersUtil.addParameterToParameters(myFhirContext, retval, "link");
ParametersUtil.addPartString(myFhirContext, resultPart, "personId", empiLink.getPersonId());
ParametersUtil.addPartString(myFhirContext, resultPart, "targetId", empiLink.getTargetId());
protected void validateSameVersion(IAnyResource theResource, IPrimitiveType<String> theResourceId) {
String storedId = theResource.getIdElement().getValue();
String requestedId = theResourceId.getValue();
if (hasVersionIdPart(requestedId) && !storedId.equals(requestedId)) {
throw new ResourceVersionConflictException("Requested resource " + requestedId + " is not the latest version. Latest version is " + storedId);
}
}
private boolean hasVersionIdPart(String theId) {
return new IdDt(theId).hasVersionIdPart();
if (includeResultAndSource) {
ParametersUtil.addPartString(myFhirContext, resultPart, "matchResult", empiLink.getMatchResult().name());
ParametersUtil.addPartString(myFhirContext, resultPart, "linkSource", empiLink.getLinkSource().name());
ParametersUtil.addPartBoolean(myFhirContext, resultPart, "eidMatch", empiLink.getEidMatch());
ParametersUtil.addPartBoolean(myFhirContext, resultPart, "newPerson", empiLink.getNewPerson());
ParametersUtil.addPartDecimal(myFhirContext, resultPart, "score", empiLink.getScore());
}
});
return retval;
}
}

View File

@ -0,0 +1,72 @@
package ca.uhn.fhir.empi.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiConstants;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.validation.IResourceLoader;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class EmpiControllerHelper {
private final FhirContext myFhirContext;
private final IResourceLoader myResourceLoader;
@Autowired
public EmpiControllerHelper(FhirContext theFhirContext, IResourceLoader theResourceLoader) {
myFhirContext = theFhirContext;
myResourceLoader = theResourceLoader;
}
public void validateSameVersion(IAnyResource theResource, String theResourceId) {
String storedId = theResource.getIdElement().getValue();
if (hasVersionIdPart(theResourceId) && !storedId.equals(theResourceId)) {
throw new ResourceVersionConflictException("Requested resource " + theResourceId + " is not the latest version. Latest version is " + storedId);
}
}
private boolean hasVersionIdPart(String theId) {
return new IdDt(theId).hasVersionIdPart();
}
public IAnyResource getLatestPersonFromIdOrThrowException(String theParamName, String theId) {
IdDt personId = EmpiControllerUtil.getPersonIdDtOrThrowException(theParamName, theId);
return loadResource(personId.toUnqualifiedVersionless());
}
public IAnyResource getLatestTargetFromIdOrThrowException(String theParamName, String theId) {
IIdType targetId = EmpiControllerUtil.getTargetIdDtOrThrowException(theParamName, theId);
return loadResource(targetId.toUnqualifiedVersionless());
}
protected IAnyResource loadResource(IIdType theResourceId) {
Class<? extends IBaseResource> resourceClass = myFhirContext.getResourceDefinition(theResourceId.getResourceType()).getImplementingClass();
return (IAnyResource) myResourceLoader.load(resourceClass, theResourceId);
}
public void validateMergeResources(IAnyResource theFromPerson, IAnyResource theToPerson) {
validateIsEmpiManaged(ProviderConstants.EMPI_MERGE_PERSONS_FROM_PERSON_ID, theFromPerson);
validateIsEmpiManaged(ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, theToPerson);
}
public String toJson(IAnyResource theAnyResource) {
return myFhirContext.newJsonParser().encodeResourceToString(theAnyResource);
}
private void validateIsEmpiManaged(String theName, IAnyResource thePerson) {
if (!"Person".equals(myFhirContext.getResourceType(thePerson))) {
throw new InvalidRequestException("Only Person resources can be merged. The " + theName + " points to a " + myFhirContext.getResourceType(thePerson));
}
if (!EmpiUtil.isEmpiManaged(thePerson)) {
throw new InvalidRequestException("Only EMPI managed resources can be merged. Empi managed resource have the " + EmpiConstants.CODE_HAPI_EMPI_MANAGED + " tag.");
}
}
}

View File

@ -0,0 +1,57 @@
package ca.uhn.fhir.empi.provider;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.util.EmpiUtil;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IIdType;
public class EmpiControllerUtil {
public static EmpiMatchResultEnum extractMatchResultOrNull(String theMatchResult) {
if (theMatchResult == null) {
return null;
}
return EmpiMatchResultEnum.valueOf(theMatchResult);
}
public static EmpiLinkSourceEnum extractLinkSourceOrNull(String theLinkSource) {
if (theLinkSource == null) {
return null;
}
return EmpiLinkSourceEnum.valueOf(theLinkSource);
}
public static IIdType extractPersonIdDtOrNull(String theName, String thePersonId) {
if (thePersonId == null) {
return null;
}
return getPersonIdDtOrThrowException(theName, thePersonId);
}
public static IIdType extractTargetIdDtOrNull(String theName, String theTargetId) {
if (theTargetId == null) {
return null;
}
return getTargetIdDtOrThrowException(theName, theTargetId);
}
static IdDt getPersonIdDtOrThrowException(String theParamName, String theId) {
IdDt personId = new IdDt(theId);
if (!"Person".equals(personId.getResourceType()) ||
personId.getIdPart() == null) {
throw new InvalidRequestException(theParamName + " is '" + theId + "'. must have form Person/<id> where <id> is the id of the person");
}
return personId;
}
public static IIdType getTargetIdDtOrThrowException(String theParamName, String theId) {
IdDt targetId = new IdDt(theId);
String resourceType = targetId.getResourceType();
if (!EmpiUtil.supportedTargetType(resourceType) ||
targetId.getIdPart() == null) {
throw new InvalidRequestException(theParamName + " is '" + theId + "'. must have form Patient/<id> or Practitioner/<id> where <id> is the id of the resource");
}
return targetId;
}
}

View File

@ -21,14 +21,12 @@ package ca.uhn.fhir.empi.provider;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.api.EmpiLinkJson;
import ca.uhn.fhir.empi.api.IEmpiControllerSvc;
import ca.uhn.fhir.empi.api.IEmpiExpungeSvc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.api.IEmpiResetSvc;
import ca.uhn.fhir.empi.api.IEmpiSubmitSvc;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
@ -36,7 +34,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.validation.IResourceLoader;
import ca.uhn.fhir.util.ParametersUtil;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.DecimalType;
@ -52,14 +50,13 @@ import org.hl7.fhir.instance.model.api.IIdType;
import java.util.Collection;
import java.util.UUID;
import java.util.stream.Stream;
public class EmpiProviderDstu3 extends BaseEmpiProvider {
private final IEmpiControllerSvc myEmpiControllerSvc;
private final IEmpiMatchFinderSvc myEmpiMatchFinderSvc;
private final IEmpiPersonMergerSvc myPersonMergerSvc;
private final IEmpiLinkUpdaterSvc myEmpiLinkUpdaterSvc;
private final IEmpiLinkQuerySvc myEmpiLinkQuerySvc;
private final IEmpiResetSvc myEmpiResetSvc;
private final IEmpiBatchSvc myEmpiBatchSvc;
private final IEmpiExpungeSvc myEmpiResetSvc;
private final IEmpiSubmitSvc myEmpiBatchSvc;
/**
* Constructor
@ -67,12 +64,10 @@ public class EmpiProviderDstu3 extends BaseEmpiProvider {
* Note that this is not a spring bean. Any necessary injections should
* happen in the constructor
*/
public EmpiProviderDstu3(FhirContext theFhirContext, IEmpiMatchFinderSvc theEmpiMatchFinderSvc, IEmpiPersonMergerSvc thePersonMergerSvc, IEmpiLinkUpdaterSvc theEmpiLinkUpdaterSvc, IEmpiLinkQuerySvc theEmpiLinkQuerySvc, IResourceLoader theResourceLoader, IEmpiResetSvc theEmpiResetSvc, IEmpiBatchSvc theEmpiBatchSvc) {
super(theFhirContext, theResourceLoader);
public EmpiProviderDstu3(FhirContext theFhirContext, IEmpiControllerSvc theEmpiControllerSvc, IEmpiMatchFinderSvc theEmpiMatchFinderSvc, IEmpiExpungeSvc theEmpiResetSvc, IEmpiSubmitSvc theEmpiBatchSvc) {
super(theFhirContext);
myEmpiControllerSvc = theEmpiControllerSvc;
myEmpiMatchFinderSvc = theEmpiMatchFinderSvc;
myPersonMergerSvc = thePersonMergerSvc;
myEmpiLinkUpdaterSvc = theEmpiLinkUpdaterSvc;
myEmpiLinkQuerySvc = theEmpiLinkQuerySvc;
myEmpiResetSvc = theEmpiResetSvc;
myEmpiBatchSvc = theEmpiBatchSvc;
}
@ -101,13 +96,8 @@ public class EmpiProviderDstu3 extends BaseEmpiProvider {
@OperationParam(name=ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, min = 1, max = 1) StringType theToPersonId,
RequestDetails theRequestDetails) {
validateMergeParameters(theFromPersonId, theToPersonId);
IAnyResource fromPerson = getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_MERGE_PERSONS_FROM_PERSON_ID, theFromPersonId.getValue());
IAnyResource toPerson = getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, theToPersonId.getValue());
validateMergeResources(fromPerson, toPerson);
validateSameVersion(fromPerson, theFromPersonId);
validateSameVersion(toPerson, theToPersonId);
return (Person) myPersonMergerSvc.mergePersons(fromPerson, toPerson, createEmpiContext(theRequestDetails));
return (Person) myEmpiControllerSvc.mergePersons(theFromPersonId.getValue(), theToPersonId.getValue(), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.MERGE_PERSONS));
}
@Operation(name = ProviderConstants.EMPI_UPDATE_LINK, type = Person.class)
@ -117,13 +107,8 @@ public class EmpiProviderDstu3 extends BaseEmpiProvider {
ServletRequestDetails theRequestDetails) {
validateUpdateLinkParameters(thePersonId, theTargetId, theMatchResult);
EmpiMatchResultEnum matchResult = extractMatchResultOrNull(theMatchResult);
IAnyResource person = getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_PERSON_ID, thePersonId.getValue());
IAnyResource target = getLatestTargetFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_TARGET_ID, theTargetId.getValue());
validateSameVersion(person, thePersonId);
validateSameVersion(target, theTargetId);
return (Person) myEmpiLinkUpdaterSvc.updateLink(person, target, matchResult, createEmpiContext(theRequestDetails));
return (Person) myEmpiControllerSvc.updateLink(thePersonId.getValue(), theTargetId.getValue(), theMatchResult.getValue(), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.UPDATE_LINK));
}
@Operation(name = ProviderConstants.EMPI_QUERY_LINKS)
@ -132,17 +117,15 @@ public class EmpiProviderDstu3 extends BaseEmpiProvider {
@OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_MATCH_RESULT, min = 0, max = 1) StringType theMatchResult,
@OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_MATCH_RESULT, min = 0, max = 1) StringType theLinkSource,
ServletRequestDetails theRequestDetails) {
IIdType personId = extractPersonIdDtOrNull(ProviderConstants.EMPI_QUERY_LINKS_PERSON_ID, thePersonId);
IIdType targetId = extractTargetIdDtOrNull(ProviderConstants.EMPI_QUERY_LINKS_TARGET_ID, theTargetId);
EmpiMatchResultEnum matchResult = extractMatchResultOrNull(theMatchResult);
EmpiLinkSourceEnum linkSource = extractLinkSourceOrNull(theLinkSource);
return (Parameters) myEmpiLinkQuerySvc.queryLinks(personId, targetId, matchResult, linkSource, createEmpiContext(theRequestDetails));
Stream<EmpiLinkJson> empiLinkJson = myEmpiControllerSvc.queryLinks(extractStringOrNull(thePersonId), extractStringOrNull(theTargetId), extractStringOrNull(theMatchResult), extractStringOrNull(theLinkSource), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.QUERY_LINKS));
return (Parameters) parametersFromEmpiLinks(empiLinkJson, true);
}
@Operation(name = ProviderConstants.EMPI_DUPLICATE_PERSONS)
public Parameters getDuplicatePersons(ServletRequestDetails theRequestDetails) {
return (Parameters) myEmpiLinkQuerySvc.getPossibleDuplicates(createEmpiContext(theRequestDetails));
Stream<EmpiLinkJson> possibleDuplicates = myEmpiControllerSvc.getDuplicatePersons(createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.QUERY_LINKS));
return (Parameters) parametersFromEmpiLinks(possibleDuplicates, false);
}
@Operation(name = ProviderConstants.EMPI_NOT_DUPLICATE)
@ -152,22 +135,21 @@ public class EmpiProviderDstu3 extends BaseEmpiProvider {
ServletRequestDetails theRequestDetails) {
validateNotDuplicateParameters(thePersonId, theTargetId);
IAnyResource person = getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_PERSON_ID, thePersonId.getValue());
IAnyResource target = getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_TARGET_ID, theTargetId.getValue());
validateSameVersion(person, thePersonId);
validateSameVersion(target, theTargetId);
myEmpiControllerSvc.notDuplicatePerson(thePersonId.getValue(), theTargetId.getValue(), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.NOT_DUPLICATE));
return (Parameters) myEmpiLinkUpdaterSvc.notDuplicatePerson(person, target, createEmpiContext(theRequestDetails));
Parameters retval = (Parameters) ParametersUtil.newInstance(myFhirContext);
ParametersUtil.addParameterToParametersBoolean(myFhirContext, retval, "success", true);
return retval;
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, returnParameters = {
@Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type= DecimalType.class)
})
public Parameters empiBatchOnAllTargets(
@OperationParam(name= ProviderConstants.EMPI_BATCH_RUN_CRITERIA,min = 0 , max = 1) StringType theCriteria,
ServletRequestDetails theRequestDetails) {
String criteria = convertCriteriaToString(theCriteria);
long submittedCount = myEmpiBatchSvc.runEmpiOnAllTargetTypes(criteria);
long submittedCount = myEmpiBatchSvc.submitAllTargetTypesToEmpi(criteria);
return buildEmpiOutParametersWithCount(submittedCount);
}
@ -191,45 +173,45 @@ public class EmpiProviderDstu3 extends BaseEmpiProvider {
return parameters;
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Patient.class, returnParameters = {
@Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Patient.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = DecimalType.class)
})
public Parameters empiBatchPatientInstance(
@IdParam IIdType theIdParam,
RequestDetails theRequest) {
long submittedCount = myEmpiBatchSvc.runEmpiOnTarget(theIdParam);
long submittedCount = myEmpiBatchSvc.submitTargetToEmpi(theIdParam);
return buildEmpiOutParametersWithCount(submittedCount);
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Patient.class, returnParameters = {
@Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Patient.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = DecimalType.class)
})
public Parameters empiBatchPatientType(
@OperationParam(name = ProviderConstants.EMPI_BATCH_RUN_CRITERIA) StringType theCriteria,
RequestDetails theRequest) {
String criteria = convertCriteriaToString(theCriteria);
long submittedCount = myEmpiBatchSvc.runEmpiOnPatientType(criteria);
long submittedCount = myEmpiBatchSvc.submitPatientTypeToEmpi(criteria);
return buildEmpiOutParametersWithCount(submittedCount);
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Practitioner.class, returnParameters = {
@Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Practitioner.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = DecimalType.class)
})
public Parameters empiBatchPractitionerInstance(
@IdParam IIdType theIdParam,
RequestDetails theRequest) {
long submittedCount = myEmpiBatchSvc.runEmpiOnTarget(theIdParam);
long submittedCount = myEmpiBatchSvc.submitTargetToEmpi(theIdParam);
return buildEmpiOutParametersWithCount(submittedCount);
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Practitioner.class, returnParameters = {
@Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Practitioner.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = DecimalType.class)
})
public Parameters empiBatchPractitionerType(
@OperationParam(name = ProviderConstants.EMPI_BATCH_RUN_CRITERIA) StringType theCriteria,
RequestDetails theRequest) {
String criteria = convertCriteriaToString(theCriteria);
long submittedCount = myEmpiBatchSvc.runEmpiOnPractitionerType(criteria);
long submittedCount = myEmpiBatchSvc.submitPractitionerTypeToEmpi(criteria);
return buildEmpiOutParametersWithCount(submittedCount);
}

View File

@ -22,14 +22,11 @@ package ca.uhn.fhir.empi.provider;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiResetSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.api.IEmpiControllerSvc;
import ca.uhn.fhir.empi.api.IEmpiExpungeSvc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.api.IEmpiSubmitSvc;
import ca.uhn.fhir.rest.server.provider.ResourceProviderFactory;
import ca.uhn.fhir.validation.IResourceLoader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -42,25 +39,19 @@ public class EmpiProviderLoader {
@Autowired
private IEmpiMatchFinderSvc myEmpiMatchFinderSvc;
@Autowired
private IEmpiPersonMergerSvc myPersonMergerSvc;
private IEmpiControllerSvc myEmpiControllerSvc;
@Autowired
private IEmpiLinkUpdaterSvc myEmpiLinkUpdaterSvc;
private IEmpiExpungeSvc myEmpiResetSvc;
@Autowired
private IEmpiLinkQuerySvc myEmpiLinkQuerySvc;
@Autowired
private IResourceLoader myResourceLoader;
@Autowired
private IEmpiResetSvc myEmpiResetSvc;
@Autowired
private IEmpiBatchSvc myEmpiBatchSvc;
private IEmpiSubmitSvc myEmpiBatchSvc;
public void loadProvider() {
switch (myFhirContext.getVersion().getVersion()) {
case DSTU3:
myResourceProviderFactory.addSupplier(() -> new EmpiProviderDstu3(myFhirContext, myEmpiMatchFinderSvc, myPersonMergerSvc, myEmpiLinkUpdaterSvc, myEmpiLinkQuerySvc, myResourceLoader, myEmpiResetSvc, myEmpiBatchSvc));
myResourceProviderFactory.addSupplier(() -> new EmpiProviderDstu3(myFhirContext, myEmpiControllerSvc, myEmpiMatchFinderSvc, myEmpiResetSvc, myEmpiBatchSvc));
break;
case R4:
myResourceProviderFactory.addSupplier(() -> new EmpiProviderR4(myFhirContext, myEmpiMatchFinderSvc, myPersonMergerSvc, myEmpiLinkUpdaterSvc, myEmpiLinkQuerySvc, myResourceLoader, myEmpiResetSvc, myEmpiBatchSvc));
myResourceProviderFactory.addSupplier(() -> new EmpiProviderR4(myFhirContext, myEmpiControllerSvc, myEmpiMatchFinderSvc, myEmpiResetSvc, myEmpiBatchSvc));
break;
default:
throw new ConfigurationException("EMPI not supported for FHIR version " + myFhirContext.getVersion().getVersion());

View File

@ -21,14 +21,12 @@ package ca.uhn.fhir.empi.provider;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.api.IEmpiBatchSvc;
import ca.uhn.fhir.empi.api.IEmpiLinkQuerySvc;
import ca.uhn.fhir.empi.api.IEmpiLinkUpdaterSvc;
import ca.uhn.fhir.empi.api.EmpiLinkJson;
import ca.uhn.fhir.empi.api.IEmpiControllerSvc;
import ca.uhn.fhir.empi.api.IEmpiExpungeSvc;
import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc;
import ca.uhn.fhir.empi.api.IEmpiPersonMergerSvc;
import ca.uhn.fhir.empi.api.IEmpiResetSvc;
import ca.uhn.fhir.empi.api.IEmpiSubmitSvc;
import ca.uhn.fhir.empi.model.EmpiTransactionContext;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
@ -36,7 +34,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.validation.IResourceLoader;
import ca.uhn.fhir.util.ParametersUtil;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -53,14 +51,13 @@ import org.hl7.fhir.r4.model.StringType;
import java.util.Collection;
import java.util.UUID;
import java.util.stream.Stream;
public class EmpiProviderR4 extends BaseEmpiProvider {
private final IEmpiControllerSvc myEmpiControllerSvc;
private final IEmpiMatchFinderSvc myEmpiMatchFinderSvc;
private final IEmpiPersonMergerSvc myPersonMergerSvc;
private final IEmpiLinkUpdaterSvc myEmpiLinkUpdaterSvc;
private final IEmpiLinkQuerySvc myEmpiLinkQuerySvc;
private final IEmpiResetSvc myEmpiExpungeSvc;
private final IEmpiBatchSvc myEmpiBatchSvc;
private final IEmpiExpungeSvc myEmpiExpungeSvc;
private final IEmpiSubmitSvc myEmpiSubmitSvc;
/**
* Constructor
@ -68,14 +65,12 @@ public class EmpiProviderR4 extends BaseEmpiProvider {
* Note that this is not a spring bean. Any necessary injections should
* happen in the constructor
*/
public EmpiProviderR4(FhirContext theFhirContext, IEmpiMatchFinderSvc theEmpiMatchFinderSvc, IEmpiPersonMergerSvc thePersonMergerSvc, IEmpiLinkUpdaterSvc theEmpiLinkUpdaterSvc, IEmpiLinkQuerySvc theEmpiLinkQuerySvc, IResourceLoader theResourceLoader, IEmpiResetSvc theEmpiExpungeSvc, IEmpiBatchSvc theEmpiBatchSvc) {
super(theFhirContext, theResourceLoader);
public EmpiProviderR4(FhirContext theFhirContext, IEmpiControllerSvc theEmpiControllerSvc, IEmpiMatchFinderSvc theEmpiMatchFinderSvc, IEmpiExpungeSvc theEmpiExpungeSvc, IEmpiSubmitSvc theEmpiSubmitSvc) {
super(theFhirContext);
myEmpiControllerSvc = theEmpiControllerSvc;
myEmpiMatchFinderSvc = theEmpiMatchFinderSvc;
myPersonMergerSvc = thePersonMergerSvc;
myEmpiLinkUpdaterSvc = theEmpiLinkUpdaterSvc;
myEmpiLinkQuerySvc = theEmpiLinkQuerySvc;
myEmpiExpungeSvc = theEmpiExpungeSvc;
myEmpiBatchSvc = theEmpiBatchSvc;
myEmpiSubmitSvc = theEmpiSubmitSvc;
}
@Operation(name = ProviderConstants.EMPI_MATCH, type = Patient.class)
@ -103,13 +98,8 @@ public class EmpiProviderR4 extends BaseEmpiProvider {
@OperationParam(name=ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, min = 1, max = 1) StringType theToPersonId,
RequestDetails theRequestDetails) {
validateMergeParameters(theFromPersonId, theToPersonId);
IAnyResource fromPerson = getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_MERGE_PERSONS_FROM_PERSON_ID, theFromPersonId.getValue());
IAnyResource toPerson = getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_MERGE_PERSONS_TO_PERSON_ID, theToPersonId.getValue());
validateMergeResources(fromPerson, toPerson);
validateSameVersion(fromPerson, theFromPersonId);
validateSameVersion(toPerson, theToPersonId);
return (Person) myPersonMergerSvc.mergePersons(fromPerson, toPerson, createEmpiContext(theRequestDetails));
return (Person) myEmpiControllerSvc.mergePersons(theFromPersonId.getValue(), theToPersonId.getValue(), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.MERGE_PERSONS));
}
@Operation(name = ProviderConstants.EMPI_UPDATE_LINK, type = Person.class)
@ -119,13 +109,8 @@ public class EmpiProviderR4 extends BaseEmpiProvider {
ServletRequestDetails theRequestDetails) {
validateUpdateLinkParameters(thePersonId, theTargetId, theMatchResult);
EmpiMatchResultEnum matchResult = extractMatchResultOrNull(theMatchResult);
IAnyResource person = getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_PERSON_ID, thePersonId.getValue());
IAnyResource target = getLatestTargetFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_TARGET_ID, theTargetId.getValue());
validateSameVersion(person, thePersonId);
validateSameVersion(target, theTargetId);
return (Person) myEmpiLinkUpdaterSvc.updateLink(person, target, matchResult, createEmpiContext(theRequestDetails));
return (Person) myEmpiControllerSvc.updateLink(thePersonId.getValueNotNull(), theTargetId.getValue(), theMatchResult.getValue(), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.UPDATE_LINK));
}
@Operation(name = ProviderConstants.EMPI_CLEAR, returnParameters = {
@ -150,17 +135,15 @@ public class EmpiProviderR4 extends BaseEmpiProvider {
@OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_MATCH_RESULT, min = 0, max = 1) StringType theMatchResult,
@OperationParam(name=ProviderConstants.EMPI_QUERY_LINKS_LINK_SOURCE, min = 0, max = 1) StringType theLinkSource,
ServletRequestDetails theRequestDetails) {
IIdType personId = extractPersonIdDtOrNull(ProviderConstants.EMPI_QUERY_LINKS_PERSON_ID, thePersonId);
IIdType targetId = extractTargetIdDtOrNull(ProviderConstants.EMPI_QUERY_LINKS_TARGET_ID, theTargetId);
EmpiMatchResultEnum matchResult = extractMatchResultOrNull(theMatchResult);
EmpiLinkSourceEnum linkSource = extractLinkSourceOrNull(theLinkSource);
return (Parameters) myEmpiLinkQuerySvc.queryLinks(personId, targetId, matchResult, linkSource, createEmpiContext(theRequestDetails));
Stream<EmpiLinkJson> empiLinkJson = myEmpiControllerSvc.queryLinks(extractStringOrNull(thePersonId), extractStringOrNull(theTargetId), extractStringOrNull(theMatchResult), extractStringOrNull(theLinkSource), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.QUERY_LINKS));
return (Parameters) parametersFromEmpiLinks(empiLinkJson, true);
}
@Operation(name = ProviderConstants.EMPI_DUPLICATE_PERSONS, idempotent = true)
public Parameters getDuplicatePersons(ServletRequestDetails theRequestDetails) {
return (Parameters) myEmpiLinkQuerySvc.getPossibleDuplicates(createEmpiContext(theRequestDetails));
Stream<EmpiLinkJson> possibleDuplicates = myEmpiControllerSvc.getDuplicatePersons(createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.DUPLICATE_PERSONS));
return (Parameters) parametersFromEmpiLinks(possibleDuplicates, false);
}
@Operation(name = ProviderConstants.EMPI_NOT_DUPLICATE)
@ -169,22 +152,21 @@ public class EmpiProviderR4 extends BaseEmpiProvider {
ServletRequestDetails theRequestDetails) {
validateNotDuplicateParameters(thePersonId, theTargetId);
IAnyResource person = getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_PERSON_ID, thePersonId.getValue());
IAnyResource target = getLatestPersonFromIdOrThrowException(ProviderConstants.EMPI_UPDATE_LINK_TARGET_ID, theTargetId.getValue());
validateSameVersion(person, thePersonId);
validateSameVersion(target, theTargetId);
myEmpiControllerSvc.notDuplicatePerson(thePersonId.getValue(), theTargetId.getValue(), createEmpiContext(theRequestDetails, EmpiTransactionContext.OperationType.NOT_DUPLICATE));
return (Parameters) myEmpiLinkUpdaterSvc.notDuplicatePerson(person, target, createEmpiContext(theRequestDetails));
Parameters retval = (Parameters) ParametersUtil.newInstance(myFhirContext);
ParametersUtil.addParameterToParametersBoolean(myFhirContext, retval, "success", true);
return retval;
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, returnParameters = {
@Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type= IntegerType.class)
})
public Parameters empiBatchOnAllTargets(
@OperationParam(name= ProviderConstants.EMPI_BATCH_RUN_CRITERIA,min = 0 , max = 1) StringType theCriteria,
ServletRequestDetails theRequestDetails) {
String criteria = convertCriteriaToString(theCriteria);
long submittedCount = myEmpiBatchSvc.runEmpiOnAllTargetTypes(criteria);
long submittedCount = myEmpiSubmitSvc.submitAllTargetTypesToEmpi(criteria);
return buildEmpiOutParametersWithCount(submittedCount);
}
@ -192,45 +174,45 @@ public class EmpiProviderR4 extends BaseEmpiProvider {
return theCriteria == null ? null : theCriteria.getValueAsString();
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Patient.class, returnParameters = {
@Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Patient.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class)
})
public Parameters empiBatchPatientInstance(
@IdParam IIdType theIdParam,
RequestDetails theRequest) {
long submittedCount = myEmpiBatchSvc.runEmpiOnTarget(theIdParam);
long submittedCount = myEmpiSubmitSvc.submitTargetToEmpi(theIdParam);
return buildEmpiOutParametersWithCount(submittedCount);
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Patient.class, returnParameters = {
@Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Patient.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class)
})
public Parameters empiBatchPatientType(
@OperationParam(name = ProviderConstants.EMPI_BATCH_RUN_CRITERIA) StringType theCriteria,
RequestDetails theRequest) {
String criteria = convertCriteriaToString(theCriteria);
long submittedCount = myEmpiBatchSvc.runEmpiOnPatientType(criteria);
long submittedCount = myEmpiSubmitSvc.submitPatientTypeToEmpi(criteria);
return buildEmpiOutParametersWithCount(submittedCount);
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Practitioner.class, returnParameters = {
@Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Practitioner.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class)
})
public Parameters empiBatchPractitionerInstance(
@IdParam IIdType theIdParam,
RequestDetails theRequest) {
long submittedCount = myEmpiBatchSvc.runEmpiOnTarget(theIdParam);
long submittedCount = myEmpiSubmitSvc.submitTargetToEmpi(theIdParam);
return buildEmpiOutParametersWithCount(submittedCount);
}
@Operation(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN, idempotent = false, type = Practitioner.class, returnParameters = {
@Operation(name = ProviderConstants.OPERATION_EMPI_SUBMIT, idempotent = false, type = Practitioner.class, returnParameters = {
@OperationParam(name = ProviderConstants.OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT, type = IntegerType.class)
})
public Parameters empiBatchPractitionerType(
@OperationParam(name = ProviderConstants.EMPI_BATCH_RUN_CRITERIA) StringType theCriteria,
RequestDetails theRequest) {
String criteria = convertCriteriaToString(theCriteria);
long submittedCount = myEmpiBatchSvc.runEmpiOnPractitionerType(criteria);
long submittedCount = myEmpiSubmitSvc.submitPractitionerTypeToEmpi(criteria);
return buildEmpiOutParametersWithCount(submittedCount);
}

View File

@ -28,6 +28,7 @@ import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson;
import ca.uhn.fhir.empi.rules.json.EmpiFilterSearchParamJson;
import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson;
import ca.uhn.fhir.empi.rules.json.EmpiRulesJson;
import ca.uhn.fhir.empi.rules.json.EmpiSimilarityJson;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.server.util.ISearchParamRetriever;
import ca.uhn.fhir.util.FhirTerser;
@ -99,18 +100,19 @@ public class EmpiRuleValidator implements IEmpiRuleValidator {
throw new ConfigurationException("Two MatchFields have the same name '" + fieldMatch.getName() + "'");
}
names.add(fieldMatch.getName());
validateThreshold(fieldMatch);
if (fieldMatch.getSimilarity() != null) {
validateSimilarity(fieldMatch);
} else if (fieldMatch.getMatcher() == null) {
throw new ConfigurationException("MatchField " + fieldMatch.getName() + " has neither a similarity nor a matcher. At least one must be present.");
}
validatePath(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 validateSimilarity(EmpiFieldMatchJson theFieldMatch) {
EmpiSimilarityJson similarity = theFieldMatch.getSimilarity();
if (similarity.getMatchThreshold() == null) {
throw new ConfigurationException("MatchField " + theFieldMatch.getName() + " similarity " + similarity.getAlgorithm() + " requires a matchThreshold");
}
}

View File

@ -20,45 +20,38 @@ package ca.uhn.fhir.empi.rules.json;
* #L%
*/
import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiMatchEvaluation;
import ca.uhn.fhir.empi.rules.matcher.EmpiMatcherEnum;
import ca.uhn.fhir.model.api.IModelJson;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hl7.fhir.instance.model.api.IBase;
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 EmpiMetricEnum} which determines the actual similarity values.
* <p></p>
* 1. A {@link EmpiMatcherEnum} which determines the actual similarity values.
* 2. A given resource type (e.g. Patient)
* 3. A given FHIRPath expression for finding the particular primitive to be used for comparison. (e.g. name.given)
*/
public class EmpiFieldMatchJson implements IModelJson {
@JsonProperty(value = "name", required = true)
String myName;
@JsonProperty(value = "resourceType", required = true)
String myResourceType;
@JsonProperty(value = "resourcePath", required = true)
String myResourcePath;
@JsonProperty(value = "metric", required = true)
EmpiMetricEnum myMetric;
@JsonProperty("matchThreshold")
Double myMatchThreshold;
/**
* For String value types, should the values be normalized (case, accents) before they are compared
*/
@JsonProperty(value = "exact")
boolean myExact;
public EmpiMetricEnum getMetric() {
return myMetric;
}
@JsonProperty(value = "matcher", required = false)
EmpiMatcherJson myMatcher;
public EmpiFieldMatchJson setMetric(EmpiMetricEnum theMetric) {
myMetric = theMetric;
return this;
}
@JsonProperty(value = "similarity", required = false)
EmpiSimilarityJson mySimilarity;
public String getResourceType() {
return myResourceType;
@ -78,16 +71,6 @@ public class EmpiFieldMatchJson implements IModelJson {
return this;
}
@Nullable
public Double getMatchThreshold() {
return myMatchThreshold;
}
public EmpiFieldMatchJson setMatchThreshold(double theMatchThreshold) {
myMatchThreshold = theMatchThreshold;
return this;
}
public String getName() {
return myName;
}
@ -97,12 +80,32 @@ public class EmpiFieldMatchJson implements IModelJson {
return this;
}
public boolean getExact() {
return myExact;
public EmpiMatcherJson getMatcher() {
return myMatcher;
}
public EmpiFieldMatchJson setExact(boolean theExact) {
myExact = theExact;
public EmpiFieldMatchJson setMatcher(EmpiMatcherJson theMatcher) {
myMatcher = theMatcher;
return this;
}
public EmpiSimilarityJson getSimilarity() {
return mySimilarity;
}
public EmpiFieldMatchJson setSimilarity(EmpiSimilarityJson theSimilarity) {
mySimilarity = theSimilarity;
return this;
}
public EmpiMatchEvaluation match(FhirContext theFhirContext, IBase theLeftValue, IBase theRightValue) {
if (myMatcher != null) {
boolean result = myMatcher.match(theFhirContext, theLeftValue, theRightValue);
return new EmpiMatchEvaluation(result, result ? 1.0 : 0.0);
}
if (mySimilarity != null) {
return mySimilarity.match(theFhirContext, theLeftValue, theRightValue);
}
throw new InternalErrorException("Field Match " + myName + " has neither a matcher nor a similarity.");
}
}

View File

@ -0,0 +1,72 @@
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.matcher.EmpiMatcherEnum;
import ca.uhn.fhir.model.api.IModelJson;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hl7.fhir.instance.model.api.IBase;
public class EmpiMatcherJson implements IModelJson {
@JsonProperty(value = "algorithm", required = true)
EmpiMatcherEnum myAlgorithm;
@JsonProperty(value = "identifierSystem", required = false)
String myIdentifierSystem;
/**
* For String value types, should the values be normalized (case, accents) before they are compared
*/
@JsonProperty(value = "exact")
boolean myExact;
public EmpiMatcherEnum getAlgorithm() {
return myAlgorithm;
}
public EmpiMatcherJson setAlgorithm(EmpiMatcherEnum theAlgorithm) {
myAlgorithm = theAlgorithm;
return this;
}
public String getIdentifierSystem() {
return myIdentifierSystem;
}
public EmpiMatcherJson setIdentifierSystem(String theIdentifierSystem) {
myIdentifierSystem = theIdentifierSystem;
return this;
}
public boolean getExact() {
return myExact;
}
public EmpiMatcherJson setExact(boolean theExact) {
myExact = theExact;
return this;
}
public boolean match(FhirContext theFhirContext, IBase theLeftValue, IBase theRightValue) {
return myAlgorithm.match(theFhirContext, theLeftValue, theRightValue, myExact, myIdentifierSystem);
}
}

View File

@ -0,0 +1,76 @@
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.api.EmpiMatchEvaluation;
import ca.uhn.fhir.empi.rules.similarity.EmpiSimilarityEnum;
import ca.uhn.fhir.model.api.IModelJson;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hl7.fhir.instance.model.api.IBase;
import javax.annotation.Nullable;
public class EmpiSimilarityJson implements IModelJson {
@JsonProperty(value = "algorithm", required = true)
EmpiSimilarityEnum myAlgorithm;
@JsonProperty(value = "matchThreshold", required = true)
Double myMatchThreshold;
/**
* For String value types, should the values be normalized (case, accents) before they are compared
*/
@JsonProperty(value = "exact")
boolean myExact;
public EmpiSimilarityEnum getAlgorithm() {
return myAlgorithm;
}
public EmpiSimilarityJson setAlgorithm(EmpiSimilarityEnum theAlgorithm) {
myAlgorithm = theAlgorithm;
return this;
}
@Nullable
public Double getMatchThreshold() {
return myMatchThreshold;
}
public EmpiSimilarityJson setMatchThreshold(double theMatchThreshold) {
myMatchThreshold = theMatchThreshold;
return this;
}
public boolean getExact() {
return myExact;
}
public EmpiSimilarityJson setExact(boolean theExact) {
myExact = theExact;
return this;
}
public EmpiMatchEvaluation match(FhirContext theFhirContext, IBase theLeftValue, IBase theRightValue) {
return myAlgorithm.match(theFhirContext, theLeftValue, theRightValue, myExact, myMatchThreshold);
}
}

View File

@ -54,10 +54,10 @@ public class VectorMatchResultMap {
}
private EmpiMatchResultEnum computeMatchResult(Long theVector) {
if (myMatchVectors.stream().anyMatch(v -> (v & theVector) != 0)) {
if (myMatchVectors.stream().anyMatch(v -> (v & theVector) == v)) {
return EmpiMatchResultEnum.MATCH;
}
if (myPossibleMatchVectors.stream().anyMatch(v -> (v & theVector) != 0)) {
if (myPossibleMatchVectors.stream().anyMatch(v -> (v & theVector) == v)) {
return EmpiMatchResultEnum.POSSIBLE_MATCH;
}
return EmpiMatchResultEnum.NO_MATCH;

View File

@ -0,0 +1,69 @@
package ca.uhn.fhir.empi.rules.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.context.phonetic.PhoneticEncoderEnum;
import org.hl7.fhir.instance.model.api.IBase;
/**
* Enum for holding all the known FHIR Element matchers that we support in HAPI. The string matchers first
* encode the string using an Apache Encoder before comparing them. https://commons.apache.org/proper/commons-codec/userguide.html
*/
public enum EmpiMatcherEnum {
CAVERPHONE1(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.CAVERPHONE1))),
CAVERPHONE2(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.CAVERPHONE2))),
COLOGNE(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.COLOGNE))),
DOUBLE_METAPHONE(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.DOUBLE_METAPHONE))),
MATCH_RATING_APPROACH(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.MATCH_RATING_APPROACH))),
METAPHONE(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.METAPHONE))),
NYSIIS(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.NYSIIS))),
REFINED_SOUNDEX(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.REFINED_SOUNDEX))),
SOUNDEX(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.SOUNDEX))),
STRING(new HapiStringMatcher()),
SUBSTRING(new HapiStringMatcher(new SubstringStringMatcher())),
DATE(new HapiDateMatcher()),
NAME_ANY_ORDER(new NameMatcher(EmpiPersonNameMatchModeEnum.ANY_ORDER)),
NAME_FIRST_AND_LAST(new NameMatcher(EmpiPersonNameMatchModeEnum.FIRST_AND_LAST)),
IDENTIFIER(new IdentifierMatcher());
private final IEmpiFieldMatcher myEmpiFieldMatcher;
EmpiMatcherEnum(IEmpiFieldMatcher theEmpiFieldMatcher) {
myEmpiFieldMatcher = theEmpiFieldMatcher;
}
/**
* Determines whether two FHIR elements match according using the provided IEmpiFieldMatcher
* @param theFhirContext
* @param theLeftBase left FHIR element to compare
* @param theRightBase right FHIR element to compare
* @param theExact used by String matchers. If "false" then the string is normalized (case, accents) before comparing. If "true" then an exact string comparison is performed.
* @param theIdentifierSystem used optionally by the IDENTIFIER matcher, when present, only matches the identifiers if they belong to this system.
* @return
*/
public boolean match(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem) {
return myEmpiFieldMatcher.matches(theFhirContext, theLeftBase, theRightBase, theExact, theIdentifierSystem);
}
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.empi.rules.metric.matcher;
package ca.uhn.fhir.empi.rules.matcher;
/*-
* #%L
@ -28,7 +28,7 @@ public class HapiDateMatcher implements IEmpiFieldMatcher {
private final HapiDateMatcherR4 myHapiDateMatcherR4 = new HapiDateMatcherR4();
@Override
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) {
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem) {
switch (theFhirContext.getVersion().getVersion()) {
case DSTU3:
return myHapiDateMatcherDstu3.match(theLeftBase, theRightBase);

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.empi.rules.metric.matcher;
package ca.uhn.fhir.empi.rules.matcher;
/*-
* #%L

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.empi.rules.metric.matcher;
package ca.uhn.fhir.empi.rules.matcher;
/*-
* #%L
@ -39,7 +39,7 @@ public class HapiStringMatcher extends BaseHapiStringMetric implements IEmpiFiel
}
@Override
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) {
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem) {
if (theLeftBase instanceof IPrimitiveType && theRightBase instanceof IPrimitiveType) {
String leftString = extractString((IPrimitiveType<?>) theLeftBase, theExact);
String rightString = extractString((IPrimitiveType<?>) theRightBase, theExact);
@ -48,5 +48,4 @@ public class HapiStringMatcher extends BaseHapiStringMetric implements IEmpiFiel
}
return false;
}
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.empi.rules.metric.matcher;
package ca.uhn.fhir.empi.rules.matcher;
/*-
* #%L
@ -21,12 +21,11 @@ package ca.uhn.fhir.empi.rules.metric.matcher;
*/
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);
public interface IEmpiFieldMatcher {
boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem);
}

View File

@ -0,0 +1,46 @@
package ca.uhn.fhir.empi.rules.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.util.CanonicalIdentifier;
import ca.uhn.fhir.empi.util.IdentifierUtil;
import org.hl7.fhir.instance.model.api.IBase;
public class IdentifierMatcher implements IEmpiFieldMatcher {
/**
*
* @return true if the two fhir identifiers are the same. If @param theIdentifierSystem is not null, then the
* matcher only returns true if the identifier systems also match this system.
* @throws UnsupportedOperationException if either Base is not an Identifier instance
*/
@Override
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem) {
CanonicalIdentifier left = IdentifierUtil.identifierDtFromIdentifier(theLeftBase);
if (theIdentifierSystem != null) {
if (!theIdentifierSystem.equals(left.getSystemElement().getValueAsString())) {
return false;
}
}
CanonicalIdentifier right = IdentifierUtil.identifierDtFromIdentifier(theRightBase);
return left.equals(right);
}
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.empi.rules.metric.matcher;
package ca.uhn.fhir.empi.rules.matcher;
/*-
* #%L
@ -41,7 +41,7 @@ public class NameMatcher implements IEmpiFieldMatcher {
}
@Override
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) {
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem) {
String leftFamilyName = NameUtil.extractFamilyName(theFhirContext, theLeftBase);
String rightFamilyName = NameUtil.extractFamilyName(theFhirContext, theRightBase);
if (StringUtils.isEmpty(leftFamilyName) || StringUtils.isEmpty(rightFamilyName)) {

View File

@ -1,102 +0,0 @@
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.context.phonetic.PhoneticEncoderEnum;
import ca.uhn.fhir.empi.api.EmpiMatchEvaluation;
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.NameMatcher;
import ca.uhn.fhir.empi.rules.metric.matcher.PhoneticEncoderMatcher;
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.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 {
CAVERPHONE1(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.CAVERPHONE1))),
CAVERPHONE2(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.CAVERPHONE2))),
COLOGNE(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.COLOGNE))),
DOUBLE_METAPHONE(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.DOUBLE_METAPHONE))),
MATCH_RATING_APPROACH(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.MATCH_RATING_APPROACH))),
METAPHONE(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.METAPHONE))),
NYSIIS(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.NYSIIS))),
REFINED_SOUNDEX(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.REFINED_SOUNDEX))),
SOUNDEX(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.SOUNDEX))),
STRING(new HapiStringMatcher()),
SUBSTRING(new HapiStringMatcher(new SubstringStringMatcher())),
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 EmpiMatchEvaluation match(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, @Nullable Double theThreshold) {
if (isSimilarity()) {
return matchBySimilarity((IEmpiFieldSimilarity) myEmpiFieldMetric, theFhirContext, theLeftBase, theRightBase, theExact, theThreshold);
} else {
return matchByMatcher((IEmpiFieldMatcher) myEmpiFieldMetric, theFhirContext, theLeftBase, theRightBase, theExact);
}
}
private EmpiMatchEvaluation matchBySimilarity(IEmpiFieldSimilarity theSimilarity, FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, Double theThreshold) {
double similarityResult = theSimilarity.similarity(theFhirContext, theLeftBase, theRightBase, theExact);
return new EmpiMatchEvaluation(similarityResult >= theThreshold, similarityResult);
}
private EmpiMatchEvaluation matchByMatcher(IEmpiFieldMatcher theMatcher, FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) {
boolean matcherResult = theMatcher.matches(theFhirContext, theLeftBase, theRightBase, theExact);
return new EmpiMatchEvaluation(matcherResult, matcherResult ? 1.0 : 0.0);
}
public boolean isSimilarity() {
return myEmpiFieldMetric instanceof IEmpiFieldSimilarity;
}
}

View File

@ -1,24 +0,0 @@
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,55 @@
package ca.uhn.fhir.empi.rules.similarity;
/*-
* #%L
* HAPI FHIR - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.api.EmpiMatchEvaluation;
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;
import javax.annotation.Nullable;
public enum EmpiSimilarityEnum {
JARO_WINKLER(new HapiStringSimilarity(new JaroWinkler())),
COSINE(new HapiStringSimilarity(new Cosine())),
JACCARD(new HapiStringSimilarity(new Jaccard())),
LEVENSCHTEIN(new HapiStringSimilarity(new NormalizedLevenshtein())),
SORENSEN_DICE(new HapiStringSimilarity(new SorensenDice()));
private final IEmpiFieldSimilarity myEmpiFieldSimilarity;
EmpiSimilarityEnum(IEmpiFieldSimilarity theEmpiFieldSimilarity) {
myEmpiFieldSimilarity = theEmpiFieldSimilarity;
}
public EmpiMatchEvaluation match(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, @Nullable Double theThreshold) {
return matchBySimilarity(myEmpiFieldSimilarity, theFhirContext, theLeftBase, theRightBase, theExact, theThreshold);
}
private EmpiMatchEvaluation matchBySimilarity(IEmpiFieldSimilarity theSimilarity, FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, Double theThreshold) {
double similarityResult = theSimilarity.similarity(theFhirContext, theLeftBase, theRightBase, theExact);
return new EmpiMatchEvaluation(similarityResult >= theThreshold, similarityResult);
}
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.empi.rules.metric.similarity;
package ca.uhn.fhir.empi.rules.similarity;
/*-
* #%L
@ -21,7 +21,7 @@ package ca.uhn.fhir.empi.rules.metric.similarity;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.empi.rules.metric.matcher.BaseHapiStringMetric;
import ca.uhn.fhir.empi.rules.matcher.BaseHapiStringMetric;
import info.debatty.java.stringsimilarity.interfaces.NormalizedStringSimilarity;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IPrimitiveType;

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.empi.rules.metric.similarity;
package ca.uhn.fhir.empi.rules.similarity;
/*-
* #%L
@ -21,12 +21,11 @@ package ca.uhn.fhir.empi.rules.metric.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 extends IEmpiFieldMetric {
public interface IEmpiFieldSimilarity {
double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact);
}

View File

@ -82,7 +82,7 @@ public class EmpiResourceFieldMatcher {
}
private EmpiMatchEvaluation match(IBase theLeftValue, IBase theRightValue) {
return myEmpiFieldMatchJson.getMetric().match(myFhirContext, theLeftValue, theRightValue, myEmpiFieldMatchJson.getExact(), myEmpiFieldMatchJson.getMatchThreshold());
return myEmpiFieldMatchJson.match(myFhirContext, theLeftValue, theRightValue);
}
private void validate(IBaseResource theResource) {

View File

@ -0,0 +1,98 @@
package ca.uhn.fhir.empi.util;
/*-
* #%L
* HAPI FHIR - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.model.api.IElement;
import ca.uhn.fhir.model.base.composite.BaseIdentifierDt;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.model.primitive.UriDt;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import java.util.List;
/**
* Version independent identifier
*/
public class CanonicalIdentifier extends BaseIdentifierDt {
UriDt mySystem;
StringDt myValue;
@Override
public UriDt getSystemElement() {
return mySystem;
}
@Override
public StringDt getValueElement() {
return myValue;
}
@Override
public CanonicalIdentifier setSystem(String theUri) {
mySystem = new UriDt((theUri));
return this;
}
@Override
public CanonicalIdentifier setValue(String theString) {
myValue = new StringDt(theString);
return this;
}
@Override
public <T extends IElement> List<T> getAllPopulatedChildElementsOfType(Class<T> theType) {
throw new UnsupportedOperationException();
}
@Override
public boolean isEmpty() {
if (mySystem != null && !mySystem.isEmpty()) {
return false;
}
if (myValue != null && !myValue.isEmpty()) {
return false;
}
return true;
}
@Override
public boolean equals(Object theO) {
if (this == theO) return true;
if (theO == null || getClass() != theO.getClass()) return false;
CanonicalIdentifier that = (CanonicalIdentifier) theO;
return new EqualsBuilder()
.append(mySystem, that.mySystem)
.append(myValue, that.myValue)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37)
.append(mySystem)
.append(myValue)
.toHashCode();
}
}

View File

@ -0,0 +1,45 @@
package ca.uhn.fhir.empi.util;
/*-
* #%L
* HAPI FHIR - Enterprise Master Patient Index
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.instance.model.api.IBase;
public class IdentifierUtil {
public static CanonicalIdentifier identifierDtFromIdentifier(IBase theIdentifier) {
CanonicalIdentifier retval = new CanonicalIdentifier();
// TODO add other fields like "use" etc
if (theIdentifier instanceof org.hl7.fhir.dstu3.model.Identifier) {
org.hl7.fhir.dstu3.model.Identifier ident = (org.hl7.fhir.dstu3.model.Identifier) theIdentifier;
retval.setSystem(ident.getSystem()).setValue(ident.getValue());
} else if (theIdentifier instanceof org.hl7.fhir.r4.model.Identifier) {
org.hl7.fhir.r4.model.Identifier ident = (org.hl7.fhir.r4.model.Identifier) theIdentifier;
retval.setSystem(ident.getSystem()).setValue(ident.getValue());
} else if (theIdentifier instanceof org.hl7.fhir.r5.model.Identifier) {
org.hl7.fhir.r5.model.Identifier ident = (org.hl7.fhir.r5.model.Identifier) theIdentifier;
retval.setSystem(ident.getSystem()).setValue(ident.getValue());
} else {
throw new InternalErrorException("Expected 'Identifier' type but was '" + theIdentifier.getClass().getName() + "'");
}
return retval;
}
}

View File

@ -22,6 +22,7 @@ package ca.uhn.fhir.rest.server;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class TransactionLogMessages {
private List<String> myMessages;
@ -31,6 +32,10 @@ public class TransactionLogMessages {
myTransactionGuid = theTransactionGuid;
}
public static TransactionLogMessages createNew() {
return new TransactionLogMessages(UUID.randomUUID().toString());
}
public static TransactionLogMessages createFromTransactionGuid(String theTransactionGuid) {
return new TransactionLogMessages(theTransactionGuid);
}

View File

@ -42,17 +42,7 @@ public class EmpiRuleValidatorTest extends BaseR4Test {
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"));
assertThat(e.getMessage(), is("MatchField given-name similarity COSINE requires a matchThreshold"));
}
}

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.empi.rules.json;
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.similarity.EmpiSimilarityEnum;
import ca.uhn.fhir.empi.rules.svc.BaseEmpiRulesR4Test;
import ca.uhn.fhir.util.JsonUtil;
import org.junit.jupiter.api.BeforeEach;
@ -48,7 +48,7 @@ public class EmpiRulesJsonR4Test extends BaseEmpiRulesR4Test {
assertEquals(EmpiMatchResultEnum.MATCH, rulesDeser.getMatchResult(myBothNameFields));
EmpiFieldMatchJson second = rulesDeser.get(1);
assertEquals("name.family", second.getResourcePath());
assertEquals(EmpiMetricEnum.JARO_WINKLER, second.getMetric());
assertEquals(EmpiSimilarityEnum.JARO_WINKLER, second.getSimilarity().getAlgorithm());
}
@Test
@ -60,11 +60,11 @@ public class EmpiRulesJsonR4Test extends BaseEmpiRulesR4Test {
public void getVector() {
VectorMatchResultMap vectorMatchResultMap = myRules.getVectorMatchResultMapForUnitTest();
assertEquals(1, vectorMatchResultMap.getVector(PATIENT_GIVEN));
assertEquals(2, vectorMatchResultMap.getVector(PATIENT_LAST));
assertEquals(3, vectorMatchResultMap.getVector(String.join(",", PATIENT_GIVEN, PATIENT_LAST)));
assertEquals(3, vectorMatchResultMap.getVector(String.join(", ", PATIENT_GIVEN, PATIENT_LAST)));
assertEquals(3, vectorMatchResultMap.getVector(String.join(", ", PATIENT_GIVEN, PATIENT_LAST)));
assertEquals(3, vectorMatchResultMap.getVector(String.join(", \n ", PATIENT_GIVEN, PATIENT_LAST)));
assertEquals(2, vectorMatchResultMap.getVector(PATIENT_FAMILY));
assertEquals(3, vectorMatchResultMap.getVector(String.join(",", PATIENT_GIVEN, PATIENT_FAMILY)));
assertEquals(3, vectorMatchResultMap.getVector(String.join(", ", PATIENT_GIVEN, PATIENT_FAMILY)));
assertEquals(3, vectorMatchResultMap.getVector(String.join(", ", PATIENT_GIVEN, PATIENT_FAMILY)));
assertEquals(3, vectorMatchResultMap.getVector(String.join(", \n ", PATIENT_GIVEN, PATIENT_FAMILY)));
try {
vectorMatchResultMap.getVector("bad");
fail();

View File

@ -1,7 +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 ca.uhn.fhir.empi.rules.matcher.EmpiMatcherEnum;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -27,9 +27,10 @@ public class VectorMatchResultMapTest {
@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));
EmpiMatcherJson matcherJson = new EmpiMatcherJson().setAlgorithm(EmpiMatcherEnum.STRING);
empiRulesJson.addMatchField(new EmpiFieldMatchJson().setName("given").setResourceType("Patient").setResourcePath("name.given").setMatcher(matcherJson));
empiRulesJson.addMatchField(new EmpiFieldMatchJson().setName("family").setResourceType("Patient").setResourcePath("name.family").setMatcher(matcherJson));
empiRulesJson.addMatchField(new EmpiFieldMatchJson().setName("prefix").setResourceType("Patient").setResourcePath("name.prefix").setMatcher(matcherJson));
empiRulesJson.putMatchResult("given,family", EmpiMatchResultEnum.MATCH);
empiRulesJson.putMatchResult("given", EmpiMatchResultEnum.POSSIBLE_MATCH);

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.empi.rules.metric.matcher;
package ca.uhn.fhir.empi.rules.matcher;
import ca.uhn.fhir.context.FhirContext;

View File

@ -1,6 +1,5 @@
package ca.uhn.fhir.empi.rules.metric.matcher;
package ca.uhn.fhir.empi.rules.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;
@ -44,7 +43,7 @@ public class DateMatcherR4Test extends BaseMatcherR4Test {
}
private boolean dateMatch(Date theDate, Date theSameMonth, TemporalPrecisionEnum theTheDay) {
return EmpiMetricEnum.DATE.match(ourFhirContext, new DateType(theDate, theTheDay), new DateType(theSameMonth, theTheDay), true);
return EmpiMatcherEnum.DATE.match(ourFhirContext, new DateType(theDate, theTheDay), new DateType(theSameMonth, theTheDay), true, null);
}
@Test
@ -84,6 +83,6 @@ public class DateMatcherR4Test extends BaseMatcherR4Test {
}
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);
return EmpiMatcherEnum.DATE.match(ourFhirContext, new DateTimeType(theDate, theTheDay), new DateTimeType(theSameSecond, theTheDay2), true, null);
}
}

View File

@ -0,0 +1,70 @@
package ca.uhn.fhir.empi.rules.matcher;
import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson;
import ca.uhn.fhir.empi.rules.json.EmpiMatcherJson;
import org.hl7.fhir.r4.model.Identifier;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class IdentifierMatcherR4Test extends BaseMatcherR4Test {
public static final String MATCHING_SYSTEM = "http://match";
public static final String OTHER_SYSTEM = "http://other";
private static final String MATCHING_VALUE = "matchme";
private static final String OTHER_VALUE = "strange";
@Test
public void testIdentifierMatch() {
Identifier left = new Identifier().setSystem(MATCHING_SYSTEM).setValue(MATCHING_VALUE);
Identifier right = new Identifier().setSystem(MATCHING_SYSTEM).setValue(MATCHING_VALUE);
EmpiMatcherJson matcher = new EmpiMatcherJson().setAlgorithm(EmpiMatcherEnum.IDENTIFIER);
EmpiFieldMatchJson fieldMatch = new EmpiFieldMatchJson().setMatcher(matcher);
assertTrue(fieldMatch.match(ourFhirContext, left, right).match);
}
@Test
public void testIdentifierNoMatch() {
Identifier left = new Identifier().setSystem(MATCHING_SYSTEM).setValue(MATCHING_VALUE);
Identifier rightWrongSystem = new Identifier().setSystem(OTHER_SYSTEM).setValue(MATCHING_VALUE);
Identifier rightWrongValue = new Identifier().setSystem(MATCHING_SYSTEM).setValue(OTHER_VALUE);
Identifier rightNoSystem = new Identifier().setValue(MATCHING_VALUE);
Identifier rightNoValue = new Identifier().setSystem(MATCHING_SYSTEM);
EmpiMatcherJson matcher = new EmpiMatcherJson().setAlgorithm(EmpiMatcherEnum.IDENTIFIER);
EmpiFieldMatchJson fieldMatch = new EmpiFieldMatchJson().setMatcher(matcher);
assertFalse(fieldMatch.match(ourFhirContext, left, rightWrongSystem).match);
assertFalse(fieldMatch.match(ourFhirContext, left, rightWrongValue).match);
assertFalse(fieldMatch.match(ourFhirContext, left, rightNoSystem).match);
assertFalse(fieldMatch.match(ourFhirContext, left, rightNoValue).match);
assertFalse(fieldMatch.match(ourFhirContext, rightWrongSystem, left).match);
assertFalse(fieldMatch.match(ourFhirContext, rightWrongValue, left).match);
assertFalse(fieldMatch.match(ourFhirContext, rightNoSystem, left).match);
assertFalse(fieldMatch.match(ourFhirContext, rightNoValue, left).match);
}
@Test
public void testIdentifierNamedSystemMatch() {
Identifier left = new Identifier().setSystem(MATCHING_SYSTEM).setValue(MATCHING_VALUE);
Identifier right = new Identifier().setSystem(MATCHING_SYSTEM).setValue(MATCHING_VALUE);
EmpiMatcherJson matcher = new EmpiMatcherJson().setAlgorithm(EmpiMatcherEnum.IDENTIFIER).setIdentifierSystem(MATCHING_SYSTEM);
EmpiFieldMatchJson fieldMatch = new EmpiFieldMatchJson().setMatcher(matcher);
assertTrue(fieldMatch.match(ourFhirContext, left, right).match);
}
@Test
public void testIdentifierSystemNoMatch() {
Identifier left = new Identifier().setSystem(OTHER_SYSTEM).setValue(MATCHING_VALUE);
Identifier right = new Identifier().setSystem(OTHER_SYSTEM).setValue(MATCHING_VALUE);
EmpiMatcherJson matcher = new EmpiMatcherJson().setAlgorithm(EmpiMatcherEnum.IDENTIFIER).setIdentifierSystem(MATCHING_SYSTEM);
EmpiFieldMatchJson fieldMatch = new EmpiFieldMatchJson().setMatcher(matcher);
assertFalse(fieldMatch.match(ourFhirContext, left, right).match);
}
}

View File

@ -0,0 +1,154 @@
package ca.uhn.fhir.empi.rules.matcher;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Enumeration;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class StringMatcherR4Test extends BaseMatcherR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(StringMatcherR4Test.class);
public static final String LEFT = "namadega";
public static final String RIGHT = "namaedga";
@Test
public void testNamadega() {
assertTrue(match(EmpiMatcherEnum.COLOGNE, LEFT, RIGHT));
assertTrue(match(EmpiMatcherEnum.DOUBLE_METAPHONE, LEFT, RIGHT));
assertTrue(match(EmpiMatcherEnum.MATCH_RATING_APPROACH, LEFT, RIGHT));
assertTrue(match(EmpiMatcherEnum.METAPHONE, LEFT, RIGHT));
assertTrue(match(EmpiMatcherEnum.SOUNDEX, LEFT, RIGHT));
assertTrue(match(EmpiMatcherEnum.METAPHONE, LEFT, RIGHT));
assertFalse(match(EmpiMatcherEnum.CAVERPHONE1, LEFT, RIGHT));
assertFalse(match(EmpiMatcherEnum.CAVERPHONE2, LEFT, RIGHT));
assertFalse(match(EmpiMatcherEnum.NYSIIS, LEFT, RIGHT));
assertFalse(match(EmpiMatcherEnum.REFINED_SOUNDEX, LEFT, RIGHT));
assertFalse(match(EmpiMatcherEnum.STRING, LEFT, RIGHT));
assertFalse(match(EmpiMatcherEnum.SUBSTRING, LEFT, RIGHT));
}
@Test
public void testMetaphone() {
assertTrue(match(EmpiMatcherEnum.METAPHONE, "Durie", "dury"));
assertTrue(match(EmpiMatcherEnum.METAPHONE, "Balo", "ballo"));
assertTrue(match(EmpiMatcherEnum.METAPHONE, "Hans Peter", "Hanspeter"));
assertTrue(match(EmpiMatcherEnum.METAPHONE, "Lawson", "Law son"));
assertFalse(match(EmpiMatcherEnum.METAPHONE, "Allsop", "Allsob"));
assertFalse(match(EmpiMatcherEnum.METAPHONE, "Gevne", "Geve"));
assertFalse(match(EmpiMatcherEnum.METAPHONE, "Bruce", "Bruch"));
assertFalse(match(EmpiMatcherEnum.METAPHONE, "Smith", "Schmidt"));
assertFalse(match(EmpiMatcherEnum.METAPHONE, "Jyothi", "Jyoti"));
}
@Test
public void testDoubleMetaphone() {
assertTrue(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Durie", "dury"));
assertTrue(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Balo", "ballo"));
assertTrue(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Hans Peter", "Hanspeter"));
assertTrue(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Lawson", "Law son"));
assertTrue(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Allsop", "Allsob"));
assertFalse(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Gevne", "Geve"));
assertFalse(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Bruce", "Bruch"));
assertFalse(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Smith", "Schmidt"));
assertFalse(match(EmpiMatcherEnum.DOUBLE_METAPHONE, "Jyothi", "Jyoti"));
}
@Test
public void testNormalizeCase() {
assertTrue(match(EmpiMatcherEnum.STRING, "joe", "JoE"));
assertTrue(match(EmpiMatcherEnum.STRING, "MCTAVISH", "McTavish"));
assertFalse(match(EmpiMatcherEnum.STRING, "joey", "joe"));
assertFalse(match(EmpiMatcherEnum.STRING, "joe", "joey"));
}
@Test
public void testExactString() {
assertTrue(EmpiMatcherEnum.STRING.match(ourFhirContext, new StringType("Jilly"), new StringType("Jilly"), true, null));
assertFalse(EmpiMatcherEnum.STRING.match(ourFhirContext, new StringType("MCTAVISH"), new StringType("McTavish"), true, null));
assertFalse(EmpiMatcherEnum.STRING.match(ourFhirContext, new StringType("Durie"), new StringType("dury"), true, null));
}
@Test
public void testExactBoolean() {
assertTrue(EmpiMatcherEnum.STRING.match(ourFhirContext, new BooleanType(true), new BooleanType(true), true, null));
assertFalse(EmpiMatcherEnum.STRING.match(ourFhirContext, new BooleanType(true), new BooleanType(false), true, null));
assertFalse(EmpiMatcherEnum.STRING.match(ourFhirContext, new BooleanType(false), new BooleanType(true), true, null));
}
@Test
public void testExactDateString() {
assertTrue(EmpiMatcherEnum.STRING.match(ourFhirContext, new DateType("1965-08-09"), new DateType("1965-08-09"), true, null));
assertFalse(EmpiMatcherEnum.STRING.match(ourFhirContext, new DateType("1965-08-09"), new DateType("1965-09-08"), true, null));
}
@Test
public void testExactGender() {
Enumeration<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(EmpiMatcherEnum.STRING.match(ourFhirContext, male, male, true, null));
assertFalse(EmpiMatcherEnum.STRING.match(ourFhirContext, male, female, true, null));
}
@Test
public void testSoundex() {
assertTrue(match(EmpiMatcherEnum.SOUNDEX, "Gail", "Gale"));
assertTrue(match(EmpiMatcherEnum.SOUNDEX, "John", "Jon"));
assertTrue(match(EmpiMatcherEnum.SOUNDEX, "Thom", "Tom"));
assertFalse(match(EmpiMatcherEnum.SOUNDEX, "Fred", "Frank"));
assertFalse(match(EmpiMatcherEnum.SOUNDEX, "Thomas", "Tom"));
}
@Test
public void testCaverphone1() {
assertTrue(match(EmpiMatcherEnum.CAVERPHONE1, "Gail", "Gael"));
assertTrue(match(EmpiMatcherEnum.CAVERPHONE1, "John", "Jon"));
assertFalse(match(EmpiMatcherEnum.CAVERPHONE1, "Gail", "Gale"));
assertFalse(match(EmpiMatcherEnum.CAVERPHONE1, "Fred", "Frank"));
assertFalse(match(EmpiMatcherEnum.CAVERPHONE1, "Thomas", "Tom"));
}
@Test
public void testCaverphone2() {
assertTrue(match(EmpiMatcherEnum.CAVERPHONE2, "Gail", "Gael"));
assertTrue(match(EmpiMatcherEnum.CAVERPHONE2, "John", "Jon"));
assertTrue(match(EmpiMatcherEnum.CAVERPHONE2, "Gail", "Gale"));
assertFalse(match(EmpiMatcherEnum.CAVERPHONE2, "Fred", "Frank"));
assertFalse(match(EmpiMatcherEnum.CAVERPHONE2, "Thomas", "Tom"));
}
@Test
public void testNormalizeSubstring() {
assertTrue(match(EmpiMatcherEnum.SUBSTRING, "BILLY", "Bill"));
assertTrue(match(EmpiMatcherEnum.SUBSTRING, "Bill", "Billy"));
assertTrue(match(EmpiMatcherEnum.SUBSTRING, "FRED", "Frederik"));
assertFalse(match(EmpiMatcherEnum.SUBSTRING, "Fred", "Friederik"));
}
private boolean match(EmpiMatcherEnum theMatcher, String theLeft, String theRight) {
return theMatcher.match(ourFhirContext, new StringType(theLeft), new StringType(theRight), false, null);
}
}

View File

@ -0,0 +1,38 @@
package ca.uhn.fhir.empi.rules.matcher;
import ca.uhn.fhir.empi.rules.similarity.HapiStringSimilarity;
import ca.uhn.fhir.empi.rules.similarity.IEmpiFieldSimilarity;
import info.debatty.java.stringsimilarity.Cosine;
import info.debatty.java.stringsimilarity.Jaccard;
import info.debatty.java.stringsimilarity.JaroWinkler;
import info.debatty.java.stringsimilarity.NormalizedLevenshtein;
import info.debatty.java.stringsimilarity.SorensenDice;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class StringSimilarityR4Test extends BaseMatcherR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(StringSimilarityR4Test.class);
public static final String LEFT = "somon";
public static final String RIGHT = "slomon";
private static final HapiStringSimilarity JARO_WINKLER = new HapiStringSimilarity(new JaroWinkler());
private static final HapiStringSimilarity COSINE = new HapiStringSimilarity(new Cosine());
private static final HapiStringSimilarity JACCARD = new HapiStringSimilarity(new Jaccard());
private static final HapiStringSimilarity LEVENSCHTEIN = new HapiStringSimilarity(new NormalizedLevenshtein());
private static final HapiStringSimilarity SORENSEN_DICE = new HapiStringSimilarity(new SorensenDice());
@Test
public void testSlomon() {
ourLog.info("" + similarity(JARO_WINKLER, LEFT, RIGHT));
ourLog.info("" + similarity(COSINE, LEFT, RIGHT));
ourLog.info("" + similarity(JACCARD, LEFT, RIGHT));
ourLog.info("" + similarity(LEVENSCHTEIN, LEFT, RIGHT));
ourLog.info("" + similarity(SORENSEN_DICE, LEFT, RIGHT));
}
private double similarity(IEmpiFieldSimilarity theSimilarity, String theLeft, String theRight) {
return theSimilarity.similarity(ourFhirContext, new StringType(theLeft), new StringType(theRight), false);
}
}

View File

@ -1,132 +0,0 @@
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.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.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

@ -6,13 +6,14 @@ 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 ca.uhn.fhir.empi.rules.json.EmpiSimilarityJson;
import ca.uhn.fhir.empi.rules.similarity.EmpiSimilarityEnum;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.BeforeEach;
public abstract class BaseEmpiRulesR4Test extends BaseR4Test {
public static final String PATIENT_GIVEN = "patient-given";
public static final String PATIENT_LAST = "patient-last";
public static final String PATIENT_FAMILY = "patient-last";
public static final double NAME_THRESHOLD = 0.8;
protected EmpiFieldMatchJson myGivenNameMatchField;
@ -24,9 +25,8 @@ public abstract class BaseEmpiRulesR4Test extends BaseR4Test {
.setName(PATIENT_GIVEN)
.setResourceType("Patient")
.setResourcePath("name.given")
.setMetric(EmpiMetricEnum.COSINE)
.setMatchThreshold(NAME_THRESHOLD);
myBothNameFields = String.join(",", PATIENT_GIVEN, PATIENT_LAST);
.setSimilarity(new EmpiSimilarityJson().setAlgorithm(EmpiSimilarityEnum.COSINE).setMatchThreshold(NAME_THRESHOLD));
myBothNameFields = String.join(",", PATIENT_GIVEN, PATIENT_FAMILY);
}
protected EmpiRulesJson buildActiveBirthdateIdRules() {
@ -44,11 +44,10 @@ public abstract class BaseEmpiRulesR4Test extends BaseR4Test {
EmpiFieldMatchJson lastNameMatchField = new EmpiFieldMatchJson()
.setName(PATIENT_LAST)
.setName(PATIENT_FAMILY)
.setResourceType("Patient")
.setResourcePath("name.family")
.setMetric(EmpiMetricEnum.JARO_WINKLER)
.setMatchThreshold(NAME_THRESHOLD);
.setSimilarity(new EmpiSimilarityJson().setAlgorithm(EmpiSimilarityEnum.JARO_WINKLER).setMatchThreshold(NAME_THRESHOLD));
EmpiRulesJson retval = new EmpiRulesJson();
retval.setVersion("test version");

View File

@ -3,8 +3,9 @@ package ca.uhn.fhir.empi.rules.svc;
import ca.uhn.fhir.empi.BaseR4Test;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson;
import ca.uhn.fhir.empi.rules.json.EmpiMatcherJson;
import ca.uhn.fhir.empi.rules.json.EmpiRulesJson;
import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum;
import ca.uhn.fhir.empi.rules.matcher.EmpiMatcherEnum;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.BeforeAll;
@ -12,7 +13,7 @@ import org.junit.jupiter.api.Test;
public class CustomResourceMatcherR4Test extends BaseR4Test {
public static final String FIELD_EXACT_MATCH_NAME = EmpiMetricEnum.NAME_ANY_ORDER.name();
public static final String FIELD_EXACT_MATCH_NAME = EmpiMatcherEnum.NAME_ANY_ORDER.name();
private static Patient ourJohnHenry;
private static Patient ourJohnHENRY;
private static Patient ourJaneHenry;
@ -24,7 +25,7 @@ public class CustomResourceMatcherR4Test extends BaseR4Test {
@Test
public void testExactNameAnyOrder() {
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_ANY_ORDER, true));
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMatcherEnum.NAME_ANY_ORDER, true));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
@ -37,7 +38,7 @@ public class CustomResourceMatcherR4Test extends BaseR4Test {
@Test
public void testNormalizedNameAnyOrder() {
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_ANY_ORDER, false));
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMatcherEnum.NAME_ANY_ORDER, false));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
@ -50,7 +51,7 @@ public class CustomResourceMatcherR4Test extends BaseR4Test {
@Test
public void testExactNameFirstAndLast() {
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_FIRST_AND_LAST, true));
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMatcherEnum.NAME_FIRST_AND_LAST, true));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatchResult(EmpiMatchResultEnum.MATCH, 1L, 1.0, false, false, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
@ -64,7 +65,7 @@ public class CustomResourceMatcherR4Test extends BaseR4Test {
@Test
public void testNormalizedNameFirstAndLast() {
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_FIRST_AND_LAST, false));
EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMatcherEnum.NAME_FIRST_AND_LAST, false));
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
assertMatch(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
@ -75,13 +76,13 @@ public class CustomResourceMatcherR4Test extends BaseR4Test {
assertMatch(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry));
}
private EmpiRulesJson buildNameRules(EmpiMetricEnum theExactNameAnyOrder, boolean theExact) {
private EmpiRulesJson buildNameRules(EmpiMatcherEnum theAlgorithm, boolean theExact) {
EmpiMatcherJson matcherJson = new EmpiMatcherJson().setAlgorithm(theAlgorithm).setExact(theExact);
EmpiFieldMatchJson nameAnyOrderFieldMatch = new EmpiFieldMatchJson()
.setName(FIELD_EXACT_MATCH_NAME)
.setResourceType("Patient")
.setResourcePath("name")
.setMetric(theExactNameAnyOrder)
.setExact(theExact);
.setMatcher(matcherJson);
EmpiRulesJson retval = new EmpiRulesJson();
retval.addMatchField(nameAnyOrderFieldMatch);

View File

@ -1,7 +1,8 @@
package ca.uhn.fhir.empi.rules.svc;
import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson;
import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum;
import ca.uhn.fhir.empi.rules.json.EmpiSimilarityJson;
import ca.uhn.fhir.empi.rules.similarity.EmpiSimilarityEnum;
import ca.uhn.fhir.parser.DataFormatException;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Patient;
@ -65,8 +66,7 @@ public class EmpiResourceFieldMatcherR4Test extends BaseEmpiRulesR4Test {
.setName("patient-foo")
.setResourceType("Patient")
.setResourcePath("foo")
.setMetric(EmpiMetricEnum.COSINE)
.setMatchThreshold(NAME_THRESHOLD);
.setSimilarity(new EmpiSimilarityJson().setAlgorithm(EmpiSimilarityEnum.COSINE).setMatchThreshold(NAME_THRESHOLD));
EmpiResourceFieldMatcher comparator = new EmpiResourceFieldMatcher(ourFhirContext, matchField);
comparator.match(myJohn, myJohny);
fail();

View File

@ -0,0 +1,93 @@
package ca.uhn.fhir.empi.rules.svc;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.empi.api.EmpiMatchOutcome;
import ca.uhn.fhir.empi.api.EmpiMatchResultEnum;
import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson;
import ca.uhn.fhir.empi.rules.json.EmpiMatcherJson;
import ca.uhn.fhir.empi.rules.json.EmpiRulesJson;
import ca.uhn.fhir.empi.rules.matcher.EmpiMatcherEnum;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ResourceMatcherR4Test extends BaseEmpiRulesR4Test {
private static final String PATIENT_PHONE = "phone";
private static final String MATCH_FIELDS = PATIENT_GIVEN + "," + PATIENT_FAMILY + "," + PATIENT_PHONE;
public static final String PHONE_NUMBER = "123 456789";
private Patient myLeft;
private Patient myRight;
@Override
@BeforeEach
public void before() {
super.before();
when(mySearchParamRetriever.getActiveSearchParam("Patient", "birthdate")).thenReturn(mock(RuntimeSearchParam.class));
when(mySearchParamRetriever.getActiveSearchParam("Patient", "identifier")).thenReturn(mock(RuntimeSearchParam.class));
when(mySearchParamRetriever.getActiveSearchParam("Patient", "active")).thenReturn(mock(RuntimeSearchParam.class));
{
myLeft = new Patient();
HumanName name = myLeft.addName();
name.addGiven("zulaiha");
name.setFamily("namadega");
myLeft.addTelecom().setValue(PHONE_NUMBER);
myLeft.setId("Patient/1");
}
{
myRight = new Patient();
HumanName name = myRight.addName();
name.addGiven("zulaiha");
name.setFamily("namaedga");
myRight.addTelecom().setValue(PHONE_NUMBER);
myRight.setId("Patient/2");
}
}
@Test
public void testMetaphoneMatchResult() {
EmpiResourceMatcherSvc matcherSvc = buildMatcher(buildNamePhoneRules(EmpiMatcherEnum.METAPHONE));
EmpiMatchOutcome result = matcherSvc.match(myLeft, myRight);
assertMatchResult(EmpiMatchResultEnum.MATCH, 7L, 3.0, false, false, result);
}
@Test
public void testStringMatchResult() {
EmpiResourceMatcherSvc matcherSvc = buildMatcher(buildNamePhoneRules(EmpiMatcherEnum.STRING));
EmpiMatchOutcome result = matcherSvc.match(myLeft, myRight);
assertMatchResult(EmpiMatchResultEnum.NO_MATCH, 5L, 2.0, false, false, result);
}
protected EmpiRulesJson buildNamePhoneRules(EmpiMatcherEnum theMatcherEnum) {
EmpiFieldMatchJson lastNameMatchField = new EmpiFieldMatchJson()
.setName(PATIENT_FAMILY)
.setResourceType("Patient")
.setResourcePath("name.family")
.setMatcher(new EmpiMatcherJson().setAlgorithm(theMatcherEnum));
EmpiFieldMatchJson firstNameMatchField = new EmpiFieldMatchJson()
.setName(PATIENT_GIVEN)
.setResourceType("Patient")
.setResourcePath("name.given")
.setMatcher(new EmpiMatcherJson().setAlgorithm(theMatcherEnum));
EmpiFieldMatchJson phoneField = new EmpiFieldMatchJson()
.setName(PATIENT_PHONE)
.setResourceType("Patient")
.setResourcePath("telecom.value")
.setMatcher(new EmpiMatcherJson().setAlgorithm(EmpiMatcherEnum.STRING));
EmpiRulesJson retval = new EmpiRulesJson();
retval.setVersion("test version");
retval.addMatchField(firstNameMatchField);
retval.addMatchField(lastNameMatchField);
retval.addMatchField(phoneField);
retval.putMatchResult(MATCH_FIELDS, EmpiMatchResultEnum.MATCH);
return retval;
}
}

View File

@ -6,8 +6,10 @@
"name" : "given-name",
"resourceType" : "Patient",
"resourcePath" : "name.first",
"metric" : "STRING",
"exact" : true
"matcher" : {
"algorithm": "STRING",
"exact" : true
}
}],
"matchResultMap" : {
"given-name" : "POSSIBLE_MATCH"

View File

@ -7,14 +7,18 @@
"name": "foo",
"resourceType": "Patient",
"resourcePath": "name.family",
"metric": "STRING",
"exact": true
"matcher" : {
"algorithm": "STRING",
"exact": true
}
},
{
"name": "foo",
"resourceType": "Patient",
"resourcePath": "name.given",
"metric": "STRING"
"matcher" : {
"algorithm": "STRING"
}
}
],
"matchResultMap": {

View File

@ -6,8 +6,10 @@
"name" : "given-name",
"resourceType" : "*",
"resourcePath" : "name.given",
"metric" : "COSINE",
"matchThreshold" : 0.8
"similarity" : {
"algorithm": "COSINE",
"matchThreshold" : 0.8
}
}],
"matchResultMap" : {
"given-name" : "POSSIBLE_MATCH",

View File

@ -6,7 +6,9 @@
"name" : "given-name",
"resourceType" : "*",
"resourcePath" : "name.given",
"metric" : "COSINE"
"similarity" : {
"algorithm": "COSINE"
}
}],
"matchResultMap" : {
"given-name" : "POSSIBLE_MATCH"

View File

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

View File

@ -84,7 +84,7 @@ public class ProviderConstants {
public static final String EMPI_CLEAR = "$empi-clear";
public static final String EMPI_CLEAR_TARGET_TYPE = "targetType";
public static final String OPERATION_EMPI_BATCH_RUN = "$empi-submit";
public static final String OPERATION_EMPI_SUBMIT = "$empi-submit";
public static final String EMPI_BATCH_RUN_CRITERIA= "criteria" ;
public static final String OPERATION_EMPI_BATCH_RUN_OUT_PARAM_SUBMIT_COUNT = "submitted" ;
public static final String OPERATION_EMPI_CLEAR_OUT_PARAM_DELETED_COUNT = "deleted";