From ac0d8c0fe1897eb870baf29d9141b0914e638ce3 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Sun, 14 Jun 2020 15:32:50 -0400 Subject: [PATCH 1/5] Test fixes --- .../jpa/term/TermCodeSystemStorageSvcImpl.java | 12 +++++++++--- .../jpa/term/TermDeferredStorageSvcImpl.java | 16 ++++++++++------ .../FhirResourceDaoDstu3CodeSystemTest.java | 5 ++++- .../ResourceProviderDstu3CodeSystemTest.java | 5 ++++- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcImpl.java index c4cc2ca5eda..331c7aa2f24 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermCodeSystemStorageSvcImpl.java @@ -213,9 +213,15 @@ public class TermCodeSystemStorageSvcImpl implements ITermCodeSystemStorageSvc { myCodeSystemDao.flush(); }); - List codeSystemVersions = myCodeSystemVersionDao.findByCodeSystemPid(theCodeSystem.getPid()); - for (TermCodeSystemVersion next : codeSystemVersions) { - deleteCodeSystemVersion(next.getPid()); + List codeSystemVersionPids = txTemplate.execute(t -> { + List codeSystemVersions = myCodeSystemVersionDao.findByCodeSystemPid(theCodeSystem.getPid()); + return codeSystemVersions + .stream() + .map(v -> v.getPid()) + .collect(Collectors.toList()); + }); + for (Long next : codeSystemVersionPids) { + deleteCodeSystemVersion(next); } txTemplate.executeWithoutResult(t -> { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermDeferredStorageSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermDeferredStorageSvcImpl.java index bb934847a7d..b4c1a5d86d1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermDeferredStorageSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TermDeferredStorageSvcImpl.java @@ -64,7 +64,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc { @Autowired protected PlatformTransactionManager myTransactionMgr; private boolean myProcessDeferred = true; - private List myCodeSystemsToDelete = Collections.synchronizedList(new ArrayList<>()); + private List myDefferedCodeSystemsDeletions = Collections.synchronizedList(new ArrayList<>()); private List myDeferredConcepts = Collections.synchronizedList(new ArrayList<>()); private List myDeferredValueSets = Collections.synchronizedList(new ArrayList<>()); private List myDeferredConceptMaps = Collections.synchronizedList(new ArrayList<>()); @@ -109,7 +109,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc { public void deleteCodeSystem(TermCodeSystem theCodeSystem) { theCodeSystem.setCodeSystemUri("urn:uuid:" + UUID.randomUUID().toString()); myCodeSystemDao.save(theCodeSystem); - myCodeSystemsToDelete.add(theCodeSystem); + myDefferedCodeSystemsDeletions.add(theCodeSystem); } @Override @@ -155,8 +155,11 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc { ourLog.info("Saving {} deferred concept relationships...", count); while (relCount < count && myConceptLinksToSaveLater.size() > 0) { TermConceptParentChildLink next = myConceptLinksToSaveLater.remove(0); + assert next.getChild() != null; + assert next.getParent() != null; - if (!myConceptDao.findById(next.getChild().getId()).isPresent() || !myConceptDao.findById(next.getParent().getId()).isPresent()) { + if ((next.getChild().getId() == null || !myConceptDao.findById(next.getChild().getId()).isPresent()) + || (next.getParent().getId() == null || !myConceptDao.findById(next.getParent().getId()).isPresent())) { ourLog.warn("Not inserting link from child {} to parent {} because it appears to have been deleted", next.getParent().getCode(), next.getChild().getCode()); continue; } @@ -194,6 +197,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc { myDeferredValueSets.clear(); myDeferredConceptMaps.clear(); myDeferredConcepts.clear(); + myDefferedCodeSystemsDeletions.clear(); } @Transactional(propagation = Propagation.NEVER) @@ -243,10 +247,10 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc { } private void processDeferredCodeSystemDeletions() { - for (TermCodeSystem next : myCodeSystemsToDelete) { + for (TermCodeSystem next : myDefferedCodeSystemsDeletions) { myCodeSystemStorageSvc.deleteCodeSystem(next); } - myCodeSystemsToDelete.clear(); + myDefferedCodeSystemsDeletions.clear(); } @Override @@ -277,7 +281,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc { } private boolean isDeferredCodeSystemDeletions() { - return !myCodeSystemsToDelete.isEmpty(); + return !myDefferedCodeSystemsDeletions.isEmpty(); } private boolean isDeferredConcepts() { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3CodeSystemTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3CodeSystemTest.java index b59a83b60fa..c9784f48643 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3CodeSystemTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3CodeSystemTest.java @@ -68,7 +68,10 @@ public class FhirResourceDaoDstu3CodeSystemTest extends BaseJpaDstu3Test { }); // Delete the code system - myCodeSystemDao.delete(id); + runInTransaction(()->{ + myCodeSystemDao.delete(id); + }); + myTerminologyDeferredStorageSvc.saveDeferred(); runInTransaction(()->{ assertEquals(0L, myConceptDao.count()); }); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3CodeSystemTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3CodeSystemTest.java index 374644e846c..b52a67c22db 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3CodeSystemTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3CodeSystemTest.java @@ -114,10 +114,13 @@ public class ResourceProviderDstu3CodeSystemTest extends BaseResourceProviderDst // Create the code system CodeSystem cs = (CodeSystem) myFhirCtx.newJsonParser().parseResource(input); ourClient.update().resource(cs).execute(); - runInTransaction(() -> assertEquals(26, myConceptDao.count())); + runInTransaction(() -> assertEquals(26L, myConceptDao.count())); // Delete the code system ourClient.delete().resource(cs).execute(); + runInTransaction(() -> assertEquals(26L, myConceptDao.count())); + + myTerminologyDeferredStorageSvc.saveDeferred(); runInTransaction(() -> assertEquals(24L, myConceptDao.count())); } From db4b7494360e77df051ac6538590e75c4dfaa5c4 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Sun, 14 Jun 2020 15:44:33 -0400 Subject: [PATCH 2/5] License updates --- .../jpa/migrate/taskdef/ColumnTypeEnum.java | 20 +++++++++++++++++++ .../ColumnTypeToDriverTypeToSqlType.java | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ColumnTypeEnum.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ColumnTypeEnum.java index 87272a08309..1010837ea32 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ColumnTypeEnum.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ColumnTypeEnum.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.migrate.taskdef; +/*- + * #%L + * HAPI FHIR JPA Server - Migration + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + public enum ColumnTypeEnum { LONG, diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ColumnTypeToDriverTypeToSqlType.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ColumnTypeToDriverTypeToSqlType.java index f2d93fb3db2..26e67f2374e 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ColumnTypeToDriverTypeToSqlType.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/ColumnTypeToDriverTypeToSqlType.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.migrate.taskdef; +/*- + * #%L + * HAPI FHIR JPA Server - Migration + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import ca.uhn.fhir.jpa.migrate.DriverTypeEnum; import java.util.HashMap; From d164e2d45057b85cbe12b3bdb5536f5cab019525 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Sun, 14 Jun 2020 17:15:56 -0400 Subject: [PATCH 3/5] Empi 28 matchers (#1918) * adding matchers * reorganize resource matching api * added precision sensitive date matcher * stricter rules validation * validate thresholds * validate paths. with FIXMES * validate searchparams * fix merge compile error * add soundex, validate no duplicate names * add normalize substring * add exact field to matcher * EXACT -> STRING, exact=true * cleanup test method * match test passes with fixmes * fixed vector matching * fixed vector matching * updating documentation and fixing tests * updated rules documentation with latest matchers * updated rules documentation with latest matchers * created eid page * eid documentation * pre-review cleanup * clean up beans * disentangling beans * checkstyle * noop to trigger CI * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi.md Co-authored-by: Tadgh * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi.md Co-authored-by: Tadgh * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi.md Co-authored-by: Tadgh * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md Co-authored-by: Tadgh * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md Co-authored-by: Tadgh * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md Co-authored-by: Tadgh * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md Co-authored-by: Tadgh * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_rules.md Co-authored-by: Tadgh * review feedback * review feedback * review feedback * review feedback * review feedback Co-authored-by: Tadgh --- .../ca/uhn/hapi/fhir/docs/files.properties | 6 +- .../hapi/fhir/docs/images/empi-create-1.svg | 1 + .../hapi/fhir/docs/images/empi-create-2.svg | 1 + .../hapi/fhir/docs/images/empi-create-3.svg | 1 + .../hapi/fhir/docs/images/empi-create-4.svg | 1 + .../hapi/fhir/docs/images/empi-create-5.svg | 1 + .../hapi/fhir/docs/images/empi-update-1.svg | 1 + .../hapi/fhir/docs/images/empi-update-2.svg | 1 + .../hapi/fhir/docs/images/empi-update-3.svg | 1 + .../hapi/fhir/docs/images/empi-update-4.svg | 1 + .../hapi/fhir/docs/images/empi-update-5.svg | 1 + .../hapi/fhir/docs/images/empi-update-6.svg | 1 + .../hapi/fhir/docs/server_jpa_empi/empi.md | 157 ++--------- .../fhir/docs/server_jpa_empi/empi_details.md | 79 ++++++ .../fhir/docs/server_jpa_empi/empi_eid.md | 42 +++ .../fhir/docs/server_jpa_empi/empi_rules.md | 264 ++++++++++++++++++ .../docs/server_jpa_empi/empi_settings.md | 11 - .../jpa/empi/config/EmpiConsumerConfig.java | 15 +- .../jpa/empi/config/EmpiSubmitterConfig.java | 6 + .../jpa/empi/svc/EmpiCandidateSearchSvc.java | 4 +- .../jpa/empi/svc/EmpiMatchFinderSvcImpl.java | 6 +- .../fhir/jpa/empi/svc/EmpiSearchParamSvc.java | 8 +- .../ca/uhn/fhir/jpa/empi/BaseEmpiR4Test.java | 4 +- .../jpa/empi/config/BaseTestEmpiConfig.java | 5 +- .../jpa/empi/helper/EmpiHelperConfig.java | 5 +- .../fhir/jpa/empi/svc/EmpiLinkSvcTest.java | 2 +- .../src/test/resources/empi/empi-rules.json | 73 +++-- .../src/test/resources/logback-test.xml | 2 +- hapi-fhir-server-empi/pom.xml | 4 + .../empi/rules/config/EmpiRuleValidator.java | 122 ++++++++ .../fhir/empi/rules/config/EmpiSettings.java | 13 +- .../empi/rules/json/DistanceMetricEnum.java | 71 ----- .../empi/rules/json/EmpiFieldMatchJson.java | 37 ++- .../rules/json/EmpiFilterSearchParamJson.java | 8 +- .../json/EmpiResourceSearchParamJson.java | 4 +- .../fhir/empi/rules/json/EmpiRulesJson.java | 43 +-- .../empi/rules/json/VectorMatchResultMap.java | 25 +- .../empi/rules/metric/EmpiMetricEnum.java | 89 ++++++ .../empi/rules/metric/IEmpiFieldMetric.java | 4 + .../metric/matcher/BaseHapiStringMetric.java | 14 + .../matcher/DoubleMetaphoneStringMatcher.java | 10 + .../matcher}/EmpiPersonNameMatchModeEnum.java | 8 +- .../rules/metric/matcher/HapiDateMatcher.java | 21 ++ .../metric/matcher/HapiDateMatcherDstu3.java | 40 +++ .../metric/matcher/HapiDateMatcherR4.java | 40 +++ .../metric/matcher/HapiStringMatcher.java | 52 ++++ .../matcher/IEmpiFieldMatcher.java} | 14 +- .../metric/matcher/IEmpiStringMatcher.java | 5 + .../matcher/MetaphoneStringMatcher.java | 10 + .../matcher/NameMatcher.java} | 20 +- .../metric/matcher/StringEncoderMatcher.java | 26 ++ .../matcher/SubstringStringMatcher.java | 8 + .../similarity/HapiStringSimilarity.java | 15 +- .../similarity/IEmpiFieldSimilarity.java | 7 +- ...tor.java => EmpiResourceFieldMatcher.java} | 6 +- ...orSvc.java => EmpiResourceMatcherSvc.java} | 24 +- .../java/ca/uhn/fhir/empi/BaseR4Test.java | 72 +---- .../rules/config/EmpiRuleValidatorTest.java | 98 +++++-- .../empi/rules/json/EmpiRulesJsonR4Test.java | 10 +- .../rules/json/VectorMatchResultMapTest.java | 17 ++ .../metric/matcher/BaseMatcherR4Test.java | 7 + .../metric/matcher/DateMatcherR4Test.java | 89 ++++++ .../metric/matcher/StringMatcherR4Test.java | 132 +++++++++ .../empi/rules/svc/BaseEmpiRulesR4Test.java | 63 +++++ .../svc/CustomResourceComparatorR4Test.java | 117 -------- .../svc/CustomResourceMatcherR4Test.java | 117 ++++++++ .../svc/EmpiResourceComparatorSvcR4Test.java | 53 ---- ...va => EmpiResourceFieldMatcherR4Test.java} | 14 +- .../svc/EmpiResourceMatcherSvcR4Test.java | 59 ++++ .../ca/uhn/fhir/empi/svc/EIDHelperR4Test.java | 28 +- .../test/resources/bad-rules-bad-filter.json | 9 + .../test/resources/bad-rules-bad-path.json | 14 + .../resources/bad-rules-bad-searchparam.json | 9 + .../src/test/resources/bad-rules-bad-url.json | 7 + .../resources/bad-rules-duplicate-name.json | 22 ++ ...rules.json => bad-rules-missing-name.json} | 3 +- .../bad-rules-missing-threshold.json | 13 + .../resources/bad-rules-unused-threshold.json | 14 + .../server/util/ISearchParamRetriever.java | 10 + 79 files changed, 1788 insertions(+), 629 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-1.svg create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-2.svg create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-3.svg create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-4.svg create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-5.svg create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-1.svg create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-2.svg create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-3.svg create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-4.svg create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-5.svg create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-6.svg create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_eid.md create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_rules.md delete mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_settings.md delete mode 100644 hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/DistanceMetricEnum.java create mode 100644 hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/EmpiMetricEnum.java create mode 100644 hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/IEmpiFieldMetric.java create mode 100644 hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/BaseHapiStringMetric.java create mode 100644 hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/DoubleMetaphoneStringMatcher.java rename hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/{similarity => metric/matcher}/EmpiPersonNameMatchModeEnum.java (85%) create mode 100644 hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcher.java create mode 100644 hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherDstu3.java create mode 100644 hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherR4.java create mode 100644 hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiStringMatcher.java rename hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/{similarity/ReferenceMatchSimilarity.java => metric/matcher/IEmpiFieldMatcher.java} (66%) create mode 100644 hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/IEmpiStringMatcher.java create mode 100644 hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/MetaphoneStringMatcher.java rename hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/{similarity/NameSimilarity.java => metric/matcher/NameMatcher.java} (78%) create mode 100644 hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/StringEncoderMatcher.java create mode 100644 hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/SubstringStringMatcher.java rename hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/{ => metric}/similarity/HapiStringSimilarity.java (73%) rename hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/{ => metric}/similarity/IEmpiFieldSimilarity.java (82%) rename hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/{EmpiResourceFieldComparator.java => EmpiResourceFieldMatcher.java} (92%) rename hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/{EmpiResourceComparatorSvc.java => EmpiResourceMatcherSvc.java} (77%) create mode 100644 hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/metric/matcher/BaseMatcherR4Test.java create mode 100644 hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/metric/matcher/DateMatcherR4Test.java create mode 100644 hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/metric/matcher/StringMatcherR4Test.java create mode 100644 hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/BaseEmpiRulesR4Test.java delete mode 100644 hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/CustomResourceComparatorR4Test.java create mode 100644 hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/CustomResourceMatcherR4Test.java delete mode 100644 hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceComparatorSvcR4Test.java rename hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/{EmpiResourceFieldComparatorR4Test.java => EmpiResourceFieldMatcherR4Test.java} (81%) create mode 100644 hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceMatcherSvcR4Test.java create mode 100644 hapi-fhir-server-empi/src/test/resources/bad-rules-bad-filter.json create mode 100644 hapi-fhir-server-empi/src/test/resources/bad-rules-bad-path.json create mode 100644 hapi-fhir-server-empi/src/test/resources/bad-rules-bad-searchparam.json create mode 100644 hapi-fhir-server-empi/src/test/resources/bad-rules-bad-url.json create mode 100644 hapi-fhir-server-empi/src/test/resources/bad-rules-duplicate-name.json rename hapi-fhir-server-empi/src/test/resources/{bad-rules.json => bad-rules-missing-name.json} (80%) create mode 100644 hapi-fhir-server-empi/src/test/resources/bad-rules-missing-threshold.json create mode 100644 hapi-fhir-server-empi/src/test/resources/bad-rules-unused-threshold.json create mode 100644 hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRetriever.java diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties index 73ba427f9c4..97c9c5f35df 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties @@ -50,9 +50,11 @@ page.server_jpa.diff=Diff Operation page.server_jpa.lastn=LastN Operation section.server_jpa_empi.title=JPA Server: EMPI -page.server_jpa_empi.empi=Enterprise Master Patient Index +page.server_jpa_empi.empi=EMPI Getting Started +page.server_jpa_empi.empi_rules=EMPI Rules +page.server_jpa_empi.empi_eid=EMPI Enterprise Identifiers page.server_jpa_empi.empi_operations=EMPI Operations -page.server_jpa_empi.empi_settings=Enabling EMPI in HAPI FHIR +page.server_jpa_empi.empi_details=EMPI Technical Details section.server_jpa_partitioning.title=JPA Server: Partitioning and Multitenancy page.server_jpa_partitioning.partitioning=Partitioning and Multitenancy diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-1.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-1.svg new file mode 100644 index 00000000000..cd597760b78 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-2.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-2.svg new file mode 100644 index 00000000000..1d3aa9c7270 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-3.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-3.svg new file mode 100644 index 00000000000..8e25f06e66a --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-4.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-4.svg new file mode 100644 index 00000000000..ab41f9095de --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-5.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-5.svg new file mode 100644 index 00000000000..251d0944576 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-create-5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-1.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-1.svg new file mode 100644 index 00000000000..4dff241c6f7 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-2.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-2.svg new file mode 100644 index 00000000000..6595695aed2 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-3.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-3.svg new file mode 100644 index 00000000000..2ed5cbfb52b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-4.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-4.svg new file mode 100644 index 00000000000..e8d4387b77b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-5.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-5.svg new file mode 100644 index 00000000000..3b65343a4b3 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-6.svg b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-6.svg new file mode 100644 index 00000000000..6f121750ab4 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/images/empi-update-6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi.md index 205c5cee9fa..76728325004 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi.md @@ -1,158 +1,37 @@ -# Enterprise Master Person Index (EMPI) +# EMPI Getting Started -HAPI FHIR 5.0.0 introduced preliminary support for **EMPI**. +## Introduction + +HAPI FHIR 5.1.0 introduces preliminary support for **EMPI**. An EMPI allows for links to be created and maintained between different Patient and/or Practitioner resources. These links are used to indicate the fact that different Patient/Practitioner resources are known or believed to refer to the same actual (real world) person. -These links may be created and updated using different combinations of automatic linking as well as manual linking. +These links may be created and updated using different combinations of automatic linking and manual linking. -Note: The following sections describe linking between Patient and Person resources. The same information applies for linking between Practitioner and Person, but for readability it is not repeated. +Note: This documentation describes EMPI for Patient resources. The same information applies for Practitioner resources. You can substitute "Practitioner" for "Patient" anywhere it appears in this documentation. ## Working Example -The [JPA Server Starter](/hapi-fhir/docs/server_jpa/get_started.html) project contains a complete working example of the HAPI EMPI feature and documentation about how to enable and configure it. You may wish to browse its source to see how this works. +A complete working example of HAPI EMPI can be found in the [JPA Server Starter](/hapi-fhir/docs/server_jpa/get_started.html) project. You may wish to browse its source to see how it is set up. -## Person linking in FHIR +## Overview -Because HAPI EMPI is implemented on the HAPI JPA Server, it uses the FHIR model to represent roles and links. The following illustration shows an example of how these links work. +To get up and running with HAPI EMPI, either enable it using the `hapi.properties` file in the JPA Server Starter, or follow the instructions below to (enable it in HAPI FHIR directly)[#empi-settings]. -EMPI links +Once EMPI is enabled, the next thing you will want to do is configure your [EMPI Rules](/hapi-fhir/docs/server_jpa_empi/empi_rules.html) -There are several resources that are used: +HAPI EMPI watches for incoming Patient resources and automatically links them to Person resources based on these rules. For example, if the rules indicate that any two patients with the same ssn, birthdate and first and last name are the same person, then two different Patient resources with matching values for these attributes will automatically be linked to the same Person resource. If no existing resources match the incoming Patient, then a new Person resource will be created and linked to the incoming Patient. -* Patient - Represents the record of a person who receives healthcare services -* Person - Represents a master record with links to one or more Patient and/or Practitioner resources that belong to the same person +Based on how well two patients match, the EMPI Rules may link the Patient to the Person as a MATCH or a POSSIBLE_MATCH. In the case of a POSSIBLE_MATCH, a user will need to later use [EMPI Operations](/hapi-fhir/docs/server_jpa_empi/empi_operations.html) to either confirm the link as a MATCH, or mark the link as a NO_MATCH in which case HAPI EMPI will create a new Person for them. -# Automatic Linking +Another thing that can happen in the linking process is HAPI EMPI can determine that two Person resources may be duplicates. In this case, it marks them as POSSIBLE_DUPLICATE and the user can use [EMPI Operations](/hapi-fhir/docs/server_jpa_empi/empi_operations.html) to either merge the two Persons or mark them as NO_MATCH in which case HAPI EMPI will know not to mark them as possible duplicates in the future. -With EMPI enabled, the basic default behavior of the EMPI is simply to create a new Person record for every Patient that is created such that there is a 1:1 relationship between them. Any relinking is then expected to be done manually (i.e. via the forthcoming empi operations). +HAPI EMPI keeps track of which links were automatically established vs manually verified. Manual links always take precedence over automatic links. Once a link for a patient has been manually verified, HAPI EMPI won't modify or remove it. -In a typical configuration it is often desirable to have links be created automatically using matching rules. For example, you might decide that if a Patient shares the same name, gender, and date of birth as another Patient, you have at least a little confidence that they are the same Person. +## EMPI Settings -This automatic linking is done via configurable matching rules that create a links between Patients and Persons. Based on the strength of the match configured in these rules, the link will be set to either POSSIBLE_MATCH or MATCHED. +Follow these steps to enable EMPI on the server: -## Design Principles +The [EmpiSettings](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html) bean contains configuration settings related to EMPI within the server. To enable EMPI, the [setEnabled(boolean)](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html#setEnabled(boolean)) property should be enabled. -Below are some simplifying principles HAPI EMPI enforces to reduce complexity and ensure data integrity. - -1. When EMPI is enabled on a HAPI FHIR server, any Person resource in the repository that has the "hapi-empi" tag is considered read-only via the FHIR endpoint. These Person resources are managed exclusively by HAPI EMPI. Users can only directly change them via special empi operations. In most cases, users will indirectly change them by creating and updating Patient and Practitioner ("Patient") resources. For the rest of this document, assume "Person" refers to a "hapi-empi" tagged Person resource. - -1. Every Patient in the system has a MATCH link to at most one Person resource. - -1. Every Patient resource in the system has a MATCH link to a Person resource unless that Patient has the "no-empi" tag or it has POSSIBLE_MATCH links pending review. - -1. The HAPI EMPI rules define a single identifier system that holds the external enterprise id ("EID"). If a Patient has an external EID, then the Person it links to always has the same EID. If a patient has no EID when it arrives, the person created from this patient is given an internal EID. - -1. A Person can have both an internal EID(auto-created by HAPI), and an external EID (provided by an external system). - -1. Two different Person resources cannot have the same EID. - -1. Patient resources are only ever compared to Person resources via this EID. For all other matches, Patient resources are only ever compared to Patient resources and Practitioner resources are only ever compared to Practitioner resources. - -## Links - -1. HAPI EMPI manages empi-link records ("links") that link a Patient resource to a Person resource. When these are created/updated by matching rules, the links are marked as AUTO. When these links are changed manually, they are marked as MANUAL. - -1. Once a link has been manually assigned as NO_MATCH or MATCHED, the system will not change it. - -1. When a new Patient resource is created/updated then it is compared to all other Patient resources in the repository. The outcome of each of these comparisons is either NO_MATCH, POSSIBLE_MATCH or MATCHED. - -1. Whenever a MATCHED link is established between a Patient resource and a Person resource, that Patient is always added to that Person resource links. All MATCHED links have corresponding Person resource links and all Person resource links have corresponding MATCHED empi-link records. You can think of the fields of the empi-link records as extra meta-data associated with each Person.link.target. - -### Possible rule match outcomes: - -When a new Patient resource is compared with all other resources of that type in the repository, there are four possible cases: - -* CASE 1: No MATCHED and no POSSIBLE_MATCHED outcomes -> a new Person resource is created and linked to that Patient as MATCHED. All fields are copied from the Patient to the Person. If the incoming resource has an EID, it is copied to the Person. Otherwise a new UUID is created and used as the internal EID. - -* CASE 2: All of the MATCHED Patient resources are already linked to the same Person -> a new Link is created between the new Patient and that Person and is set to MATCHED. - -* CASE 3: The MATCHED Patient resources link to more than one Person -> Mark all links as POSSIBLE_MATCHED. All other Person resources are marked as POSSIBLE_DUPLICATE of this first Person. These duplicates are manually reviewed later and either merged or marked as NO_MATCH and the system will no longer consider them as a POSSIBLE_DUPLICATE going forward. POSSIBLE_DUPLICATE is the only link type that can have a Person as both the source and target of the link. - -* CASE 4: Only POSSIBLE_MATCH outcomes -> In this case, empi-link records are created with POSSIBLE_MATCH outcome and await manual assignment to either NO_MATCH or MATCHED. Person resources are not changed. - -# Rules - -HAPI EMPI rules are managed via a single json document. This document contains a version. empi-links derived from these rules are marked with this version. The following configuration is stored in the rules: - -* **resourceSearchParams**: These define fields which must have at least one exact match before two resources are considered for matching. This is like a list of "pre-searches" that find potential candidates for matches, to avoid the expensive operation of running a match score calculation on all resources in the system. E.g. you may only wish to consider matching two Patients if they either share at least one identifier in common or have the same birthday. -```json -[ { - "resourceType" : "Patient", - "searchParam" : "birthdate" -}, { - "resourceType" : "Patient", - "searchParam" : "identifier" -} ] -``` - -* **filterSearchParams** When searching for match candidates, only resources that match this filter are considered. E.g. you may wish to only search for Patients for which active=true. -```json -[ { - "resourceType" : "Patient", - "searchParam" : "active", - "fixedValue" : "true" -} ] -``` - -* **matchFields** Once the match candidates have been found, they are then each assigned a match vector that marks which fields match. The match vector is determined by a list of matchFields. Each matchField defines a name, distance metric, a success threshold, a resource type, and resource path to check. For example: -```json -{ - "name" : "given-name-cosine", - "resourceType" : "Patient", - "resourcePath" : "name.given", - "metric" : "COSINE", - "matchThreshold" : 0.8 -} -``` - -Note that in all the above json, valid options for `resourceType` are `Patient`, `Practitioner`, and `All`. Use `All` if the criteria is identical across both resource types, and you would like to apply the pre-search to both practitioners and patients. - -The following metrics are currently supported: -* JARO_WINKLER -* COSINE -* JACCARD -* NORMALIZED_LEVENSCHTEIN -* SORENSEN_DICE -* STANDARD_NAME_ANY_ORDER -* EXACT_NAME_ANY_ORDER -* STANDARD_NAME_FIRST_AND_LAST -* EXACT_NAME_FIRST_AND_LAST - -See [java-string-similarity](https://github.com/tdebatty/java-string-similarity) for a description of the first five metrics. For the last four, STANDARd means ignore case and accents whereas EXACT must match casing and accents exactly. Name any order matches first and last names irrespective of order, whereas FIRST_AND_LAST metrics require the name match to be in order. - -* **matchResultMap** A map which converts combinations of successful matchFields into an EMPI Match Result score for overall matching of a given pair of resources. - -```json -"matchResultMap" : { - "given-name-cosine" : "POSSIBLE_MATCH", - "given-name-jaro, last-name-jaro" : "MATCH" -} -``` - -* **eidSystem**: The external EID system that the HAPI EMPI system should expect to see on incoming Patient resources. Must be a valid URI. - -# Enterprise Identifiers - -An Enterprise Identifier(EID) is a unique identifier that can be attached to Patients or Practitioners. Each implementation is expected to use exactly one EID system for incoming resources, -defined in the mentioned `empi-rules.json` file. If a Patient or Practitioner with a valid EID is added to the system, that EID will be copied over to the Person that was matched. In the case that -the incoming Patient or Practitioner had no EID assigned, an internal EID will be created for it. There are thus two classes of EID. Internal EIDs, created by HAPI-EMPI, and External EIDs, provided -by the install. - -There are many edge cases for determining what will happen in merge and update scenarios, which will be provided in future documentation. - - -# HAPI EMPI Technical Details - -When EMPI is enabled, the HAPI FHIR JPA Server does the following things on startup: - -1. HAPI EMPI stores the extra link details in a table called `MPI_LINK`. -1. Each record in an `MPI_LINK` table corresponds to a `link.target` entry on a Person resource. HAPI EMPI uses the following convention for the Person.link.assurance level: - 1. Level 1: not used - 1. Level 2: POSSIBLE_MATCH - 1. Level 3: AUTO MATCHED - 1. Level 4: MANUAL MATCHED -1. It enables the MESSAGE subscription type and starts up the internal subscription engine. -1. It creates two MESSAGE subscriptions, called 'empi-patient' and 'empi-practitioner' that match all incoming Patient and Practitioner resources and send them to an internal queue called "empi". The JPA Server listens to this queue and links incoming resources to Persons. -1. It registers the `Patient/$match` operation. See [$match](https://www.hl7.org/fhir/operation-patient-match.html) for a description of this operation. -1. It registers a new dao interceptor that restricts access to EMPI managed Person records. +See [EMPI EID Settings](/hapi-fhir/docs/server_jpa_empi/empi_eid.html#empi-eid-settings) for a description of the EID-related settings. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md new file mode 100644 index 00000000000..7859fab2f7b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_details.md @@ -0,0 +1,79 @@ +# EMPI Implementation Details + +This section describes details of how EMPI functionality is implemented in HAPI FHIR. + +## Person linking in FHIR + +Because HAPI EMPI is implemented on the HAPI JPA Server, it uses the FHIR model to represent roles and links. The following illustration shows an example of how these links work. + +EMPI links + +There are several resources that are used: + +* Patient - Represents the record of a person who receives healthcare services +* Person - Represents a master record with links to one or more Patient and/or Practitioner resources that belong to the same person + +# Automatic Linking + +With EMPI enabled, the basic default behavior of the EMPI is simply to create a new Person record for every Patient that is created such that there is a 1:1 relationship between them. Any relinking is then expected to be done manually (i.e. via the forthcoming empi operations). + +In a typical configuration it is often desirable to have links be created automatically using matching rules. For example, you might decide that if a Patient shares the same name, gender, and date of birth as another Patient, you have at least a little confidence that they are the same Person. + +This automatic linking is done via configurable matching rules that create links between Patients and Persons. Based on the strength of the match configured in these rules, the link will be set to either POSSIBLE_MATCH or MATCH. + +## Design + +Below are some simplifying principles HAPI EMPI enforces to reduce complexity and ensure data integrity. + +1. When EMPI is enabled on a HAPI FHIR server, any Person resource in the repository that has the "hapi-empi" tag is considered read-only via the FHIR endpoint. These Person resources are managed exclusively by HAPI EMPI. Users can only directly change them via special empi operations. In most cases, users will indirectly change them by creating and updating Patient and Practitioner ("Patient") resources. For the rest of this document, assume "Person" refers to a "hapi-empi" tagged Person resource. + +1. Every Patient in the system has a MATCH link to at most one Person resource. + +1. Every Patient resource in the system has a MATCH link to a Person resource unless that Patient has the "no-empi" tag or it has POSSIBLE_MATCH links pending review. + +1. The HAPI EMPI rules define a single identifier system that holds the external enterprise id ("EID"). If a Patient has an external EID, then the Person it links to always has the same EID. If a patient has no EID when it arrives, the person created from this patient is given an internal EID. + +1. A Person can have both an internal EID(auto-created by HAPI), and an external EID (provided by an external system). + +1. Two different Person resources cannot have the same EID. + +1. Patient resources are only ever compared to Person resources via this EID. For all other matches, Patient resources are only ever compared to Patient resources and Practitioner resources are only ever compared to Practitioner resources. + +## Links + +1. HAPI EMPI manages empi-link records ("links") that link a Patient resource to a Person resource. When these are created/updated by matching rules, the links are marked as AUTO. When these links are changed manually, they are marked as MANUAL. + +1. Once a link has been manually assigned as NO_MATCH or MATCH, the system will not change it. + +1. When a new Patient resource is created/updated it is then compared to all other Patient resources in the repository. The outcome of each of these comparisons is either NO_MATCH, POSSIBLE_MATCH or MATCH. + +1. Whenever a MATCH link is established between a Patient resource and a Person resource, that Patient is always added to that Person resource links. All MATCH links have corresponding Person resource links and all Person resource links have corresponding MATCH empi-link records. You can think of the fields of the empi-link records as extra meta-data associated with each Person.link.target. + +1. HAPI EMPI stores these extra link details in a table called `MPI_LINK`. + +1. Each record in the `MPI_LINK` table corresponds to a `link.target` entry on a Person resource unless it is a NO_MATCH record. HAPI EMPI uses the following convention for the Person.link.assurance level: + 1. Level 1: not used + 1. Level 2: POSSIBLE_MATCH + 1. Level 3: AUTO MATCH + 1. Level 4: MANUAL MATCH + +### Possible rule match outcomes: + +When a new Patient resource is compared with all other resources of that type in the repository, there are four possible cases: + +* CASE 1: No MATCH and no POSSIBLE_MATCH outcomes -> a new Person resource is created and linked to that Patient as MATCH. All fields are copied from the Patient to the Person. If the incoming resource has an EID, it is copied to the Person. Otherwise a new UUID is created and used as the internal EID. + +* CASE 2: All of the MATCH Patient resources are already linked to the same Person -> a new Link is created between the new Patient and that Person and is set to MATCH. + +* CASE 3: The MATCH Patient resources link to more than one Person -> Mark all links as POSSIBLE_MATCH. All other Person resources are marked as POSSIBLE_DUPLICATE of this first Person. These duplicates are manually reviewed later and either merged or marked as NO_MATCH and the system will no longer consider them as a POSSIBLE_DUPLICATE going forward. POSSIBLE_DUPLICATE is the only link type that can have a Person as both the source and target of the link. + +* CASE 4: Only POSSIBLE_MATCH outcomes -> In this case, empi-link records are created with POSSIBLE_MATCH outcome and await manual assignment to either NO_MATCH or MATCH. Person resources are not changed. + +# HAPI EMPI Technical Details + +When EMPI is enabled, the HAPI FHIR JPA Server does the following things on startup: + +1. It enables the MESSAGE subscription type and starts up the internal subscription engine. +1. It creates two MESSAGE subscriptions, called 'empi-patient' and 'empi-practitioner' that match all incoming Patient and Practitioner resources and send them to an internal queue called "empi". The JPA Server listens to this queue and links incoming resources to Persons. +1. It registers the `Patient/$match` operation. See [$match](https://www.hl7.org/fhir/operation-patient-match.html) for a description of this operation. +1. It registers a new dao interceptor that restricts access to EMPI managed Person records. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_eid.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_eid.md new file mode 100644 index 00000000000..8287bac64f5 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_eid.md @@ -0,0 +1,42 @@ +# EMPI Enterprise Identifiers + +An Enterprise Identifier(EID) is a unique identifier that can be attached to Patients or Practitioners. Each implementation is expected to use exactly one EID system for incoming resources, defined in the EMPI Rules file. If a Patient or Practitioner with a valid EID is submitted, that EID will be copied over to the Person that was matched. In the case that the incoming Patient or Practitioner had no EID assigned, an internal EID will be created for it. There are thus two classes of EID. Internal EIDs, created by HAPI-EMPI, and External EIDs, provided by the submitted resources. + +## EMPI EID Settings + +The [EmpiSettings](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html) bean contains two EID related settings. Both are enabled by default. + +* **Prevent EID Updates** ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html#setPreventEidUpdates(boolean))): If this is enabled, then once an EID is set on a resource, it cannot be changed. If disabled, patients may have their EID updated. + +* **Prevent multiple EIDs**: ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html#setPreventMultipleEids(boolean))): If this is enabled, then a resource cannot have more than one EID, and incoming resources that break this rule will be rejected. + +## EMPI EID Scenarios + +EMPI EID management follows a complex set of rules to link related Patient records via their Enterprise Id. The following diagrams outline how EIDs are replicated from Patient resources to their linked Person resources under various scenarios according to the values of the EID Settings. + +## EMPI EID Create Scenarios + +EMPI Create 1 + +EMPI Create 1 + +EMPI Create 1 + +EMPI Create 1 + +EMPI Create 1 + +## EMPI EID Update Scenarios + +EMPI Create 1 + +EMPI Create 1 + +EMPI Create 1 + +EMPI Create 1 + +EMPI Create 1 + +EMPI Create 1 + diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_rules.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_rules.md new file mode 100644 index 00000000000..58fb0a8fce9 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_rules.md @@ -0,0 +1,264 @@ +# Rules + +HAPI EMPI rules are managed via a single json document. + +Note that in all the following configuration, valid options for `resourceType` are `Patient`, `Practitioner`, and `*`. Use `*` if the criteria is identical across both resource types, and you would like to apply it to both practitioners and patients. + +Here is an example of a full HAPI EMPI rules json document: + +```json +{ + "candidateSearchParams": [ + { + "resourceType": "Patient", + "searchParam": "birthdate" + }, + { + "resourceType": "*", + "searchParam": "identifier" + }, + { + "resourceType": "Patient", + "searchParam": "general-practitioner" + } + ], + "candidateFilterSearchParams": [ + { + "resourceType": "*", + "searchParam": "active", + "fixedValue": "true" + } + ], + "matchFields": [ + { + "name": "cosine-given-name", + "resourceType": "*", + "resourcePath": "name.given", + "metric": "COSINE", + "matchThreshold": 0.8, + "exact": true + }, + { + "name": "jaro-last-name", + "resourceType": "*", + "resourcePath": "name.family", + "metric": "JARO_WINKLER", + "matchThreshold": 0.8 + } + ], + "matchResultMap": { + "cosine-given-name" : "POSSIBLE_MATCH", + "cosine-given-name,jaro-last-name" : "MATCH" + }, + "eidSystem": "http://company.io/fhir/NamingSystem/custom-eid-system" +} +``` + +Here is a description of how each section of this document is configured. + +* **candidateSearchParams**: These define fields which must have at least one exact match before two resources are considered for matching. This is like a list of "pre-searches" that find potential candidates for matches, to avoid the expensive operation of running a match score calculation on all resources in the system. E.g. you may only wish to consider matching two Patients if they either share at least one identifier in common or have the same birthday. The HAPI FHIR server executes each of these searches separately and then takes the union of the results, so you can think of these as `OR` criteria that cast a wide net for potential candidates. In some EMPI systems, these "pre-searches" are called "blocking" searches (since they identify "blocks" of candidates that will be searched for matches). +```json +[ { + "resourceType" : "Patient", + "searchParam" : "birthdate" +}, { + "resourceType" : "Patient", + "searchParam" : "identifier" +} ] +``` + +* **candidateFilterSearchParams** When searching for match candidates, only resources that match this filter are considered. E.g. you may wish to only search for Patients for which active=true. Another way to think of these filters is all of them are "AND"ed with each candidateSearchParam above. +```json +[ { + "resourceType" : "Patient", + "searchParam" : "active", + "fixedValue" : "true" +} ] +``` + +* **matchFields** Once the match candidates have been found, they are then each compared to the incoming Patient resource. This comparison is made across a list of `matchField`s. Each matchField returns `true` or `false` indicating whether the candidate and the incoming Patient match on that field. There are two types of metrics: `Matcher` and `Similarity`. Matcher metrics return a `true` or `false` directly, whereas Similarity metrics return a score between 0.0 (no match) and 1.0 (exact match) and this score is translated to a `true/false` via a `matchThreshold`. E.g. if a `JARO_WINKLER` matchField is configured with a `matchThreshold` of 0.8 then that matchField will return `true` if the `JARO_WINKLER` similarity evaluates to a score >= 8.0. + +By default, all matchFields have `exact=false` which means that they will have all diacritical marks removed and converted to upper case before matching. `exact=true` can be added to any matchField to compare the strings as they are originally capitalized and accented. + +Here is a matcher matchField that uses the SOUNDEX matcher to determine whether two family names match. + +```json +{ + "name": "family-name-double-metaphone", + "resourceType": "*", + "resourcePath": "name.family", + "metric": "SOUNDEX" +} +``` + +Here is a matcher matchField that only matches when two family names are identical. + +```json +{ + "name": "family-name-exact", + "resourceType": "*", + "resourcePath": "name.family", + "metric": "STRING", + "exact": true +} +``` + +Here is a similarity matchField that matches when two given names match with a JARO_WINKLER threshold >0 0.8. + +```json +{ + "name" : "given-name-jaro", + "resourceType" : "Patient", + "resourcePath" : "name.given", + "metric" : "JARO_WINKLER", + "matchThreshold" : 0.8 +} +``` + +The following metrics are currently supported: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescriptionExample
STRINGmatcher + Match the values as strings. This matcher should be used with tokens (e.g. gender). + MCTAVISH = McTavish when exact = false, MCTAVISH != McTavish when exact = true
SUBSTRINGmatcher + True if one string starts with the other. + Bill = Billy, Egbert = Bert
METAPHONEmatcher + Apache Metaphone + Dury = Durie, Allsop != Allsob, Smith != Schmidt
DOUBLE_METAPHONEmatcher + Apache Double Metaphone + Dury = Durie, Allsop = Allsob, Smith != Schmidt
SOUNDEXmatcher + Apache Soundex + Jon = John, Thomas != Tom
CAVERPHONE1matcher + Apache Caverphone1 + Gail = Gael, Gail != Gale, Thomas != Tom
CAVERPHONE1matcher + Apache Caverphone1 + Gail = Gael, Gail = Gale, Thomas != Tom
DATEmatcher + Reduce the precision of the dates to the lowest precision of the two, then compare them as strings. + 2019-12,Month = 2019-12-19,Day
NAME_ANY_ORDERmatcher + Match names as strings in any order + John Henry = Henry JOHN when exact = false
NAME_FIRST_AND_LASTmatcher + Match names as strings in any order + John Henry = John HENRY when exact=false, John Henry != Henry John
JARO_WINKLERsimilarity + tdebatty Jaro Winkler +
COSINEsimilarity + tdebatty Cosine Similarity +
JACCARDsimilarity + tdebatty Jaccard Index +
LEVENSCHTEINsimilarity + tdebatty Normalized Levenshtein +
SORENSEN_DICEsimilarity + tdebatty Sorensen-Dice coefficient +
+ +* **matchResultMap** converts combinations of successful matchFields into an EMPI Match Result for overall matching of a given pair of resources. MATCH results are evaluated take precedence over POSSIBLE_MATCH results. + +```json +{ + "matchResultMap": { + "cosine-given-name" : "POSSIBLE_MATCH", + "cosine-given-name,jaro-last-name" : "MATCH" + } +} +``` + +* **eidSystem**: The external EID system that the HAPI EMPI system should expect to see on incoming Patient resources. Must be a valid URI. See [EMPI EID](/hapi-fhir/docs/server_jpa_empi/empi_eid.html) for details on how EIDs are managed by HAPI EMPI. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_settings.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_settings.md deleted file mode 100644 index c76792831ae..00000000000 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_empi/empi_settings.md +++ /dev/null @@ -1,11 +0,0 @@ -# Enabling EMPI in HAPI FHIR - -Follow these steps to enable EMPI on the server: - -The [EmpiSettings](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html) bean contains configuration settings related to EMPI within the server. To enable Empi, the [setEnabled(boolean)](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html#setEnabled(boolean)) property should be enabled. - -The following settings are enabled by default: - -* **Prevent EID Updates** ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html#setPreventEidUpdates(boolean))): If this is enabled, then once an EID is set on a resource, it cannot be changed. If disabled, patients may have their EID updated. - -* **Prevent multiple EIDs**: ([JavaDoc](/hapi-fhir/apidocs/hapi-fhir-server-empi/ca/uhn/fhir/empi/rules/config/EmpiSettings.html#setPreventMultipleEids(boolean))): If this is enabled, then a resource cannot have more than one EID, and incoming resources that break this rule will be rejected. diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiConsumerConfig.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiConsumerConfig.java index 548009e27bb..e62fd13ae3c 100644 --- a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiConsumerConfig.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiConsumerConfig.java @@ -30,7 +30,7 @@ import ca.uhn.fhir.empi.api.IEmpiSettings; import ca.uhn.fhir.empi.log.Logs; import ca.uhn.fhir.empi.provider.EmpiProviderLoader; import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator; -import ca.uhn.fhir.empi.rules.svc.EmpiResourceComparatorSvc; +import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc; import ca.uhn.fhir.empi.util.EIDHelper; import ca.uhn.fhir.empi.util.PersonHelper; import ca.uhn.fhir.jpa.empi.broker.EmpiMessageHandler; @@ -47,6 +47,7 @@ import ca.uhn.fhir.jpa.empi.svc.EmpiMatchLinkSvc; import ca.uhn.fhir.jpa.empi.svc.EmpiPersonFindingSvc; import ca.uhn.fhir.jpa.empi.svc.EmpiPersonMergerSvcImpl; import ca.uhn.fhir.jpa.empi.svc.EmpiResourceDaoSvc; +import ca.uhn.fhir.rest.server.util.ISearchParamRetriever; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -64,8 +65,6 @@ public class EmpiConsumerConfig { @Autowired IEmpiSettings myEmpiProperties; @Autowired - EmpiRuleValidator myEmpiRuleValidator; - @Autowired EmpiProviderLoader myEmpiProviderLoader; @Autowired EmpiSubscriptionLoader myEmpiSubscriptionLoader; @@ -133,8 +132,8 @@ public class EmpiConsumerConfig { } @Bean - EmpiRuleValidator empiRuleValidator() { - return new EmpiRuleValidator(); + EmpiRuleValidator empiRuleValidator(FhirContext theFhirContext, ISearchParamRetriever theSearchParamRetriever) { + return new EmpiRuleValidator(theFhirContext, theSearchParamRetriever); } @Bean @@ -159,8 +158,8 @@ public class EmpiConsumerConfig { } @Bean - EmpiResourceComparatorSvc empiResourceComparatorSvc(FhirContext theFhirContext, IEmpiSettings theEmpiConfig) { - return new EmpiResourceComparatorSvc(theFhirContext, theEmpiConfig); + EmpiResourceMatcherSvc empiResourceComparatorSvc(FhirContext theFhirContext, IEmpiSettings theEmpiConfig) { + return new EmpiResourceMatcherSvc(theFhirContext, theEmpiConfig); } @Bean @@ -178,8 +177,6 @@ public class EmpiConsumerConfig { if (!myEmpiProperties.isEnabled()) { return; } - - myEmpiRuleValidator.validate(myEmpiProperties.getEmpiRules()); } @EventListener(classes = {ContextRefreshedEvent.class}) diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiSubmitterConfig.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiSubmitterConfig.java index f6cb3a1b824..3853a4dc878 100644 --- a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiSubmitterConfig.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/config/EmpiSubmitterConfig.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.jpa.empi.config; * #L% */ +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator; import ca.uhn.fhir.jpa.empi.interceptor.EmpiSubmitterInterceptorLoader; import ca.uhn.fhir.jpa.empi.svc.EmpiSearchParamSvc; import org.springframework.context.annotation.Bean; @@ -38,4 +40,8 @@ public class EmpiSubmitterConfig { return new EmpiSearchParamSvc(); } + @Bean + EmpiRuleValidator empiRuleValidator(FhirContext theFhirContext) { + return new EmpiRuleValidator(theFhirContext, empiSearchParamSvc()); + } } diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiCandidateSearchSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiCandidateSearchSvc.java index 0495cd471ff..edb4e58ae1d 100644 --- a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiCandidateSearchSvc.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiCandidateSearchSvc.java @@ -71,11 +71,11 @@ public class EmpiCandidateSearchSvc { public Collection findCandidates(String theResourceType, IAnyResource theResource) { Map matchedPidsToResources = new HashMap<>(); - List filterSearchParams = myEmpiConfig.getEmpiRules().getFilterSearchParams(); + List filterSearchParams = myEmpiConfig.getEmpiRules().getCandidateFilterSearchParams(); List filterCriteria = buildFilterQuery(filterSearchParams, theResourceType); - for (EmpiResourceSearchParamJson resourceSearchParam : myEmpiConfig.getEmpiRules().getResourceSearchParams()) { + for (EmpiResourceSearchParamJson resourceSearchParam : myEmpiConfig.getEmpiRules().getCandidateSearchParams()) { if (!isSearchParamForResource(theResourceType, resourceSearchParam)) { continue; diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchFinderSvcImpl.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchFinderSvcImpl.java index fa27cdaa32b..00d221799a0 100644 --- a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchFinderSvcImpl.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiMatchFinderSvcImpl.java @@ -22,7 +22,7 @@ package ca.uhn.fhir.jpa.empi.svc; import ca.uhn.fhir.empi.api.IEmpiMatchFinderSvc; import ca.uhn.fhir.empi.api.MatchedTarget; -import ca.uhn.fhir.empi.rules.svc.EmpiResourceComparatorSvc; +import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc; import org.hl7.fhir.instance.model.api.IAnyResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -37,7 +37,7 @@ public class EmpiMatchFinderSvcImpl implements IEmpiMatchFinderSvc { @Autowired private EmpiCandidateSearchSvc myEmpiCandidateSearchSvc; @Autowired - private EmpiResourceComparatorSvc myEmpiResourceComparatorSvc; + private EmpiResourceMatcherSvc myEmpiResourceMatcherSvc; @Override @Nonnull @@ -45,7 +45,7 @@ public class EmpiMatchFinderSvcImpl implements IEmpiMatchFinderSvc { Collection targetCandidates = myEmpiCandidateSearchSvc.findCandidates(theResourceType, theResource); return targetCandidates.stream() - .map(candidate -> new MatchedTarget(candidate, myEmpiResourceComparatorSvc.getMatchResult(theResource, candidate))) + .map(candidate -> new MatchedTarget(candidate, myEmpiResourceMatcherSvc.getMatchResult(theResource, candidate))) .collect(Collectors.toList()); } diff --git a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiSearchParamSvc.java b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiSearchParamSvc.java index 195abe1b4fa..42bc5d0b7c3 100644 --- a/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiSearchParamSvc.java +++ b/hapi-fhir-jpaserver-empi/src/main/java/ca/uhn/fhir/jpa/empi/svc/EmpiSearchParamSvc.java @@ -28,6 +28,7 @@ import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry; +import ca.uhn.fhir.rest.server.util.ISearchParamRetriever; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -35,7 +36,7 @@ import org.springframework.stereotype.Service; import java.util.List; @Service -public class EmpiSearchParamSvc { +public class EmpiSearchParamSvc implements ISearchParamRetriever { @Autowired FhirContext myFhirContext; @Autowired @@ -55,4 +56,9 @@ public class EmpiSearchParamSvc { RuntimeSearchParam activeSearchParam = mySearchParamRegistry.getActiveSearchParam(resourceType, theFilterSearchParam.getSearchParam()); return mySearchParamExtractorService.extractParamValuesAsStrings(activeSearchParam, theResource); } + + @Override + public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) { + return mySearchParamRegistry.getActiveSearchParam(theResourceName, theParamName); + } } diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/BaseEmpiR4Test.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/BaseEmpiR4Test.java index de9a7aadd01..6b0fa70e17b 100644 --- a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/BaseEmpiR4Test.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/BaseEmpiR4Test.java @@ -6,7 +6,7 @@ import ca.uhn.fhir.empi.api.EmpiLinkSourceEnum; import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; import ca.uhn.fhir.empi.api.IEmpiSettings; import ca.uhn.fhir.empi.model.EmpiTransactionContext; -import ca.uhn.fhir.empi.rules.svc.EmpiResourceComparatorSvc; +import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc; import ca.uhn.fhir.empi.util.EIDHelper; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; @@ -82,7 +82,7 @@ abstract public class BaseEmpiR4Test extends BaseJpaR4Test { @Autowired protected IFhirResourceDao myPractitionerDao; @Autowired - protected EmpiResourceComparatorSvc myEmpiResourceComparatorSvc; + protected EmpiResourceMatcherSvc myEmpiResourceMatcherSvc; @Autowired protected IEmpiLinkDao myEmpiLinkDao; @Autowired diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/config/BaseTestEmpiConfig.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/config/BaseTestEmpiConfig.java index ea27a5ec876..2eeebefc5e9 100644 --- a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/config/BaseTestEmpiConfig.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/config/BaseTestEmpiConfig.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.empi.config; import ca.uhn.fhir.empi.api.IEmpiSettings; +import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator; import ca.uhn.fhir.empi.rules.config.EmpiSettings; import ca.uhn.fhir.jpa.empi.helper.EmpiLinkHelper; import com.google.common.base.Charsets; @@ -23,11 +24,11 @@ public abstract class BaseTestEmpiConfig { boolean myPreventMultipleEids; @Bean - IEmpiSettings empiProperties() throws IOException { + IEmpiSettings empiSettings(EmpiRuleValidator theEmpiRuleValidator) throws IOException { DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); Resource resource = resourceLoader.getResource("empi/empi-rules.json"); String json = IOUtils.toString(resource.getInputStream(), Charsets.UTF_8); - return new EmpiSettings() + return new EmpiSettings(theEmpiRuleValidator) .setEnabled(false) .setScriptText(json) .setPreventEidUpdates(myPreventEidUpdates) diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/EmpiHelperConfig.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/EmpiHelperConfig.java index ca38066af87..8e9a91e1c56 100644 --- a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/EmpiHelperConfig.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/helper/EmpiHelperConfig.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.empi.helper; import ca.uhn.fhir.empi.api.IEmpiSettings; +import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator; import ca.uhn.fhir.empi.rules.config.EmpiSettings; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; @@ -26,13 +27,13 @@ public class EmpiHelperConfig { @Primary @Bean - IEmpiSettings empiProperties() throws IOException { + IEmpiSettings empiSettings(EmpiRuleValidator theEmpiRuleValidator) throws IOException { DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); Resource resource = resourceLoader.getResource("empi/empi-rules.json"); String json = IOUtils.toString(resource.getInputStream(), Charsets.UTF_8); // Set Enabled to true, and set strict mode. - return new EmpiSettings() + return new EmpiSettings(theEmpiRuleValidator) .setEnabled(true) .setScriptText(json) .setPreventEidUpdates(myPreventEidUpdates) diff --git a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkSvcTest.java b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkSvcTest.java index 1080b554e39..ea95311fac1 100644 --- a/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkSvcTest.java +++ b/hapi-fhir-jpaserver-empi/src/test/java/ca/uhn/fhir/jpa/empi/svc/EmpiLinkSvcTest.java @@ -36,7 +36,7 @@ public class EmpiLinkSvcTest extends BaseEmpiR4Test { public void compareEmptyPatients() { Patient patient = new Patient(); patient.setId("Patient/1"); - EmpiMatchResultEnum result = myEmpiResourceComparatorSvc.getMatchResult(patient, patient); + EmpiMatchResultEnum result = myEmpiResourceMatcherSvc.getMatchResult(patient, patient); assertEquals(EmpiMatchResultEnum.NO_MATCH, result); } diff --git a/hapi-fhir-jpaserver-empi/src/test/resources/empi/empi-rules.json b/hapi-fhir-jpaserver-empi/src/test/resources/empi/empi-rules.json index 78e77b6b054..b45f775d0f1 100644 --- a/hapi-fhir-jpaserver-empi/src/test/resources/empi/empi-rules.json +++ b/hapi-fhir-jpaserver-empi/src/test/resources/empi/empi-rules.json @@ -1,35 +1,46 @@ { - "candidateSearchParams" : [ { - "resourceType" : "Patient", - "searchParam" : "birthdate" - }, { - "resourceType" : "*", - "searchParam" : "identifier" - },{ - "resourceType" : "Patient", - "searchParam" : "general-practitioner" - } ], - "candidateFilterSearchParams" : [ { - "resourceType" : "*", - "searchParam" : "active", - "fixedValue" : "true" - } ], - "matchFields" : [ { - "name" : "given-name", - "resourceType" : "*", - "resourcePath" : "name.given", - "metric" : "COSINE", - "matchThreshold" : 0.8 - }, { - "name" : "last-name", - "resourceType" : "*", - "resourcePath" : "name.family", - "metric" : "JARO_WINKLER", - "matchThreshold" : 0.8 - }], - "matchResultMap" : { - "given-name" : "POSSIBLE_MATCH", - "given-name,last-name" : "MATCH" + "candidateSearchParams": [ + { + "resourceType": "Patient", + "searchParam": "birthdate" + }, + { + "resourceType": "*", + "searchParam": "identifier" + }, + { + "resourceType": "Patient", + "searchParam": "general-practitioner" + } + ], + "candidateFilterSearchParams": [ + { + "resourceType": "*", + "searchParam": "active", + "fixedValue": "true" + } + ], + "matchFields": [ + { + "name": "cosine-given-name", + "resourceType": "*", + "resourcePath": "name.given", + "metric": "COSINE", + "matchThreshold": 0.8, + "exact": true + }, + { + "name": "jaro-last-name", + "resourceType": "*", + "resourcePath": "name.family", + "metric": "JARO_WINKLER", + "matchThreshold": 0.8, + "exact": true + } + ], + "matchResultMap": { + "cosine-given-name" : "POSSIBLE_MATCH", + "cosine-given-name,jaro-last-name" : "MATCH" }, "eidSystem": "http://company.io/fhir/NamingSystem/custom-eid-system" } diff --git a/hapi-fhir-jpaserver-empi/src/test/resources/logback-test.xml b/hapi-fhir-jpaserver-empi/src/test/resources/logback-test.xml index 5f5bbc5a80f..3879bfb8568 100644 --- a/hapi-fhir-jpaserver-empi/src/test/resources/logback-test.xml +++ b/hapi-fhir-jpaserver-empi/src/test/resources/logback-test.xml @@ -59,7 +59,7 @@ 5MB - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n${log.stackfilter.pattern} + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n diff --git a/hapi-fhir-server-empi/pom.xml b/hapi-fhir-server-empi/pom.xml index 27a2e55245a..9f3c70d03a6 100644 --- a/hapi-fhir-server-empi/pom.xml +++ b/hapi-fhir-server-empi/pom.xml @@ -69,6 +69,10 @@ javax.annotation javax.annotation-api + + commons-codec + commons-codec + ch.qos.logback diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/config/EmpiRuleValidator.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/config/EmpiRuleValidator.java index 5d343bbafbb..2714d10abed 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/config/EmpiRuleValidator.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/config/EmpiRuleValidator.java @@ -21,20 +21,142 @@ package ca.uhn.fhir.empi.rules.config; */ import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.empi.api.EmpiConstants; +import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson; +import ca.uhn.fhir.empi.rules.json.EmpiFilterSearchParamJson; +import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson; import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.rest.server.util.ISearchParamRetriever; +import ca.uhn.fhir.util.FhirTerser; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.net.URI; import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.Set; @Service public class EmpiRuleValidator { + private static final Logger ourLog = LoggerFactory.getLogger(EmpiRuleValidator.class); + + private final FhirContext myFhirContext; + private final ISearchParamRetriever mySearchParamRetriever; + private final Class myPatientClass; + private final Class myPractitionerClass; + private final FhirTerser myTerser; + + @Autowired + public EmpiRuleValidator(FhirContext theFhirContext, ISearchParamRetriever theSearchParamRetriever) { + myFhirContext = theFhirContext; + myPatientClass = theFhirContext.getResourceDefinition("Patient").getImplementingClass(); + myPractitionerClass = theFhirContext.getResourceDefinition("Practitioner").getImplementingClass(); + myTerser = myFhirContext.newTerser(); + mySearchParamRetriever = theSearchParamRetriever; + } public void validate(EmpiRulesJson theEmpiRulesJson) { + validateSearchParams(theEmpiRulesJson); + validateMatchFields(theEmpiRulesJson); validateSystemIsUri(theEmpiRulesJson); } + private void validateSearchParams(EmpiRulesJson theEmpiRulesJson) { + for (EmpiResourceSearchParamJson searchParam : theEmpiRulesJson.getCandidateSearchParams()) { + validateSearchParam("candidateSearchParams", searchParam.getResourceType(), searchParam.getSearchParam()); + } + for (EmpiFilterSearchParamJson filter : theEmpiRulesJson.getCandidateFilterSearchParams()) { + validateSearchParam("candidateFilterSearchParams", filter.getResourceType(), filter.getSearchParam()); + } + } + + private void validateSearchParam(String theFieldName, String theTheResourceType, String theTheSearchParam) { + if (EmpiConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE.equals(theTheResourceType)) { + validateResourceSearchParam(theFieldName, "Patient", theTheSearchParam); + validateResourceSearchParam(theFieldName, "Practitioner", theTheSearchParam); + } else { + validateResourceSearchParam(theFieldName, theTheResourceType, theTheSearchParam); + } + } + + private void validateResourceSearchParam(String theFieldName, String theResourceType, String theSearchParam) { + if (mySearchParamRetriever.getActiveSearchParam(theResourceType, theSearchParam) == null) { + throw new ConfigurationException("Error in " + theFieldName + ": " + theResourceType + " does not have a search parameter called '" + theSearchParam + "'"); + } + } + + private void validateMatchFields(EmpiRulesJson theEmpiRulesJson) { + Set names = new HashSet<>(); + for (EmpiFieldMatchJson fieldMatch : theEmpiRulesJson.getMatchFields()) { + if (names.contains(fieldMatch.getName())) { + throw new ConfigurationException("Two MatchFields have the same name '" + fieldMatch.getName() + "'"); + } + names.add(fieldMatch.getName()); + validateThreshold(fieldMatch); + validatePath(fieldMatch); + } + } + + private void validateThreshold(EmpiFieldMatchJson theFieldMatch) { + if (theFieldMatch.getMetric().isSimilarity()) { + if (theFieldMatch.getMatchThreshold() == null) { + throw new ConfigurationException("MatchField " + theFieldMatch.getName() + " metric " + theFieldMatch.getMetric() + " requires a matchThreshold"); + } + } else if (theFieldMatch.getMatchThreshold() != null) { + throw new ConfigurationException("MatchField " + theFieldMatch.getName() + " metric " + theFieldMatch.getMetric() + " should not have a matchThreshold"); + } + } + + private void validatePath(EmpiFieldMatchJson theFieldMatch) { + String resourceType = theFieldMatch.getResourceType(); + if (EmpiConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE.equals(resourceType)) { + validatePatientPath(theFieldMatch); + validatePractitionerPath(theFieldMatch); + } else if ("Patient".equals(resourceType)) { + validatePatientPath(theFieldMatch); + } else if ("Practitioner".equals(resourceType)) { + validatePractitionerPath(theFieldMatch); + } else { + throw new ConfigurationException("MatchField " + theFieldMatch.getName() + " has unknown resourceType " + resourceType); + } + } + + private void validatePatientPath(EmpiFieldMatchJson theFieldMatch) { + try { + myTerser.getDefinition(myPatientClass, "Patient." + theFieldMatch.getResourcePath()); + } catch (DataFormatException|ConfigurationException e) { + throw new ConfigurationException("MatchField " + + theFieldMatch.getName() + + " resourceType " + + theFieldMatch.getResourceType() + + " has invalid path '" + theFieldMatch.getResourcePath() + "'. " + + e.getMessage()); + } + } + + private void validatePractitionerPath(EmpiFieldMatchJson theFieldMatch) { + try { + myTerser.getDefinition(myPractitionerClass, "Practitioner." + theFieldMatch.getResourcePath()); + } catch (DataFormatException e) { + throw new ConfigurationException("MatchField " + + theFieldMatch.getName() + + " resourceType " + + theFieldMatch.getResourceType() + + " has invalid path '" + theFieldMatch.getResourcePath() + "'. " + + e.getMessage()); + } + } + private void validateSystemIsUri(EmpiRulesJson theEmpiRulesJson) { + if (theEmpiRulesJson.getEnterpriseEIDSystem() == null) { + return; + } + try { new URI(theEmpiRulesJson.getEnterpriseEIDSystem()); } catch (URISyntaxException e) { diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/config/EmpiSettings.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/config/EmpiSettings.java index b08a66d1512..c7f778ca12f 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/config/EmpiSettings.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/config/EmpiSettings.java @@ -23,10 +23,15 @@ package ca.uhn.fhir.empi.rules.config; import ca.uhn.fhir.empi.api.IEmpiSettings; import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; import ca.uhn.fhir.util.JsonUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.io.IOException; +@Component public class EmpiSettings implements IEmpiSettings { + private final EmpiRuleValidator myEmpiRuleValidator; + private boolean myEnabled; private int myConcurrentConsumers = EMPI_DEFAULT_CONCURRENT_CONSUMERS; private String myScriptText; @@ -42,6 +47,11 @@ public class EmpiSettings implements IEmpiSettings { */ private boolean myPreventMultipleEids; + @Autowired + public EmpiSettings(EmpiRuleValidator theEmpiRuleValidator) { + myEmpiRuleValidator = theEmpiRuleValidator; + } + @Override public boolean isEnabled() { return myEnabled; @@ -68,7 +78,7 @@ public class EmpiSettings implements IEmpiSettings { public EmpiSettings setScriptText(String theScriptText) throws IOException { myScriptText = theScriptText; - myEmpiRules = JsonUtil.deserialize(theScriptText, EmpiRulesJson.class); + setEmpiRules(JsonUtil.deserialize(theScriptText, EmpiRulesJson.class)); return this; } @@ -88,6 +98,7 @@ public class EmpiSettings implements IEmpiSettings { } public EmpiSettings setEmpiRules(EmpiRulesJson theEmpiRules) { + myEmpiRuleValidator.validate(theEmpiRules); myEmpiRules = theEmpiRules; return this; } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/DistanceMetricEnum.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/DistanceMetricEnum.java deleted file mode 100644 index 2830bcfd121..00000000000 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/DistanceMetricEnum.java +++ /dev/null @@ -1,71 +0,0 @@ -package ca.uhn.fhir.empi.rules.json; - -/*- - * #%L - * HAPI FHIR - Enterprise Master Patient Index - * %% - * Copyright (C) 2014 - 2020 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.empi.rules.similarity.EmpiPersonNameMatchModeEnum; -import ca.uhn.fhir.empi.rules.similarity.HapiStringSimilarity; -import ca.uhn.fhir.empi.rules.similarity.IEmpiFieldSimilarity; -import ca.uhn.fhir.empi.rules.similarity.NameSimilarity; -import info.debatty.java.stringsimilarity.Cosine; -import info.debatty.java.stringsimilarity.Jaccard; -import info.debatty.java.stringsimilarity.JaroWinkler; -import info.debatty.java.stringsimilarity.NormalizedLevenshtein; -import info.debatty.java.stringsimilarity.SorensenDice; -import org.hl7.fhir.instance.model.api.IBase; - -/** - * Enum for holding all the known distance metrics that we support in HAPI for - * calculating differences between strings (https://en.wikipedia.org/wiki/String_metric) - */ -public enum DistanceMetricEnum implements IEmpiFieldSimilarity { - JARO_WINKLER("Jaro Winkler", new HapiStringSimilarity(new JaroWinkler())), - COSINE("Cosine", new HapiStringSimilarity(new Cosine())), - JACCARD("Jaccard", new HapiStringSimilarity(new Jaccard())), - NORMALIZED_LEVENSCHTEIN("Normalized Levenschtein", new HapiStringSimilarity(new NormalizedLevenshtein())), - SORENSEN_DICE("Sorensen Dice", new HapiStringSimilarity(new SorensenDice())), - STANDARD_NAME_ANY_ORDER("Standard name Any Order", new NameSimilarity(EmpiPersonNameMatchModeEnum.STANDARD_ANY_ORDER)), - EXACT_NAME_ANY_ORDER("Exact name Any Order", new NameSimilarity(EmpiPersonNameMatchModeEnum.EXACT_ANY_ORDER)), - STANDARD_NAME_FIRST_AND_LAST("Standard name First and Last", new NameSimilarity(EmpiPersonNameMatchModeEnum.STANDARD_FIRST_AND_LAST)), - EXACT_NAME_FIRST_AND_LAST("Exact name First and Last", new NameSimilarity(EmpiPersonNameMatchModeEnum.EXACT_FIRST_AND_LAST)); - - private final String myCode; - private final IEmpiFieldSimilarity myEmpiFieldSimilarity; - - DistanceMetricEnum(String theCode, IEmpiFieldSimilarity theEmpiFieldSimilarity) { - myCode = theCode; - myEmpiFieldSimilarity = theEmpiFieldSimilarity; - } - - public String getCode() { - return myCode; - } - - public IEmpiFieldSimilarity getEmpiFieldSimilarity() { - return myEmpiFieldSimilarity; - } - - @Override - public double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase) { - return myEmpiFieldSimilarity.similarity(theFhirContext ,theLeftBase, theRightBase); - } - -} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiFieldMatchJson.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiFieldMatchJson.java index 20f2b007041..a8014f1053b 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiFieldMatchJson.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiFieldMatchJson.java @@ -20,35 +20,42 @@ package ca.uhn.fhir.empi.rules.json; * #L% */ +import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum; import ca.uhn.fhir.model.api.IModelJson; import com.fasterxml.jackson.annotation.JsonProperty; import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * Contains all business data for determining if a match exists on a particular field, given: * - * 1. A {@link DistanceMetricEnum} which determines the actual similarity values. + * 1. A {@link EmpiMetricEnum} which determines the actual similarity values. * 2. A given resource type (e.g. Patient) * 3. A given FHIRPath expression for finding the particular primitive to be used for comparison. (e.g. name.given) */ public class EmpiFieldMatchJson implements IModelJson { - @JsonProperty("name") + @JsonProperty(value = "name", required = true) String myName; - @JsonProperty("resourceType") + @JsonProperty(value = "resourceType", required = true) String myResourceType; - @JsonProperty("resourcePath") + @JsonProperty(value = "resourcePath", required = true) String myResourcePath; - @JsonProperty("metric") - DistanceMetricEnum myMetric; + @JsonProperty(value = "metric", required = true) + EmpiMetricEnum myMetric; @JsonProperty("matchThreshold") - double myMatchThreshold; + Double myMatchThreshold; + /** + * For String value types, should the values be normalized (case, accents) before they are compared + */ + @JsonProperty(value = "exact") + boolean myExact; - public DistanceMetricEnum getMetric() { + public EmpiMetricEnum getMetric() { return myMetric; } - public EmpiFieldMatchJson setMetric(DistanceMetricEnum theMetric) { + public EmpiFieldMatchJson setMetric(EmpiMetricEnum theMetric) { myMetric = theMetric; return this; } @@ -71,7 +78,8 @@ public class EmpiFieldMatchJson implements IModelJson { return this; } - public double getMatchThreshold() { + @Nullable + public Double getMatchThreshold() { return myMatchThreshold; } @@ -88,4 +96,13 @@ public class EmpiFieldMatchJson implements IModelJson { myName = theName; return this; } + + public boolean getExact() { + return myExact; + } + + public EmpiFieldMatchJson setExact(boolean theExact) { + myExact = theExact; + return this; + } } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiFilterSearchParamJson.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiFilterSearchParamJson.java index 2313cf5903d..1db1ffd18b3 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiFilterSearchParamJson.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiFilterSearchParamJson.java @@ -29,13 +29,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; * candidate searching. e.g. When doing candidate matching, only consider candidates that match all EmpiFilterSearchParams. */ public class EmpiFilterSearchParamJson implements IModelJson { - @JsonProperty("resourceType") + @JsonProperty(value = "resourceType", required = true) String myResourceType; - @JsonProperty("searchParam") + @JsonProperty(value = "searchParam", required = true) String mySearchParam; - @JsonProperty("qualifier") + @JsonProperty(value = "qualifier", required = true) TokenParamModifier myTokenParamModifier; - @JsonProperty("fixedValue") + @JsonProperty(value = "fixedValue", required = true) String myFixedValue; public String getResourceType() { diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiResourceSearchParamJson.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiResourceSearchParamJson.java index 082b7c7b721..34b81575568 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiResourceSearchParamJson.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiResourceSearchParamJson.java @@ -27,9 +27,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; * */ public class EmpiResourceSearchParamJson implements IModelJson { - @JsonProperty("resourceType") + @JsonProperty(value = "resourceType", required = true) String myResourceType; - @JsonProperty("searchParam") + @JsonProperty(value = "searchParam", required = true) String mySearchParam; public String getResourceType() { diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiRulesJson.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiRulesJson.java index 8869bb77589..6d3c04e6525 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiRulesJson.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/EmpiRulesJson.java @@ -35,13 +35,13 @@ import java.util.Map; @JsonDeserialize(converter = EmpiRulesJson.EmpiRulesJsonConverter.class) public class EmpiRulesJson implements IModelJson { - @JsonProperty("candidateSearchParams") - List myResourceSearchParams = new ArrayList<>(); - @JsonProperty("candidateFilterSearchParams") - List myFilterSearchParams = new ArrayList<>(); - @JsonProperty("matchFields") + @JsonProperty(value = "candidateSearchParams", required = true) + List myCandidateSearchParams = new ArrayList<>(); + @JsonProperty(value = "candidateFilterSearchParams", required = true) + List myCandidateFilterSearchParams = new ArrayList<>(); + @JsonProperty(value = "matchFields", required = true) List myMatchFieldJsonList = new ArrayList<>(); - @JsonProperty("matchResultMap") + @JsonProperty(value = "matchResultMap", required = true) Map myMatchResultMap = new HashMap<>(); @JsonProperty(value = "eidSystem") String myEnterpriseEIDSystem; @@ -53,11 +53,11 @@ public class EmpiRulesJson implements IModelJson { } public void addResourceSearchParam(EmpiResourceSearchParamJson theSearchParam) { - myResourceSearchParams.add(theSearchParam); + myCandidateSearchParams.add(theSearchParam); } public void addFilterSearchParam(EmpiFilterSearchParamJson theSearchParam) { - myFilterSearchParams.add(theSearchParam); + myCandidateFilterSearchParams.add(theSearchParam); } int size() { @@ -73,8 +73,7 @@ public class EmpiRulesJson implements IModelJson { } public EmpiMatchResultEnum getMatchResult(Long theMatchVector) { - EmpiMatchResultEnum result = myVectorMatchResultMap.get(theMatchVector); - return (result == null) ? EmpiMatchResultEnum.NO_MATCH : result; + return myVectorMatchResultMap.get(theMatchVector); } public void putMatchResult(String theFieldMatchNames, EmpiMatchResultEnum theMatchResult) { @@ -97,12 +96,12 @@ public class EmpiRulesJson implements IModelJson { return Collections.unmodifiableList(myMatchFieldJsonList); } - public List getResourceSearchParams() { - return Collections.unmodifiableList(myResourceSearchParams); + public List getCandidateSearchParams() { + return Collections.unmodifiableList(myCandidateSearchParams); } - public List getFilterSearchParams() { - return Collections.unmodifiableList(myFilterSearchParams); + public List getCandidateFilterSearchParams() { + return Collections.unmodifiableList(myCandidateFilterSearchParams); } public String getEnterpriseEIDSystem() { @@ -132,8 +131,8 @@ public class EmpiRulesJson implements IModelJson { } public String getSummary() { - return myResourceSearchParams.size() + " Candidate Search Params, " + - myFilterSearchParams.size() + " Filter Search Params, " + + return myCandidateSearchParams.size() + " Candidate Search Params, " + + myCandidateFilterSearchParams.size() + " Filter Search Params, " + myMatchFieldJsonList.size() + " Match Fields, " + myMatchResultMap.size() + " Match Result Entries"; } @@ -142,6 +141,18 @@ public class EmpiRulesJson implements IModelJson { return myVectorMatchResultMap.getFieldMatchNames(theVector); } + public String getDetailedFieldMatchResultForUnmatchedVector(long theVector) { + List fieldMatchResult = new ArrayList<>(); + for (int i = 0; i < myMatchFieldJsonList.size(); ++i) { + if ((theVector & (1 << i)) == 0) { + fieldMatchResult.add(myMatchFieldJsonList.get(i).getName() + ": NO"); + } else { + fieldMatchResult.add(myMatchFieldJsonList.get(i).getName() + ": YES"); + } + } + return String.join("\n" ,fieldMatchResult); + } + @VisibleForTesting VectorMatchResultMap getVectorMatchResultMapForUnitTest() { return myVectorMatchResultMap; diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/VectorMatchResultMap.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/VectorMatchResultMap.java index 4aeb5094577..196c015109a 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/VectorMatchResultMap.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/json/VectorMatchResultMap.java @@ -20,15 +20,20 @@ package ca.uhn.fhir.empi.rules.json; * #L% */ +import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; import javax.annotation.Nonnull; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; public class VectorMatchResultMap { private final EmpiRulesJson myEmpiRulesJson; private Map myVectorToMatchResultMap = new HashMap<>(); + private Set myMatchVectors = new HashSet<>(); + private Set myPossibleMatchVectors = new HashSet<>(); private Map myVectorToFieldMatchNamesMap = new HashMap<>(); VectorMatchResultMap(EmpiRulesJson theEmpiRulesJson) { @@ -43,14 +48,30 @@ public class VectorMatchResultMap { } } + @Nonnull public EmpiMatchResultEnum get(Long theMatchVector) { - return myVectorToMatchResultMap.get(theMatchVector); + return myVectorToMatchResultMap.computeIfAbsent(theMatchVector, this::computeMatchResult); + } + + private EmpiMatchResultEnum computeMatchResult(Long theVector) { + if (myMatchVectors.stream().anyMatch(v -> (v & theVector) != 0)) { + return EmpiMatchResultEnum.MATCH; + } + if (myPossibleMatchVectors.stream().anyMatch(v -> (v & theVector) != 0)) { + return EmpiMatchResultEnum.POSSIBLE_MATCH; + } + return EmpiMatchResultEnum.NO_MATCH; } private void put(String theFieldMatchNames, EmpiMatchResultEnum theMatchResult) { long vector = getVector(theFieldMatchNames); myVectorToFieldMatchNamesMap.put(vector, theFieldMatchNames); myVectorToMatchResultMap.put(vector, theMatchResult); + if (theMatchResult == EmpiMatchResultEnum.MATCH) { + myMatchVectors.add(vector); + } else if (theMatchResult == EmpiMatchResultEnum.POSSIBLE_MATCH) { + myPossibleMatchVectors.add(vector); + } } public long getVector(String theFieldMatchNames) { @@ -58,7 +79,7 @@ public class VectorMatchResultMap { for (String fieldMatchName : splitFieldMatchNames(theFieldMatchNames)) { int index = getFieldMatchIndex(fieldMatchName); if (index == -1) { - throw new IllegalArgumentException("There is no matchField with name " + fieldMatchName); + throw new ConfigurationException("There is no matchField with name " + fieldMatchName); } retval |= (1 << index); } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/EmpiMetricEnum.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/EmpiMetricEnum.java new file mode 100644 index 00000000000..b50aa97047a --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/EmpiMetricEnum.java @@ -0,0 +1,89 @@ +package ca.uhn.fhir.empi.rules.metric; + +/*- + * #%L + * HAPI FHIR - Enterprise Master Patient Index + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.empi.rules.metric.matcher.DoubleMetaphoneStringMatcher; +import ca.uhn.fhir.empi.rules.metric.matcher.EmpiPersonNameMatchModeEnum; +import ca.uhn.fhir.empi.rules.metric.matcher.HapiDateMatcher; +import ca.uhn.fhir.empi.rules.metric.matcher.HapiStringMatcher; +import ca.uhn.fhir.empi.rules.metric.matcher.IEmpiFieldMatcher; +import ca.uhn.fhir.empi.rules.metric.matcher.MetaphoneStringMatcher; +import ca.uhn.fhir.empi.rules.metric.matcher.NameMatcher; +import ca.uhn.fhir.empi.rules.metric.matcher.StringEncoderMatcher; +import ca.uhn.fhir.empi.rules.metric.matcher.SubstringStringMatcher; +import ca.uhn.fhir.empi.rules.metric.similarity.HapiStringSimilarity; +import ca.uhn.fhir.empi.rules.metric.similarity.IEmpiFieldSimilarity; +import info.debatty.java.stringsimilarity.Cosine; +import info.debatty.java.stringsimilarity.Jaccard; +import info.debatty.java.stringsimilarity.JaroWinkler; +import info.debatty.java.stringsimilarity.NormalizedLevenshtein; +import info.debatty.java.stringsimilarity.SorensenDice; +import org.apache.commons.codec.language.Caverphone1; +import org.apache.commons.codec.language.Caverphone2; +import org.apache.commons.codec.language.Soundex; +import org.hl7.fhir.instance.model.api.IBase; + +import javax.annotation.Nullable; + +/** + * Enum for holding all the known distance metrics that we support in HAPI for + * calculating differences between strings (https://en.wikipedia.org/wiki/String_metric) + */ +public enum EmpiMetricEnum { + STRING(new HapiStringMatcher()), + SUBSTRING(new HapiStringMatcher(new SubstringStringMatcher())), + METAPHONE(new HapiStringMatcher(new MetaphoneStringMatcher())), + DOUBLE_METAPHONE(new HapiStringMatcher(new DoubleMetaphoneStringMatcher())), + SOUNDEX(new HapiStringMatcher(new StringEncoderMatcher(new Soundex()))), + CAVERPHONE1(new HapiStringMatcher(new StringEncoderMatcher(new Caverphone1()))), + CAVERPHONE2(new HapiStringMatcher(new StringEncoderMatcher(new Caverphone2()))), + DATE(new HapiDateMatcher()), + JARO_WINKLER(new HapiStringSimilarity(new JaroWinkler())), + COSINE(new HapiStringSimilarity(new Cosine())), + JACCARD(new HapiStringSimilarity(new Jaccard())), + LEVENSCHTEIN(new HapiStringSimilarity(new NormalizedLevenshtein())), + SORENSEN_DICE(new HapiStringSimilarity(new SorensenDice())), + NAME_ANY_ORDER(new NameMatcher(EmpiPersonNameMatchModeEnum.ANY_ORDER)), + NAME_FIRST_AND_LAST(new NameMatcher(EmpiPersonNameMatchModeEnum.FIRST_AND_LAST)); + + private final IEmpiFieldMetric myEmpiFieldMetric; + + EmpiMetricEnum(IEmpiFieldMetric theEmpiFieldMetric) { + myEmpiFieldMetric = theEmpiFieldMetric; + } + + public boolean match(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) { + return ((IEmpiFieldMatcher) myEmpiFieldMetric).matches(theFhirContext, theLeftBase, theRightBase, theExact); + } + + public boolean match(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, @Nullable Double theThreshold) { + if (isSimilarity()) { + return ((IEmpiFieldSimilarity) myEmpiFieldMetric).similarity(theFhirContext, theLeftBase, theRightBase, theExact) >= theThreshold; + } else { + return ((IEmpiFieldMatcher) myEmpiFieldMetric).matches(theFhirContext, theLeftBase, theRightBase, theExact); + } + } + + public boolean isSimilarity() { + return myEmpiFieldMetric instanceof IEmpiFieldSimilarity; + } +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/IEmpiFieldMetric.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/IEmpiFieldMetric.java new file mode 100644 index 00000000000..a13be076c28 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/IEmpiFieldMetric.java @@ -0,0 +1,4 @@ +package ca.uhn.fhir.empi.rules.metric; + +public interface IEmpiFieldMetric { +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/BaseHapiStringMetric.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/BaseHapiStringMetric.java new file mode 100644 index 00000000000..55b7e0a6c51 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/BaseHapiStringMetric.java @@ -0,0 +1,14 @@ +package ca.uhn.fhir.empi.rules.metric.matcher; + +import ca.uhn.fhir.util.StringUtil; +import org.hl7.fhir.instance.model.api.IPrimitiveType; + +public abstract class BaseHapiStringMetric { + protected String extractString(IPrimitiveType thePrimitive, boolean theExact) { + String theString = thePrimitive.getValueAsString(); + if (theExact) { + return theString; + } + return StringUtil.normalizeStringForSearchIndexing(theString); + } +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/DoubleMetaphoneStringMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/DoubleMetaphoneStringMatcher.java new file mode 100644 index 00000000000..78611b06365 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/DoubleMetaphoneStringMatcher.java @@ -0,0 +1,10 @@ +package ca.uhn.fhir.empi.rules.metric.matcher; + +import org.apache.commons.codec.language.DoubleMetaphone; + +public class DoubleMetaphoneStringMatcher implements IEmpiStringMatcher { + @Override + public boolean matches(String theLeftString, String theRightString) { + return new DoubleMetaphone().isDoubleMetaphoneEqual(theLeftString, theRightString); + } +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/EmpiPersonNameMatchModeEnum.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/EmpiPersonNameMatchModeEnum.java similarity index 85% rename from hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/EmpiPersonNameMatchModeEnum.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/EmpiPersonNameMatchModeEnum.java index fdbbf798ece..ee8b2735c16 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/EmpiPersonNameMatchModeEnum.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/EmpiPersonNameMatchModeEnum.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.empi.rules.similarity; +package ca.uhn.fhir.empi.rules.metric.matcher; /*- * #%L @@ -21,8 +21,6 @@ package ca.uhn.fhir.empi.rules.similarity; */ public enum EmpiPersonNameMatchModeEnum { - STANDARD_ANY_ORDER, - EXACT_ANY_ORDER, - STANDARD_FIRST_AND_LAST, - EXACT_FIRST_AND_LAST + ANY_ORDER, + FIRST_AND_LAST } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcher.java new file mode 100644 index 00000000000..8a46c98a423 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcher.java @@ -0,0 +1,21 @@ +package ca.uhn.fhir.empi.rules.metric.matcher; + +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.instance.model.api.IBase; + +public class HapiDateMatcher implements IEmpiFieldMatcher { + private final HapiDateMatcherDstu3 myHapiDateMatcherDstu3 = new HapiDateMatcherDstu3(); + private final HapiDateMatcherR4 myHapiDateMatcherR4 = new HapiDateMatcherR4(); + + @Override + public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) { + switch (theFhirContext.getVersion().getVersion()) { + case DSTU3: + return myHapiDateMatcherDstu3.match(theLeftBase, theRightBase); + case R4: + return myHapiDateMatcherR4.match(theLeftBase, theRightBase); + default: + throw new UnsupportedOperationException("Version not supported: " + theFhirContext.getVersion().getVersion()); + } + } +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherDstu3.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherDstu3.java new file mode 100644 index 00000000000..c0e0930a2fe --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherDstu3.java @@ -0,0 +1,40 @@ +package ca.uhn.fhir.empi.rules.metric.matcher; + +import org.hl7.fhir.dstu3.model.BaseDateTimeType; +import org.hl7.fhir.dstu3.model.DateTimeType; +import org.hl7.fhir.dstu3.model.DateType; +import org.hl7.fhir.instance.model.api.IBase; + +public class HapiDateMatcherDstu3 { + // TODO KHS code duplication (tried generalizing it with generics, but it got too convoluted) + public boolean match(IBase theLeftBase, IBase theRightBase) { + if (theLeftBase instanceof BaseDateTimeType && theRightBase instanceof BaseDateTimeType) { + BaseDateTimeType leftDate = (BaseDateTimeType) theLeftBase; + BaseDateTimeType rightDate = (BaseDateTimeType) theRightBase; + int comparison = leftDate.getPrecision().compareTo(rightDate.getPrecision()); + if (comparison == 0) { + return leftDate.getValueAsString().equals(rightDate.getValueAsString()); + } + BaseDateTimeType leftPDate; + BaseDateTimeType rightPDate; + if (comparison > 0) { + leftPDate = leftDate; + if (rightDate instanceof DateType) { + rightPDate = new DateType(rightDate.getValue(), leftDate.getPrecision()); + } else { + rightPDate = new DateTimeType(rightDate.getValue(), leftDate.getPrecision()); + } + } else { + rightPDate = rightDate; + if (leftDate instanceof DateType) { + leftPDate = new DateType(leftDate.getValue(), rightDate.getPrecision()); + } else { + leftPDate = new DateTimeType(leftDate.getValue(), rightDate.getPrecision()); + } + } + return leftPDate.getValueAsString().equals(rightPDate.getValueAsString()); + } + + return false; + } +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherR4.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherR4.java new file mode 100644 index 00000000000..6a7c2f27645 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherR4.java @@ -0,0 +1,40 @@ +package ca.uhn.fhir.empi.rules.metric.matcher; + +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.r4.model.BaseDateTimeType; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.DateType; + +public class HapiDateMatcherR4 { + // TODO KHS code duplication (tried generalizing it with generics, but it got too convoluted) + public boolean match(IBase theLeftBase, IBase theRightBase) { + if (theLeftBase instanceof BaseDateTimeType && theRightBase instanceof BaseDateTimeType) { + BaseDateTimeType leftDate = (BaseDateTimeType) theLeftBase; + BaseDateTimeType rightDate = (BaseDateTimeType) theRightBase; + int comparison = leftDate.getPrecision().compareTo(rightDate.getPrecision()); + if (comparison == 0) { + return leftDate.getValueAsString().equals(rightDate.getValueAsString()); + } + BaseDateTimeType leftPDate; + BaseDateTimeType rightPDate; + if (comparison > 0) { + leftPDate = leftDate; + if (rightDate instanceof DateType) { + rightPDate = new DateType(rightDate.getValue(), leftDate.getPrecision()); + } else { + rightPDate = new DateTimeType(rightDate.getValue(), leftDate.getPrecision()); + } + } else { + rightPDate = rightDate; + if (leftDate instanceof DateType) { + leftPDate = new DateType(leftDate.getValue(), rightDate.getPrecision()); + } else { + leftPDate = new DateTimeType(leftDate.getValue(), rightDate.getPrecision()); + } + } + return leftPDate.getValueAsString().equals(rightPDate.getValueAsString()); + } + + return false; + } +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiStringMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiStringMatcher.java new file mode 100644 index 00000000000..4efd84129a3 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiStringMatcher.java @@ -0,0 +1,52 @@ +package ca.uhn.fhir.empi.rules.metric.matcher; + +/*- + * #%L + * HAPI FHIR - Enterprise Master Patient Index + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IPrimitiveType; + +/** + * Similarity measure for two IBase fields whose similarity can be measured by their String representations. + */ +public class HapiStringMatcher extends BaseHapiStringMetric implements IEmpiFieldMatcher { + private final IEmpiStringMatcher myStringMatcher; + + public HapiStringMatcher(IEmpiStringMatcher theStringMatcher) { + myStringMatcher = theStringMatcher; + } + + public HapiStringMatcher() { + myStringMatcher = String::equals; + } + + @Override + public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) { + if (theLeftBase instanceof IPrimitiveType && theRightBase instanceof IPrimitiveType) { + String leftString = extractString((IPrimitiveType) theLeftBase, theExact); + String rightString = extractString((IPrimitiveType) theRightBase, theExact); + + return myStringMatcher.matches(leftString, rightString); + } + return false; + } + +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/ReferenceMatchSimilarity.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/IEmpiFieldMatcher.java similarity index 66% rename from hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/ReferenceMatchSimilarity.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/IEmpiFieldMatcher.java index eb65f7dc6f6..c13cbb0675f 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/ReferenceMatchSimilarity.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/IEmpiFieldMatcher.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.empi.rules.similarity; +package ca.uhn.fhir.empi.rules.metric.matcher; /*- * #%L @@ -21,12 +21,12 @@ package ca.uhn.fhir.empi.rules.similarity; */ import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.empi.rules.metric.IEmpiFieldMetric; import org.hl7.fhir.instance.model.api.IBase; -public class ReferenceMatchSimilarity implements IEmpiFieldSimilarity { - @Override - public double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase) { - System.out.println("wip!"); - return 1; - } +/** + * Measure how similar two IBase (resource fields) are to one another. 1.0 means identical. 0.0 means completely different. + */ +public interface IEmpiFieldMatcher extends IEmpiFieldMetric { + boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact); } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/IEmpiStringMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/IEmpiStringMatcher.java new file mode 100644 index 00000000000..d99fcc1a897 --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/IEmpiStringMatcher.java @@ -0,0 +1,5 @@ +package ca.uhn.fhir.empi.rules.metric.matcher; + +public interface IEmpiStringMatcher { + boolean matches(String theLeftString, String theRightString); +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/MetaphoneStringMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/MetaphoneStringMatcher.java new file mode 100644 index 00000000000..7694444322b --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/MetaphoneStringMatcher.java @@ -0,0 +1,10 @@ +package ca.uhn.fhir.empi.rules.metric.matcher; + +import org.apache.commons.codec.language.Metaphone; + +public class MetaphoneStringMatcher implements IEmpiStringMatcher { + @Override + public boolean matches(String theLeftString, String theRightString) { + return new Metaphone().isMetaphoneEqual(theLeftString, theRightString); + } +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/NameSimilarity.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/NameMatcher.java similarity index 78% rename from hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/NameSimilarity.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/NameMatcher.java index 045c1015e76..6fad4ffd01e 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/NameSimilarity.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/NameMatcher.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.empi.rules.similarity; +package ca.uhn.fhir.empi.rules.metric.matcher; /*- * #%L @@ -32,30 +32,28 @@ import java.util.stream.Collectors; /** * Similarity measure for two IBase name fields */ -public class NameSimilarity implements IEmpiFieldSimilarity { +public class NameMatcher implements IEmpiFieldMatcher { + private final EmpiPersonNameMatchModeEnum myMatchMode; - public NameSimilarity(EmpiPersonNameMatchModeEnum theMatchMode) { + public NameMatcher(EmpiPersonNameMatchModeEnum theMatchMode) { myMatchMode = theMatchMode; } @Override - public double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase) { + public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) { String leftFamilyName = NameUtil.extractFamilyName(theFhirContext, theLeftBase); String rightFamilyName = NameUtil.extractFamilyName(theFhirContext, theRightBase); if (StringUtils.isEmpty(leftFamilyName) || StringUtils.isEmpty(rightFamilyName)) { - return 0.0; + return false; } boolean match = false; - boolean exact = - myMatchMode == EmpiPersonNameMatchModeEnum.EXACT_ANY_ORDER || - myMatchMode == EmpiPersonNameMatchModeEnum.STANDARD_FIRST_AND_LAST; List leftGivenNames = NameUtil.extractGivenNames(theFhirContext, theLeftBase); List rightGivenNames = NameUtil.extractGivenNames(theFhirContext, theRightBase); - if (!exact) { + if (!theExact) { leftFamilyName = StringUtil.normalizeStringForSearchIndexing(leftFamilyName); rightFamilyName = StringUtil.normalizeStringForSearchIndexing(rightFamilyName); leftGivenNames = leftGivenNames.stream().map(StringUtil::normalizeStringForSearchIndexing).collect(Collectors.toList()); @@ -65,12 +63,12 @@ public class NameSimilarity implements IEmpiFieldSimilarity { for (String leftGivenName : leftGivenNames) { for (String rightGivenName : rightGivenNames) { match |= leftGivenName.equals(rightGivenName) && leftFamilyName.equals(rightFamilyName); - if (myMatchMode == EmpiPersonNameMatchModeEnum.STANDARD_ANY_ORDER || myMatchMode == EmpiPersonNameMatchModeEnum.EXACT_ANY_ORDER) { + if (myMatchMode == EmpiPersonNameMatchModeEnum.ANY_ORDER) { match |= leftGivenName.equals(rightFamilyName) && leftFamilyName.equals(rightGivenName); } } } - return match ? 1.0 : 0.0; + return match; } } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/StringEncoderMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/StringEncoderMatcher.java new file mode 100644 index 00000000000..1685e97eabe --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/StringEncoderMatcher.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.empi.rules.metric.matcher; + +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringEncoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class StringEncoderMatcher implements IEmpiStringMatcher { + private static final Logger ourLog = LoggerFactory.getLogger(StringEncoderMatcher.class); + + private final StringEncoder myStringEncoder; + + public StringEncoderMatcher(StringEncoder theStringEncoder) { + myStringEncoder = theStringEncoder; + } + + @Override + public boolean matches(String theLeftString, String theRightString) { + try { + return myStringEncoder.encode(theLeftString).equals(myStringEncoder.encode(theRightString)); + } catch (EncoderException e) { + ourLog.error("Failed to match strings '{}' and '{}' using encoder {}", theLeftString, theRightString, myStringEncoder.getClass().getName(), e); + } + return false; + } +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/SubstringStringMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/SubstringStringMatcher.java new file mode 100644 index 00000000000..0dc94d91c4d --- /dev/null +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/SubstringStringMatcher.java @@ -0,0 +1,8 @@ +package ca.uhn.fhir.empi.rules.metric.matcher; + +public class SubstringStringMatcher implements IEmpiStringMatcher { + @Override + public boolean matches(String theLeftString, String theRightString) { + return theLeftString.startsWith(theRightString) || theRightString.startsWith(theLeftString); + } +} diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/HapiStringSimilarity.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/similarity/HapiStringSimilarity.java similarity index 73% rename from hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/HapiStringSimilarity.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/similarity/HapiStringSimilarity.java index 7a04123e7fa..d04920c1d51 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/HapiStringSimilarity.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/similarity/HapiStringSimilarity.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.empi.rules.similarity; +package ca.uhn.fhir.empi.rules.metric.similarity; /*- * #%L @@ -21,6 +21,7 @@ package ca.uhn.fhir.empi.rules.similarity; */ import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.empi.rules.metric.matcher.BaseHapiStringMetric; import info.debatty.java.stringsimilarity.interfaces.NormalizedStringSimilarity; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IPrimitiveType; @@ -28,18 +29,20 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; /** * Similarity measure for two IBase fields whose similarity can be measured by their String representations. */ -public class HapiStringSimilarity implements IEmpiFieldSimilarity { +public class HapiStringSimilarity extends BaseHapiStringMetric implements IEmpiFieldSimilarity { private final NormalizedStringSimilarity myStringSimilarity; public HapiStringSimilarity(NormalizedStringSimilarity theStringSimilarity) { myStringSimilarity = theStringSimilarity; } - public double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase) { + @Override + public double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact) { if (theLeftBase instanceof IPrimitiveType && theRightBase instanceof IPrimitiveType) { - IPrimitiveType leftString = (IPrimitiveType) theLeftBase; - IPrimitiveType rightString = (IPrimitiveType) theRightBase; - return myStringSimilarity.similarity(leftString.getValueAsString(), rightString.getValueAsString()); + String leftString = extractString((IPrimitiveType) theLeftBase, theExact); + String rightString = extractString((IPrimitiveType) theRightBase, theExact); + + return myStringSimilarity.similarity(leftString, rightString); } return 0.0; } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/IEmpiFieldSimilarity.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/similarity/IEmpiFieldSimilarity.java similarity index 82% rename from hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/IEmpiFieldSimilarity.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/similarity/IEmpiFieldSimilarity.java index e3ee1c23b10..0594224324b 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/similarity/IEmpiFieldSimilarity.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/similarity/IEmpiFieldSimilarity.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.empi.rules.similarity; +package ca.uhn.fhir.empi.rules.metric.similarity; /*- * #%L @@ -21,11 +21,12 @@ package ca.uhn.fhir.empi.rules.similarity; */ import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.empi.rules.metric.IEmpiFieldMetric; import org.hl7.fhir.instance.model.api.IBase; /** * Measure how similar two IBase (resource fields) are to one another. 1.0 means identical. 0.0 means completely different. */ -public interface IEmpiFieldSimilarity { - double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase); +public interface IEmpiFieldSimilarity extends IEmpiFieldMetric { + double similarity(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact); } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldComparator.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldMatcher.java similarity index 92% rename from hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldComparator.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldMatcher.java index f3d39a55c0b..f3eb57c57b5 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldComparator.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldMatcher.java @@ -34,13 +34,13 @@ import static ca.uhn.fhir.empi.api.EmpiConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE; /** * This class is responsible for performing matching between raw-typed values of a left record and a right record. */ -public class EmpiResourceFieldComparator { +public class EmpiResourceFieldMatcher { private final FhirContext myFhirContext; private final EmpiFieldMatchJson myEmpiFieldMatchJson; private final String myResourceType; private final String myResourcePath; - public EmpiResourceFieldComparator(FhirContext theFhirContext, EmpiFieldMatchJson theEmpiFieldMatchJson) { + public EmpiResourceFieldMatcher(FhirContext theFhirContext, EmpiFieldMatchJson theEmpiFieldMatchJson) { myFhirContext = theFhirContext; myEmpiFieldMatchJson = theEmpiFieldMatchJson; myResourceType = theEmpiFieldMatchJson.getResourceType(); @@ -80,7 +80,7 @@ public class EmpiResourceFieldComparator { } private boolean match(IBase theLeftValue, IBase theRightValue) { - return myEmpiFieldMatchJson.getMetric().similarity(myFhirContext, theLeftValue, theRightValue) >= myEmpiFieldMatchJson.getMatchThreshold(); + return myEmpiFieldMatchJson.getMetric().match(myFhirContext, theLeftValue, theRightValue, myEmpiFieldMatchJson.getExact(), myEmpiFieldMatchJson.getMatchThreshold()); } private void validate(IBaseResource theResource) { diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceComparatorSvc.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceMatcherSvc.java similarity index 77% rename from hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceComparatorSvc.java rename to hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceMatcherSvc.java index 3712d168223..46989304448 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceComparatorSvc.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceMatcherSvc.java @@ -43,16 +43,16 @@ import java.util.List; */ @Service -public class EmpiResourceComparatorSvc { +public class EmpiResourceMatcherSvc { private static final Logger ourLog = Logs.getEmpiTroubleshootingLog(); private final FhirContext myFhirContext; private final IEmpiSettings myEmpiConfig; private EmpiRulesJson myEmpiRulesJson; - private final List myFieldComparators = new ArrayList<>(); + private final List myFieldMatchers = new ArrayList<>(); @Autowired - public EmpiResourceComparatorSvc(FhirContext theFhirContext, IEmpiSettings theEmpiConfig) { + public EmpiResourceMatcherSvc(FhirContext theFhirContext, IEmpiSettings theEmpiConfig) { myFhirContext = theFhirContext; myEmpiConfig = theEmpiConfig; } @@ -64,7 +64,7 @@ public class EmpiResourceComparatorSvc { throw new ConfigurationException("Failed to load EMPI Rules. If EMPI is enabled, then EMPI rules must be available in context."); } for (EmpiFieldMatchJson matchFieldJson : myEmpiRulesJson.getMatchFields()) { - myFieldComparators.add(new EmpiResourceFieldComparator(myFhirContext, matchFieldJson)); + myFieldMatchers.add(new EmpiResourceFieldMatcher(myFhirContext, matchFieldJson)); } } @@ -79,14 +79,18 @@ public class EmpiResourceComparatorSvc { * @return an {@link EmpiMatchResultEnum} indicating the result of the comparison. */ public EmpiMatchResultEnum getMatchResult(IBaseResource theLeftResource, IBaseResource theRightResource) { - return compare(theLeftResource, theRightResource); + return match(theLeftResource, theRightResource); } - EmpiMatchResultEnum compare(IBaseResource theLeftResource, IBaseResource theRightResource) { + EmpiMatchResultEnum match(IBaseResource theLeftResource, IBaseResource theRightResource) { long matchVector = getMatchVector(theLeftResource, theRightResource); EmpiMatchResultEnum matchResult = myEmpiRulesJson.getMatchResult(matchVector); - if (ourLog.isTraceEnabled() && matchResult == EmpiMatchResultEnum.MATCH || matchResult == EmpiMatchResultEnum.POSSIBLE_MATCH) { - ourLog.trace("{} {} with field matchers {}", matchResult, theRightResource.getIdElement().toUnqualifiedVersionless(), myEmpiRulesJson.getFieldMatchNamesForVector(matchVector)); + if (ourLog.isDebugEnabled()) { + if (matchResult == EmpiMatchResultEnum.MATCH || matchResult == EmpiMatchResultEnum.POSSIBLE_MATCH) { + ourLog.debug("{} {} with field matchers {}", matchResult, theRightResource.getIdElement().toUnqualifiedVersionless(), myEmpiRulesJson.getFieldMatchNamesForVector(matchVector)); + } else if (ourLog.isTraceEnabled()) { + ourLog.trace("{} {}. Field matcher results: {}", matchResult, theRightResource.getIdElement().toUnqualifiedVersionless(), myEmpiRulesJson.getDetailedFieldMatchResultForUnmatchedVector(matchVector)); + } } return matchResult; } @@ -108,9 +112,9 @@ public class EmpiResourceComparatorSvc { */ private long getMatchVector(IBaseResource theLeftResource, IBaseResource theRightResource) { long retval = 0; - for (int i = 0; i < myFieldComparators.size(); ++i) { + for (int i = 0; i < myFieldMatchers.size(); ++i) { //any that are not for the resourceType in question. - EmpiResourceFieldComparator fieldComparator = myFieldComparators.get(i); + EmpiResourceFieldMatcher fieldComparator = myFieldMatchers.get(i); if (fieldComparator.match(theLeftResource, theRightResource)) { retval |= (1 << i); } diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/BaseR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/BaseR4Test.java index 71e55e90631..495f3e5d107 100644 --- a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/BaseR4Test.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/BaseR4Test.java @@ -1,39 +1,21 @@ package ca.uhn.fhir.empi; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator; import ca.uhn.fhir.empi.rules.config.EmpiSettings; -import ca.uhn.fhir.empi.rules.json.DistanceMetricEnum; -import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson; -import ca.uhn.fhir.empi.rules.json.EmpiFilterSearchParamJson; -import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson; import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; -import ca.uhn.fhir.empi.rules.svc.EmpiResourceComparatorSvc; +import ca.uhn.fhir.empi.rules.svc.EmpiResourceMatcherSvc; +import ca.uhn.fhir.rest.server.util.ISearchParamRetriever; import org.hl7.fhir.r4.model.Patient; -import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import static org.mockito.Mockito.mock; + +@RunWith(MockitoJUnitRunner.class) public abstract class BaseR4Test { protected static final FhirContext ourFhirContext = FhirContext.forR4(); - public static final String PATIENT_GIVEN = "patient-given"; - public static final String PATIENT_LAST = "patient-last"; - public static final String PATIENT_GENERAL_PRACTITIONER= "patient-practitioner"; - - - public static final double NAME_THRESHOLD = 0.8; - protected EmpiFieldMatchJson myGivenNameMatchField; - protected EmpiFieldMatchJson myParentMatchField; - protected String myBothNameFields; - - @Before - public void before() { - myGivenNameMatchField = new EmpiFieldMatchJson() - .setName(PATIENT_GIVEN) - .setResourceType("Patient") - .setResourcePath("name.given") - .setMetric(DistanceMetricEnum.COSINE) - .setMatchThreshold(NAME_THRESHOLD); - myBothNameFields = String.join(",", PATIENT_GIVEN, PATIENT_LAST); - } + protected ISearchParamRetriever mySearchParamRetriever = mock(ISearchParamRetriever.class); protected Patient buildJohn() { Patient patient = new Patient(); @@ -49,40 +31,8 @@ public abstract class BaseR4Test { return patient; } - protected EmpiRulesJson buildActiveBirthdateIdRules() { - EmpiFilterSearchParamJson activePatientsBlockingFilter = new EmpiFilterSearchParamJson() - .setResourceType("Patient") - .setSearchParam(Patient.SP_ACTIVE) - .setFixedValue("true"); - - EmpiResourceSearchParamJson patientBirthdayBlocking = new EmpiResourceSearchParamJson() - .setResourceType("Patient") - .setSearchParam(Patient.SP_BIRTHDATE); - EmpiResourceSearchParamJson patientIdentifierBlocking = new EmpiResourceSearchParamJson() - .setResourceType("Patient") - .setSearchParam(Patient.SP_IDENTIFIER); - - - EmpiFieldMatchJson lastNameMatchField = new EmpiFieldMatchJson() - .setName(PATIENT_LAST) - .setResourceType("Patient") - .setResourcePath("name.family") - .setMetric(DistanceMetricEnum.JARO_WINKLER) - .setMatchThreshold(NAME_THRESHOLD); - - EmpiRulesJson retval = new EmpiRulesJson(); - retval.addResourceSearchParam(patientBirthdayBlocking); - retval.addResourceSearchParam(patientIdentifierBlocking); - retval.addFilterSearchParam(activePatientsBlockingFilter); - retval.addMatchField(myGivenNameMatchField); - retval.addMatchField(lastNameMatchField); - retval.putMatchResult(myBothNameFields, EmpiMatchResultEnum.MATCH); - retval.putMatchResult(PATIENT_GIVEN, EmpiMatchResultEnum.POSSIBLE_MATCH); - return retval; - } - - protected EmpiResourceComparatorSvc buildComparator(EmpiRulesJson theEmpiRulesJson) { - EmpiResourceComparatorSvc retval = new EmpiResourceComparatorSvc(ourFhirContext, new EmpiSettings().setEmpiRules(theEmpiRulesJson)); + protected EmpiResourceMatcherSvc buildMatcher(EmpiRulesJson theEmpiRulesJson) { + EmpiResourceMatcherSvc retval = new EmpiResourceMatcherSvc(ourFhirContext, new EmpiSettings(new EmpiRuleValidator(ourFhirContext, mySearchParamRetriever)).setEmpiRules(theEmpiRulesJson)); retval.init(); return retval; } diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/config/EmpiRuleValidatorTest.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/config/EmpiRuleValidatorTest.java index 19f1576f57d..260f5af9dfa 100644 --- a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/config/EmpiRuleValidatorTest.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/config/EmpiRuleValidatorTest.java @@ -1,7 +1,7 @@ package ca.uhn.fhir.empi.rules.config; import ca.uhn.fhir.context.ConfigurationException; -import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; +import ca.uhn.fhir.empi.BaseR4Test; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.junit.Test; @@ -11,37 +11,99 @@ import org.springframework.core.io.Resource; import java.io.IOException; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.startsWith; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; -public class EmpiRuleValidatorTest { - private EmpiRuleValidator myEmpiRuleValidator = new EmpiRuleValidator(); - +public class EmpiRuleValidatorTest extends BaseR4Test { @Test - public void testValidate() { - String invalidUri = "invalid uri"; - EmpiRulesJson sampleEmpiRulesJson = new EmpiRulesJson(); - sampleEmpiRulesJson.setEnterpriseEIDSystem(invalidUri); - + public void testValidate() throws IOException { try { - myEmpiRuleValidator.validate(sampleEmpiRulesJson); + setEmpiRuleJson("bad-rules-bad-url.json"); fail(); } catch (ConfigurationException e){ assertThat(e.getMessage(), is("Enterprise Identifier System (eidSystem) must be a valid URI")); } } - - @Test + + @Test public void testNonExistentMatchField() throws IOException { - EmpiSettings empiSettings = new EmpiSettings(); - DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); - Resource resource = resourceLoader.getResource("bad-rules.json"); - String json = IOUtils.toString(resource.getInputStream(), Charsets.UTF_8); try { - empiSettings.setScriptText(json); + setEmpiRuleJson("bad-rules-missing-name.json"); fail(); - } catch (IllegalArgumentException e) { + } catch (ConfigurationException e) { assertThat(e.getMessage(), is("There is no matchField with name foo")); } } + + @Test + public void testSimilarityHasThreshold() throws IOException { + try { + setEmpiRuleJson("bad-rules-missing-threshold.json"); + fail(); + } catch (ConfigurationException e) { + assertThat(e.getMessage(), is("MatchField given-name metric COSINE requires a matchThreshold")); + } + } + + @Test + public void testMatcherUnusedThreshold() throws IOException { + try { + setEmpiRuleJson("bad-rules-unused-threshold.json"); + fail(); + } catch (ConfigurationException e) { + assertThat(e.getMessage(), is("MatchField given-name metric STRING should not have a matchThreshold")); + } + } + + @Test + public void testMatcherBadPath() throws IOException { + try { + setEmpiRuleJson("bad-rules-bad-path.json"); + fail(); + } catch (ConfigurationException e) { + assertThat(e.getMessage(), startsWith("MatchField given-name resourceType Patient has invalid path 'name.first'. Unknown child name 'first' in element HumanName")); + } + } + + @Test + public void testMatcherBadSearchParam() throws IOException { + try { + setEmpiRuleJson("bad-rules-bad-searchparam.json"); + fail(); + } catch (ConfigurationException e) { + assertThat(e.getMessage(), startsWith("Error in candidateSearchParams: Patient does not have a search parameter called 'foo'")); + } + } + + @Test + public void testMatcherBadFilter() throws IOException { + try { + setEmpiRuleJson("bad-rules-bad-filter.json"); + fail(); + } catch (ConfigurationException e) { + assertThat(e.getMessage(), startsWith("Error in candidateFilterSearchParams: Patient does not have a search parameter called 'foo'")); + } + } + + @Test + public void testMatcherduplicateName() throws IOException { + try { + setEmpiRuleJson("bad-rules-duplicate-name.json"); + fail(); + } catch (ConfigurationException e) { + assertThat(e.getMessage(), startsWith("Two MatchFields have the same name 'foo'")); + } + } + + + private void setEmpiRuleJson(String theTheS) throws IOException { + EmpiRuleValidator empiRuleValidator = new EmpiRuleValidator(ourFhirContext, mySearchParamRetriever); + EmpiSettings empiSettings = new EmpiSettings(empiRuleValidator); + DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); + Resource resource = resourceLoader.getResource(theTheS); + String json = IOUtils.toString(resource.getInputStream(), Charsets.UTF_8); + empiSettings.setScriptText(json); + } + } diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/json/EmpiRulesJsonR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/json/EmpiRulesJsonR4Test.java index 683deda7f22..c4237ebf94d 100644 --- a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/json/EmpiRulesJsonR4Test.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/json/EmpiRulesJsonR4Test.java @@ -1,7 +1,9 @@ package ca.uhn.fhir.empi.rules.json; -import ca.uhn.fhir.empi.BaseR4Test; +import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum; +import ca.uhn.fhir.empi.rules.svc.BaseEmpiRulesR4Test; import ca.uhn.fhir.util.JsonUtil; import junit.framework.TestCase; import org.junit.Before; @@ -14,7 +16,7 @@ import java.io.IOException; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.fail; -public class EmpiRulesJsonR4Test extends BaseR4Test { +public class EmpiRulesJsonR4Test extends BaseEmpiRulesR4Test { private static final Logger ourLog = LoggerFactory.getLogger(EmpiRulesJsonR4Test.class); private EmpiRulesJson myRules; @@ -34,7 +36,7 @@ public class EmpiRulesJsonR4Test extends BaseR4Test { assertEquals(EmpiMatchResultEnum.MATCH, rulesDeser.getMatchResult(myBothNameFields)); EmpiFieldMatchJson second = rulesDeser.get(1); assertEquals("name.family", second.getResourcePath()); - TestCase.assertEquals(DistanceMetricEnum.JARO_WINKLER, second.getMetric()); + TestCase.assertEquals(EmpiMetricEnum.JARO_WINKLER, second.getMetric()); } @Test @@ -54,7 +56,7 @@ public class EmpiRulesJsonR4Test extends BaseR4Test { try { vectorMatchResultMap.getVector("bad"); fail(); - } catch (IllegalArgumentException e) { + } catch (ConfigurationException e) { assertEquals("There is no matchField with name bad", e.getMessage()); } } diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/json/VectorMatchResultMapTest.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/json/VectorMatchResultMapTest.java index 502d1530c51..645dd4322c9 100644 --- a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/json/VectorMatchResultMapTest.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/json/VectorMatchResultMapTest.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.empi.rules.json; +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum; import org.junit.Test; import static junit.framework.TestCase.assertEquals; @@ -21,4 +23,19 @@ public class VectorMatchResultMapTest { assertEquals("b", result[1]); } } + + @Test + public void testMatchBeforePossibleMatch() { + EmpiRulesJson empiRulesJson = new EmpiRulesJson(); + empiRulesJson.addMatchField(new EmpiFieldMatchJson().setName("given").setResourceType("Patient").setResourcePath("name.given").setMetric(EmpiMetricEnum.STRING)); + empiRulesJson.addMatchField(new EmpiFieldMatchJson().setName("family").setResourceType("Patient").setResourcePath("name.family").setMetric(EmpiMetricEnum.STRING)); + empiRulesJson.addMatchField(new EmpiFieldMatchJson().setName("prefix").setResourceType("Patient").setResourcePath("name.prefix").setMetric(EmpiMetricEnum.STRING)); + empiRulesJson.putMatchResult("given,family", EmpiMatchResultEnum.MATCH); + empiRulesJson.putMatchResult("given", EmpiMatchResultEnum.POSSIBLE_MATCH); + + VectorMatchResultMap map = new VectorMatchResultMap(empiRulesJson); + assertEquals(EmpiMatchResultEnum.POSSIBLE_MATCH, map.get(1L)); + assertEquals(EmpiMatchResultEnum.MATCH, map.get(3L)); + assertEquals(EmpiMatchResultEnum.MATCH, map.get(7L)); + } } diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/metric/matcher/BaseMatcherR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/metric/matcher/BaseMatcherR4Test.java new file mode 100644 index 00000000000..eeba0e5dbac --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/metric/matcher/BaseMatcherR4Test.java @@ -0,0 +1,7 @@ +package ca.uhn.fhir.empi.rules.metric.matcher; + +import ca.uhn.fhir.context.FhirContext; + +public abstract class BaseMatcherR4Test { + protected static final FhirContext ourFhirContext = FhirContext.forR4(); +} diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/metric/matcher/DateMatcherR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/metric/matcher/DateMatcherR4Test.java new file mode 100644 index 00000000000..2fbfaf54c0f --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/metric/matcher/DateMatcherR4Test.java @@ -0,0 +1,89 @@ +package ca.uhn.fhir.empi.rules.metric.matcher; + +import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum; +import ca.uhn.fhir.model.api.TemporalPrecisionEnum; +import org.hl7.fhir.r4.model.DateTimeType; +import org.hl7.fhir.r4.model.DateType; +import org.junit.Test; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class DateMatcherR4Test extends BaseMatcherR4Test { + + @Test + public void testExactDatePrecision() { + Calendar cal = new GregorianCalendar(2020,6,15); + Calendar sameMonthCal = new GregorianCalendar(2020,6,22); + Calendar sameYearCal = new GregorianCalendar(2020,11,13); + Calendar otherYearCal = new GregorianCalendar(1965,8,9); + + Date date = cal.getTime(); + Date sameMonth = sameMonthCal.getTime(); + Date sameYear = sameYearCal.getTime(); + Date otherYear = otherYearCal.getTime(); + + assertTrue(dateMatch(date, date, TemporalPrecisionEnum.DAY)); + assertFalse(dateMatch(date, sameMonth, TemporalPrecisionEnum.DAY)); + assertFalse(dateMatch(date, sameYear, TemporalPrecisionEnum.DAY)); + assertFalse(dateMatch(date, otherYear, TemporalPrecisionEnum.DAY)); + + assertTrue(dateMatch(date, date, TemporalPrecisionEnum.MONTH)); + assertTrue(dateMatch(date, sameMonth, TemporalPrecisionEnum.MONTH)); + assertFalse(dateMatch(date, sameYear, TemporalPrecisionEnum.MONTH)); + assertFalse(dateMatch(date, otherYear, TemporalPrecisionEnum.MONTH)); + + assertTrue(dateMatch(date, date, TemporalPrecisionEnum.YEAR)); + assertTrue(dateMatch(date, sameMonth, TemporalPrecisionEnum.YEAR)); + assertTrue(dateMatch(date, sameYear, TemporalPrecisionEnum.YEAR)); + assertFalse(dateMatch(date, otherYear, TemporalPrecisionEnum.YEAR)); + } + + private boolean dateMatch(Date theDate, Date theSameMonth, TemporalPrecisionEnum theTheDay) { + return EmpiMetricEnum.DATE.match(ourFhirContext, new DateType(theDate, theTheDay), new DateType(theSameMonth, theTheDay), true); + } + + @Test + public void testExactDateTimePrecision() { + Calendar cal = new GregorianCalendar(2020,6,15, 11, 12, 13); + Calendar sameSecondCal = new GregorianCalendar(2020,6,15, 11, 12, 13); + sameSecondCal.add(Calendar.MILLISECOND, 123); + + Calendar sameDayCal = new GregorianCalendar(2020,6,15, 12, 34, 56); + + Date date = cal.getTime(); + Date sameSecond = sameSecondCal.getTime(); + Date sameDay = sameDayCal.getTime(); + + // Same precision + + assertTrue(dateTimeMatch(date, date, TemporalPrecisionEnum.DAY, TemporalPrecisionEnum.DAY)); + assertTrue(dateTimeMatch(date, sameSecond, TemporalPrecisionEnum.DAY, TemporalPrecisionEnum.DAY)); + assertTrue(dateTimeMatch(date, sameDay, TemporalPrecisionEnum.DAY, TemporalPrecisionEnum.DAY)); + + assertTrue(dateTimeMatch(date, date, TemporalPrecisionEnum.SECOND, TemporalPrecisionEnum.SECOND)); + assertTrue(dateTimeMatch(date, sameSecond, TemporalPrecisionEnum.SECOND, TemporalPrecisionEnum.SECOND)); + assertFalse(dateTimeMatch(date, sameDay, TemporalPrecisionEnum.SECOND, TemporalPrecisionEnum.SECOND)); + + assertTrue(dateTimeMatch(date, date, TemporalPrecisionEnum.MILLI, TemporalPrecisionEnum.MILLI)); + assertFalse(dateTimeMatch(date, sameSecond, TemporalPrecisionEnum.MILLI, TemporalPrecisionEnum.MILLI)); + assertFalse(dateTimeMatch(date, sameDay, TemporalPrecisionEnum.MILLI, TemporalPrecisionEnum.MILLI)); + + // Different precision matches by coarser precision + assertTrue(dateTimeMatch(date, date, TemporalPrecisionEnum.SECOND, TemporalPrecisionEnum.DAY)); + assertTrue(dateTimeMatch(date, sameSecond, TemporalPrecisionEnum.SECOND, TemporalPrecisionEnum.DAY)); + assertFalse(dateTimeMatch(date, sameDay, TemporalPrecisionEnum.SECOND, TemporalPrecisionEnum.DAY)); + + assertTrue(dateTimeMatch(date, date, TemporalPrecisionEnum.DAY, TemporalPrecisionEnum.SECOND)); + assertTrue(dateTimeMatch(date, sameSecond, TemporalPrecisionEnum.DAY, TemporalPrecisionEnum.SECOND)); + assertFalse(dateTimeMatch(date, sameDay, TemporalPrecisionEnum.DAY, TemporalPrecisionEnum.SECOND)); + } + + private boolean dateTimeMatch(Date theDate, Date theSameSecond, TemporalPrecisionEnum theTheDay, TemporalPrecisionEnum theTheDay2) { + return EmpiMetricEnum.DATE.match(ourFhirContext, new DateTimeType(theDate, theTheDay), new DateTimeType(theSameSecond, theTheDay2), true); + } +} diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/metric/matcher/StringMatcherR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/metric/matcher/StringMatcherR4Test.java new file mode 100644 index 00000000000..1891426038a --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/metric/matcher/StringMatcherR4Test.java @@ -0,0 +1,132 @@ +package ca.uhn.fhir.empi.rules.metric.matcher; + +import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.Enumeration; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.StringType; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class StringMatcherR4Test extends BaseMatcherR4Test { + @Test + public void testMetaphone() { + assertTrue(match(EmpiMetricEnum.METAPHONE, new StringType("Durie"), new StringType("dury"))); + assertTrue(match(EmpiMetricEnum.METAPHONE, new StringType("Balo"), new StringType("ballo"))); + assertTrue(match(EmpiMetricEnum.METAPHONE, new StringType("Hans Peter"), new StringType("Hanspeter"))); + assertTrue(match(EmpiMetricEnum.METAPHONE, new StringType("Lawson"), new StringType("Law son"))); + + assertFalse(match(EmpiMetricEnum.METAPHONE, new StringType("Allsop"), new StringType("Allsob"))); + assertFalse(match(EmpiMetricEnum.METAPHONE, new StringType("Gevne"), new StringType("Geve"))); + assertFalse(match(EmpiMetricEnum.METAPHONE, new StringType("Bruce"), new StringType("Bruch"))); + assertFalse(match(EmpiMetricEnum.METAPHONE, new StringType("Smith"), new StringType("Schmidt"))); + assertFalse(match(EmpiMetricEnum.METAPHONE, new StringType("Jyothi"), new StringType("Jyoti"))); + } + + @Test + public void testDoubleMetaphone() { + assertTrue(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Durie"), new StringType("dury"))); + assertTrue(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Balo"), new StringType("ballo"))); + assertTrue(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Hans Peter"), new StringType("Hanspeter"))); + assertTrue(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Lawson"), new StringType("Law son"))); + assertTrue(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Allsop"), new StringType("Allsob"))); + + assertFalse(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Gevne"), new StringType("Geve"))); + assertFalse(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Bruce"), new StringType("Bruch"))); + assertFalse(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Smith"), new StringType("Schmidt"))); + assertFalse(match(EmpiMetricEnum.DOUBLE_METAPHONE, new StringType("Jyothi"), new StringType("Jyoti"))); + } + + @Test + public void testNormalizeCase() { + assertTrue(match(EmpiMetricEnum.STRING, new StringType("joe"), new StringType("JoE"))); + assertTrue(match(EmpiMetricEnum.STRING, new StringType("MCTAVISH"), new StringType("McTavish"))); + + assertFalse(match(EmpiMetricEnum.STRING, new StringType("joey"), new StringType("joe"))); + assertFalse(match(EmpiMetricEnum.STRING, new StringType("joe"), new StringType("joey"))); + } + + @Test + public void testExactString() { + assertTrue(EmpiMetricEnum.STRING.match(ourFhirContext, new StringType("Jilly"), new StringType("Jilly"), true)); + + assertFalse(EmpiMetricEnum.STRING.match(ourFhirContext, new StringType("MCTAVISH"), new StringType("McTavish"), true)); + assertFalse(EmpiMetricEnum.STRING.match(ourFhirContext, new StringType("Durie"), new StringType("dury"), true)); + } + + @Test + public void testExactBoolean() { + assertTrue(EmpiMetricEnum.STRING.match(ourFhirContext, new BooleanType(true), new BooleanType(true), true)); + + assertFalse(EmpiMetricEnum.STRING.match(ourFhirContext, new BooleanType(true), new BooleanType(false), true)); + assertFalse(EmpiMetricEnum.STRING.match(ourFhirContext, new BooleanType(false), new BooleanType(true), true)); + } + + @Test + public void testExactDateString() { + assertTrue(EmpiMetricEnum.STRING.match(ourFhirContext, new DateType("1965-08-09"), new DateType("1965-08-09"), true)); + + assertFalse(EmpiMetricEnum.STRING.match(ourFhirContext, new DateType("1965-08-09"), new DateType("1965-09-08"), true)); + } + + + @Test + public void testExactGender() { + Enumeration male = new Enumeration(new Enumerations.AdministrativeGenderEnumFactory()); + male.setValue(Enumerations.AdministrativeGender.MALE); + + Enumeration female = new Enumeration(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); + } +} diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/BaseEmpiRulesR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/BaseEmpiRulesR4Test.java new file mode 100644 index 00000000000..f9c02d9ac89 --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/BaseEmpiRulesR4Test.java @@ -0,0 +1,63 @@ +package ca.uhn.fhir.empi.rules.svc; + +import ca.uhn.fhir.empi.BaseR4Test; +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson; +import ca.uhn.fhir.empi.rules.json.EmpiFilterSearchParamJson; +import ca.uhn.fhir.empi.rules.json.EmpiResourceSearchParamJson; +import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; +import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum; +import org.hl7.fhir.r4.model.Patient; +import org.junit.Before; + +public abstract class BaseEmpiRulesR4Test extends BaseR4Test { + public static final String PATIENT_GIVEN = "patient-given"; + public static final String PATIENT_LAST = "patient-last"; + + public static final double NAME_THRESHOLD = 0.8; + protected EmpiFieldMatchJson myGivenNameMatchField; + protected String myBothNameFields; + + @Before + public void before() { + myGivenNameMatchField = new EmpiFieldMatchJson() + .setName(PATIENT_GIVEN) + .setResourceType("Patient") + .setResourcePath("name.given") + .setMetric(EmpiMetricEnum.COSINE) + .setMatchThreshold(NAME_THRESHOLD); + myBothNameFields = String.join(",", PATIENT_GIVEN, PATIENT_LAST); + } + + protected EmpiRulesJson buildActiveBirthdateIdRules() { + EmpiFilterSearchParamJson activePatientsBlockingFilter = new EmpiFilterSearchParamJson() + .setResourceType("Patient") + .setSearchParam(Patient.SP_ACTIVE) + .setFixedValue("true"); + + EmpiResourceSearchParamJson patientBirthdayBlocking = new EmpiResourceSearchParamJson() + .setResourceType("Patient") + .setSearchParam(Patient.SP_BIRTHDATE); + EmpiResourceSearchParamJson patientIdentifierBlocking = new EmpiResourceSearchParamJson() + .setResourceType("Patient") + .setSearchParam(Patient.SP_IDENTIFIER); + + + EmpiFieldMatchJson lastNameMatchField = new EmpiFieldMatchJson() + .setName(PATIENT_LAST) + .setResourceType("Patient") + .setResourcePath("name.family") + .setMetric(EmpiMetricEnum.JARO_WINKLER) + .setMatchThreshold(NAME_THRESHOLD); + + EmpiRulesJson retval = new EmpiRulesJson(); + retval.addResourceSearchParam(patientBirthdayBlocking); + retval.addResourceSearchParam(patientIdentifierBlocking); + retval.addFilterSearchParam(activePatientsBlockingFilter); + retval.addMatchField(myGivenNameMatchField); + retval.addMatchField(lastNameMatchField); + retval.putMatchResult(myBothNameFields, EmpiMatchResultEnum.MATCH); + retval.putMatchResult(PATIENT_GIVEN, EmpiMatchResultEnum.POSSIBLE_MATCH); + return retval; + } +} diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/CustomResourceComparatorR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/CustomResourceComparatorR4Test.java deleted file mode 100644 index c0ee1ae0352..00000000000 --- a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/CustomResourceComparatorR4Test.java +++ /dev/null @@ -1,117 +0,0 @@ -package ca.uhn.fhir.empi.rules.svc; - -import ca.uhn.fhir.empi.BaseR4Test; -import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; -import ca.uhn.fhir.empi.rules.json.DistanceMetricEnum; -import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson; -import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; -import org.hl7.fhir.r4.model.HumanName; -import org.hl7.fhir.r4.model.Patient; -import org.junit.BeforeClass; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class CustomResourceComparatorR4Test extends BaseR4Test { - - public static final String FIELD_EXACT_MATCH_NAME = DistanceMetricEnum.EXACT_NAME_ANY_ORDER.name(); - private static Patient ourJohnHenry; - private static Patient ourJohnHENRY; - private static Patient ourJaneHenry; - private static Patient ourJohnSmith; - private static Patient ourJohnBillyHenry; - private static Patient ourBillyJohnHenry; - private static Patient ourHenryJohn; - private static Patient ourHenryJOHN; - - @BeforeClass - public static void beforeClass() { - ourJohnHenry = buildPatientWithNames("Henry", "John"); - ourJohnHENRY = buildPatientWithNames("HENRY", "John"); - ourJaneHenry = buildPatientWithNames("Henry", "Jane"); - ourJohnSmith = buildPatientWithNames("Smith", "John"); - ourJohnBillyHenry = buildPatientWithNames("Henry", "John", "Billy"); - ourBillyJohnHenry = buildPatientWithNames("Henry", "Billy", "John"); - ourHenryJohn = buildPatientWithNames("John", "Henry"); - ourHenryJOHN = buildPatientWithNames("JOHN", "Henry"); - } - - @Test - public void testExactNameAnyOrder() { - EmpiResourceComparatorSvc nameAnyOrderComparator = buildComparator(buildNameAnyOrderRules(DistanceMetricEnum.EXACT_NAME_ANY_ORDER)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHenry)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJohn)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJOHN)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHENRY)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJaneHenry)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnSmith)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnBillyHenry)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourBillyJohnHenry)); - } - - @Test - public void testStandardNameAnyOrder() { - EmpiResourceComparatorSvc nameAnyOrderComparator = buildComparator(buildNameAnyOrderRules(DistanceMetricEnum.STANDARD_NAME_ANY_ORDER)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHenry)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJohn)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJOHN)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHENRY)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJaneHenry)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnSmith)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnBillyHenry)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourBillyJohnHenry)); - } - - - @Test - public void testExactNameFirstAndLast() { - EmpiResourceComparatorSvc nameAnyOrderComparator = buildComparator(buildNameAnyOrderRules(DistanceMetricEnum.EXACT_NAME_FIRST_AND_LAST)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHenry)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJohn)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJOHN)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHENRY)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJaneHenry)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnSmith)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnBillyHenry)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourBillyJohnHenry)); - } - - @Test - public void testStandardNameFirstAndLast() { - EmpiResourceComparatorSvc nameAnyOrderComparator = buildComparator(buildNameAnyOrderRules(DistanceMetricEnum.STANDARD_NAME_FIRST_AND_LAST)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHenry)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJohn)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourHenryJOHN)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnHENRY)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJaneHenry)); - assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnSmith)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourJohnBillyHenry)); - assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderComparator.compare(ourJohnHenry, ourBillyJohnHenry)); - } - - protected static Patient buildPatientWithNames(String theFamilyName, String... theGivenNames) { - Patient patient = new Patient(); - HumanName name = patient.addName(); - name.setFamily(theFamilyName); - for (String givenName : theGivenNames) { - name.addGiven(givenName); - } - patient.setId("Patient/1"); - return patient; - } - - private EmpiRulesJson buildNameAnyOrderRules(DistanceMetricEnum theExactNameAnyOrder) { - EmpiFieldMatchJson nameAnyOrderFieldMatch = new EmpiFieldMatchJson() - .setName(FIELD_EXACT_MATCH_NAME) - .setResourceType("Patient") - .setResourcePath("name") - .setMetric(theExactNameAnyOrder) - .setMatchThreshold(NAME_THRESHOLD); - - EmpiRulesJson retval = new EmpiRulesJson(); - retval.addMatchField(nameAnyOrderFieldMatch); - retval.putMatchResult(FIELD_EXACT_MATCH_NAME, EmpiMatchResultEnum.MATCH); - - return retval; - } -} diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/CustomResourceMatcherR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/CustomResourceMatcherR4Test.java new file mode 100644 index 00000000000..8ba7436397f --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/CustomResourceMatcherR4Test.java @@ -0,0 +1,117 @@ +package ca.uhn.fhir.empi.rules.svc; + +import ca.uhn.fhir.empi.BaseR4Test; +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson; +import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; +import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum; +import org.hl7.fhir.r4.model.HumanName; +import org.hl7.fhir.r4.model.Patient; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class CustomResourceMatcherR4Test extends BaseR4Test { + + public static final String FIELD_EXACT_MATCH_NAME = EmpiMetricEnum.NAME_ANY_ORDER.name(); + private static Patient ourJohnHenry; + private static Patient ourJohnHENRY; + private static Patient ourJaneHenry; + private static Patient ourJohnSmith; + private static Patient ourJohnBillyHenry; + private static Patient ourBillyJohnHenry; + private static Patient ourHenryJohn; + private static Patient ourHenryJOHN; + + @BeforeClass + public static void beforeClass() { + ourJohnHenry = buildPatientWithNames("Henry", "John"); + ourJohnHENRY = buildPatientWithNames("HENRY", "John"); + ourJaneHenry = buildPatientWithNames("Henry", "Jane"); + ourJohnSmith = buildPatientWithNames("Smith", "John"); + ourJohnBillyHenry = buildPatientWithNames("Henry", "John", "Billy"); + ourBillyJohnHenry = buildPatientWithNames("Henry", "Billy", "John"); + ourHenryJohn = buildPatientWithNames("John", "Henry"); + ourHenryJOHN = buildPatientWithNames("JOHN", "Henry"); + } + + @Test + public void testExactNameAnyOrder() { + EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_ANY_ORDER, true)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry)); + } + + @Test + public void testNormalizedNameAnyOrder() { + EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_ANY_ORDER, false)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry)); + } + + + @Test + public void testExactNameFirstAndLast() { + EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_FIRST_AND_LAST, true)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry)); + } + + @Test + public void testNormalizedNameFirstAndLast() { + EmpiResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(EmpiMetricEnum.NAME_FIRST_AND_LAST, false)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHENRY)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJaneHenry)); + assertEquals(EmpiMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnSmith)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnBillyHenry)); + assertEquals(EmpiMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry)); + } + + protected static Patient buildPatientWithNames(String theFamilyName, String... theGivenNames) { + Patient patient = new Patient(); + HumanName name = patient.addName(); + name.setFamily(theFamilyName); + for (String givenName : theGivenNames) { + name.addGiven(givenName); + } + patient.setId("Patient/1"); + return patient; + } + + private EmpiRulesJson buildNameRules(EmpiMetricEnum theExactNameAnyOrder, boolean theExact) { + EmpiFieldMatchJson nameAnyOrderFieldMatch = new EmpiFieldMatchJson() + .setName(FIELD_EXACT_MATCH_NAME) + .setResourceType("Patient") + .setResourcePath("name") + .setMetric(theExactNameAnyOrder) + .setExact(theExact); + + EmpiRulesJson retval = new EmpiRulesJson(); + retval.addMatchField(nameAnyOrderFieldMatch); + retval.putMatchResult(FIELD_EXACT_MATCH_NAME, EmpiMatchResultEnum.MATCH); + + return retval; + } +} diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceComparatorSvcR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceComparatorSvcR4Test.java deleted file mode 100644 index 484842cbf70..00000000000 --- a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceComparatorSvcR4Test.java +++ /dev/null @@ -1,53 +0,0 @@ -package ca.uhn.fhir.empi.rules.svc; - -import ca.uhn.fhir.empi.BaseR4Test; -import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; -import org.hl7.fhir.r4.model.Patient; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class EmpiResourceComparatorSvcR4Test extends BaseR4Test { - private EmpiResourceComparatorSvc myEmpiResourceComparatorSvc; - public static final double NAME_DELTA = 0.0001; - - private Patient myJohn; - private Patient myJohny; - - @Before - public void before() { - super.before(); - - myEmpiResourceComparatorSvc = buildComparator(buildActiveBirthdateIdRules()); - - myJohn = buildJohn(); - myJohny = buildJohny(); - } - - @Test - public void testCompareFirstNameMatch() { - EmpiMatchResultEnum result = myEmpiResourceComparatorSvc.compare(myJohn, myJohny); - assertEquals(EmpiMatchResultEnum.POSSIBLE_MATCH, result); - } - - @Test - public void testCompareBothNamesMatch() { - myJohn.addName().setFamily("Smith"); - myJohny.addName().setFamily("Smith"); - EmpiMatchResultEnum result = myEmpiResourceComparatorSvc.compare(myJohn, myJohny); - assertEquals(EmpiMatchResultEnum.MATCH, result); - } - - @Test - public void testMatchResult() { - assertEquals(EmpiMatchResultEnum.POSSIBLE_MATCH, myEmpiResourceComparatorSvc.getMatchResult(myJohn, myJohny)); - myJohn.addName().setFamily("Smith"); - myJohny.addName().setFamily("Smith"); - assertEquals(EmpiMatchResultEnum.MATCH, myEmpiResourceComparatorSvc.getMatchResult(myJohn, myJohny)); - Patient patient3 = new Patient(); - patient3.setId("Patient/3"); - patient3.addName().addGiven("Henry"); - assertEquals(EmpiMatchResultEnum.NO_MATCH, myEmpiResourceComparatorSvc.getMatchResult(myJohn, patient3)); - } -} diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldComparatorR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldMatcherR4Test.java similarity index 81% rename from hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldComparatorR4Test.java rename to hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldMatcherR4Test.java index 52b12346952..72f5128ad82 100644 --- a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldComparatorR4Test.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceFieldMatcherR4Test.java @@ -1,8 +1,7 @@ package ca.uhn.fhir.empi.rules.svc; -import ca.uhn.fhir.empi.BaseR4Test; -import ca.uhn.fhir.empi.rules.json.DistanceMetricEnum; import ca.uhn.fhir.empi.rules.json.EmpiFieldMatchJson; +import ca.uhn.fhir.empi.rules.metric.EmpiMetricEnum; import ca.uhn.fhir.parser.DataFormatException; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Patient; @@ -16,8 +15,8 @@ import static junit.framework.TestCase.fail; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.StringStartsWith.startsWith; -public class EmpiResourceFieldComparatorR4Test extends BaseR4Test { - protected EmpiResourceFieldComparator myComparator; +public class EmpiResourceFieldMatcherR4Test extends BaseEmpiRulesR4Test { + protected EmpiResourceFieldMatcher myComparator; private Patient myJohn; private Patient myJohny; @@ -25,7 +24,8 @@ public class EmpiResourceFieldComparatorR4Test extends BaseR4Test { public void before() { super.before(); - myComparator = new EmpiResourceFieldComparator(ourFhirContext, myGivenNameMatchField); + + myComparator = new EmpiResourceFieldMatcher(ourFhirContext, myGivenNameMatchField); myJohn = buildJohn(); myJohny = buildJohny(); } @@ -64,9 +64,9 @@ public class EmpiResourceFieldComparatorR4Test extends BaseR4Test { .setName("patient-foo") .setResourceType("Patient") .setResourcePath("foo") - .setMetric(DistanceMetricEnum.COSINE) + .setMetric(EmpiMetricEnum.COSINE) .setMatchThreshold(NAME_THRESHOLD); - EmpiResourceFieldComparator comparator = new EmpiResourceFieldComparator(ourFhirContext, matchField); + EmpiResourceFieldMatcher comparator = new EmpiResourceFieldMatcher(ourFhirContext, matchField); comparator.match(myJohn, myJohny); fail(); } catch (DataFormatException e) { diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceMatcherSvcR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceMatcherSvcR4Test.java new file mode 100644 index 00000000000..8843614ed2b --- /dev/null +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/rules/svc/EmpiResourceMatcherSvcR4Test.java @@ -0,0 +1,59 @@ +package ca.uhn.fhir.empi.rules.svc; + +import ca.uhn.fhir.context.RuntimeSearchParam; +import ca.uhn.fhir.empi.api.EmpiMatchResultEnum; +import org.hl7.fhir.r4.model.Patient; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class EmpiResourceMatcherSvcR4Test extends BaseEmpiRulesR4Test { + private EmpiResourceMatcherSvc myEmpiResourceMatcherSvc; + public static final double NAME_DELTA = 0.0001; + + private Patient myJohn; + private Patient myJohny; + + @Before + public void before() { + super.before(); + + when(mySearchParamRetriever.getActiveSearchParam("Patient", "birthdate")).thenReturn(mock(RuntimeSearchParam.class)); + when(mySearchParamRetriever.getActiveSearchParam("Patient", "identifier")).thenReturn(mock(RuntimeSearchParam.class)); + when(mySearchParamRetriever.getActiveSearchParam("Patient", "active")).thenReturn(mock(RuntimeSearchParam.class)); + + myEmpiResourceMatcherSvc = buildMatcher(buildActiveBirthdateIdRules()); + + myJohn = buildJohn(); + myJohny = buildJohny(); + } + + @Test + public void testCompareFirstNameMatch() { + EmpiMatchResultEnum result = myEmpiResourceMatcherSvc.match(myJohn, myJohny); + assertEquals(EmpiMatchResultEnum.POSSIBLE_MATCH, result); + } + + @Test + public void testCompareBothNamesMatch() { + myJohn.addName().setFamily("Smith"); + myJohny.addName().setFamily("Smith"); + EmpiMatchResultEnum result = myEmpiResourceMatcherSvc.match(myJohn, myJohny); + assertEquals(EmpiMatchResultEnum.MATCH, result); + } + + @Test + public void testMatchResult() { + assertEquals(EmpiMatchResultEnum.POSSIBLE_MATCH, myEmpiResourceMatcherSvc.getMatchResult(myJohn, myJohny)); + myJohn.addName().setFamily("Smith"); + myJohny.addName().setFamily("Smith"); + assertEquals(EmpiMatchResultEnum.MATCH, myEmpiResourceMatcherSvc.getMatchResult(myJohn, myJohny)); + Patient patient3 = new Patient(); + patient3.setId("Patient/3"); + patient3.addName().addGiven("Henry"); + assertEquals(EmpiMatchResultEnum.NO_MATCH, myEmpiResourceMatcherSvc.getMatchResult(myJohn, patient3)); + } +} diff --git a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/EIDHelperR4Test.java b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/EIDHelperR4Test.java index 8d71d0956bb..d17e10b3be5 100644 --- a/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/EIDHelperR4Test.java +++ b/hapi-fhir-server-empi/src/test/java/ca/uhn/fhir/empi/svc/EIDHelperR4Test.java @@ -1,12 +1,15 @@ package ca.uhn.fhir.empi.svc; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.empi.BaseR4Test; import ca.uhn.fhir.empi.model.CanonicalEID; +import ca.uhn.fhir.empi.rules.config.EmpiRuleValidator; import ca.uhn.fhir.empi.rules.config.EmpiSettings; import ca.uhn.fhir.empi.rules.json.EmpiRulesJson; import ca.uhn.fhir.empi.util.EIDHelper; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Patient; +import org.junit.Before; import org.junit.Test; import java.util.List; @@ -18,21 +21,26 @@ import static org.hamcrest.CoreMatchers.nullValue; import static org.junit.Assert.assertThat; -public class EIDHelperR4Test { +public class EIDHelperR4Test extends BaseR4Test { - private static final FhirContext myFhirContext = FhirContext.forR4(); + private static final FhirContext ourFhirContext = FhirContext.forR4(); private static final String EXTERNAL_ID_SYSTEM_FOR_TEST = "http://testsystem.io/naming-system/empi"; - private static final EmpiRulesJson myRules = new EmpiRulesJson() {{ + private static final EmpiRulesJson ourRules = new EmpiRulesJson() {{ setEnterpriseEIDSystem(EXTERNAL_ID_SYSTEM_FOR_TEST); }}; - private static final EmpiSettings mySettings = new EmpiSettings() {{ - setEmpiRules(myRules); - }}; + private EmpiSettings myEmpiSettings; - private static final EIDHelper EID_HELPER = new EIDHelper(myFhirContext, mySettings); + private EIDHelper myEidHelper; + @Before + public void before() { + myEmpiSettings = new EmpiSettings(new EmpiRuleValidator(ourFhirContext, mySearchParamRetriever)) {{ + setEmpiRules(ourRules); + }}; + myEidHelper = new EIDHelper(ourFhirContext, myEmpiSettings); + } @Test public void testExtractionOfInternalEID() { @@ -42,7 +50,7 @@ public class EIDHelperR4Test { .setValue("simpletest") .setUse(Identifier.IdentifierUse.SECONDARY); - List externalEid = EID_HELPER.getHapiEid(patient); + List externalEid = myEidHelper.getHapiEid(patient); assertThat(externalEid.isEmpty(), is(false)); assertThat(externalEid.get(0).getValue(), is(equalTo("simpletest"))); @@ -59,7 +67,7 @@ public class EIDHelperR4Test { .setSystem(EXTERNAL_ID_SYSTEM_FOR_TEST) .setValue(uniqueID); - List externalEid = EID_HELPER.getExternalEid(patient); + List externalEid = myEidHelper.getExternalEid(patient); assertThat(externalEid.isEmpty(), is(false)); assertThat(externalEid.get(0).getValue(), is(equalTo(uniqueID))); @@ -69,7 +77,7 @@ public class EIDHelperR4Test { @Test public void testCreationOfInternalEIDGeneratesUuidEID() { - CanonicalEID internalEid = EID_HELPER.createHapiEid(); + CanonicalEID internalEid = myEidHelper.createHapiEid(); assertThat(internalEid.getSystem(), is(equalTo(HAPI_ENTERPRISE_IDENTIFIER_SYSTEM))); assertThat(internalEid.getValue().length(), is(equalTo(36))); diff --git a/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-filter.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-filter.json new file mode 100644 index 00000000000..56cf810d2d7 --- /dev/null +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-filter.json @@ -0,0 +1,9 @@ +{ + "candidateSearchParams" : [], + "candidateFilterSearchParams" : [{ + "resourceType" : "Patient", + "searchParam" : "foo" + }], + "matchFields" : [], + "matchResultMap" : {} +} diff --git a/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-path.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-path.json new file mode 100644 index 00000000000..c26b88df3fc --- /dev/null +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-path.json @@ -0,0 +1,14 @@ +{ + "candidateSearchParams" : [], + "candidateFilterSearchParams" : [], + "matchFields" : [ { + "name" : "given-name", + "resourceType" : "Patient", + "resourcePath" : "name.first", + "metric" : "STRING", + "exact" : true + }], + "matchResultMap" : { + "given-name" : "POSSIBLE_MATCH" + } +} diff --git a/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-searchparam.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-searchparam.json new file mode 100644 index 00000000000..b9f1274f771 --- /dev/null +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-searchparam.json @@ -0,0 +1,9 @@ +{ + "candidateSearchParams" : [{ + "resourceType" : "Patient", + "searchParam" : "foo" + }], + "candidateFilterSearchParams" : [], + "matchFields" : [], + "matchResultMap" : {} +} diff --git a/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-url.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-url.json new file mode 100644 index 00000000000..ba386469223 --- /dev/null +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-bad-url.json @@ -0,0 +1,7 @@ +{ + "candidateSearchParams" : [], + "candidateFilterSearchParams" : [], + "matchFields" : [], + "matchResultMap" : {}, + "eidSystem": "invalid url" +} diff --git a/hapi-fhir-server-empi/src/test/resources/bad-rules-duplicate-name.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-duplicate-name.json new file mode 100644 index 00000000000..f5a6b78d362 --- /dev/null +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-duplicate-name.json @@ -0,0 +1,22 @@ +{ + "candidateSearchParams": [], + "candidateFilterSearchParams": [], + "matchFields": [ + { + "name": "foo", + "resourceType": "Patient", + "resourcePath": "name.family", + "metric": "STRING", + "exact": true + }, + { + "name": "foo", + "resourceType": "Patient", + "resourcePath": "name.given", + "metric": "STRING" + } + ], + "matchResultMap": { + "foo": "POSSIBLE_MATCH" + } +} diff --git a/hapi-fhir-server-empi/src/test/resources/bad-rules.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-missing-name.json similarity index 80% rename from hapi-fhir-server-empi/src/test/resources/bad-rules.json rename to hapi-fhir-server-empi/src/test/resources/bad-rules-missing-name.json index 360c93908f5..6859e9f4339 100644 --- a/hapi-fhir-server-empi/src/test/resources/bad-rules.json +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-missing-name.json @@ -11,6 +11,5 @@ "matchResultMap" : { "given-name" : "POSSIBLE_MATCH", "foo" : "MATCH" - }, - "eidSystem": "http://company.io/fhir/NamingSystem/custom-eid-system" + } } diff --git a/hapi-fhir-server-empi/src/test/resources/bad-rules-missing-threshold.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-missing-threshold.json new file mode 100644 index 00000000000..f0f5c1762e3 --- /dev/null +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-missing-threshold.json @@ -0,0 +1,13 @@ +{ + "candidateSearchParams" : [], + "candidateFilterSearchParams" : [], + "matchFields" : [ { + "name" : "given-name", + "resourceType" : "*", + "resourcePath" : "name.given", + "metric" : "COSINE" + }], + "matchResultMap" : { + "given-name" : "POSSIBLE_MATCH" + } +} diff --git a/hapi-fhir-server-empi/src/test/resources/bad-rules-unused-threshold.json b/hapi-fhir-server-empi/src/test/resources/bad-rules-unused-threshold.json new file mode 100644 index 00000000000..719de741126 --- /dev/null +++ b/hapi-fhir-server-empi/src/test/resources/bad-rules-unused-threshold.json @@ -0,0 +1,14 @@ +{ + "candidateSearchParams" : [], + "candidateFilterSearchParams" : [], + "matchFields" : [ { + "name" : "given-name", + "resourceType" : "*", + "resourcePath" : "name.given", + "metric" : "STRING", + "matchThreshold" : 0.8 + }], + "matchResultMap" : { + "given-name" : "POSSIBLE_MATCH" + } +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRetriever.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRetriever.java new file mode 100644 index 00000000000..75feb802818 --- /dev/null +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRetriever.java @@ -0,0 +1,10 @@ +package ca.uhn.fhir.rest.server.util; + +import ca.uhn.fhir.context.RuntimeSearchParam; + +public interface ISearchParamRetriever { + /** + * @return Returns {@literal null} if no match + */ + RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName); +} From 34c68e15afbac78fd2acad732bd7a5af58c2022b Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Sun, 14 Jun 2020 19:10:36 -0400 Subject: [PATCH 4/5] Add license headers --- .../empi/rules/metric/IEmpiFieldMetric.java | 20 +++++++++++++++++++ .../metric/matcher/BaseHapiStringMetric.java | 20 +++++++++++++++++++ .../matcher/DoubleMetaphoneStringMatcher.java | 20 +++++++++++++++++++ .../rules/metric/matcher/HapiDateMatcher.java | 20 +++++++++++++++++++ .../metric/matcher/HapiDateMatcherDstu3.java | 20 +++++++++++++++++++ .../metric/matcher/HapiDateMatcherR4.java | 20 +++++++++++++++++++ .../metric/matcher/IEmpiStringMatcher.java | 20 +++++++++++++++++++ .../matcher/MetaphoneStringMatcher.java | 20 +++++++++++++++++++ .../metric/matcher/StringEncoderMatcher.java | 20 +++++++++++++++++++ .../matcher/SubstringStringMatcher.java | 20 +++++++++++++++++++ .../server/util/ISearchParamRetriever.java | 20 +++++++++++++++++++ 11 files changed, 220 insertions(+) diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/IEmpiFieldMetric.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/IEmpiFieldMetric.java index a13be076c28..61ede86d7e7 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/IEmpiFieldMetric.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/IEmpiFieldMetric.java @@ -1,4 +1,24 @@ package ca.uhn.fhir.empi.rules.metric; +/*- + * #%L + * HAPI FHIR - Enterprise Master Patient Index + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + public interface IEmpiFieldMetric { } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/BaseHapiStringMetric.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/BaseHapiStringMetric.java index 55b7e0a6c51..e44640f1d68 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/BaseHapiStringMetric.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/BaseHapiStringMetric.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.empi.rules.metric.matcher; +/*- + * #%L + * HAPI FHIR - Enterprise Master Patient Index + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import ca.uhn.fhir.util.StringUtil; import org.hl7.fhir.instance.model.api.IPrimitiveType; diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/DoubleMetaphoneStringMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/DoubleMetaphoneStringMatcher.java index 78611b06365..7bc6b2127a6 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/DoubleMetaphoneStringMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/DoubleMetaphoneStringMatcher.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.empi.rules.metric.matcher; +/*- + * #%L + * HAPI FHIR - Enterprise Master Patient Index + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import org.apache.commons.codec.language.DoubleMetaphone; public class DoubleMetaphoneStringMatcher implements IEmpiStringMatcher { diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcher.java index 8a46c98a423..d5e9eb784f9 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcher.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.empi.rules.metric.matcher; +/*- + * #%L + * HAPI FHIR - Enterprise Master Patient Index + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import ca.uhn.fhir.context.FhirContext; import org.hl7.fhir.instance.model.api.IBase; diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherDstu3.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherDstu3.java index c0e0930a2fe..8f2524c4cc1 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherDstu3.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherDstu3.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.empi.rules.metric.matcher; +/*- + * #%L + * HAPI FHIR - Enterprise Master Patient Index + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import org.hl7.fhir.dstu3.model.BaseDateTimeType; import org.hl7.fhir.dstu3.model.DateTimeType; import org.hl7.fhir.dstu3.model.DateType; diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherR4.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherR4.java index 6a7c2f27645..c1b42b97c71 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherR4.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/HapiDateMatcherR4.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.empi.rules.metric.matcher; +/*- + * #%L + * HAPI FHIR - Enterprise Master Patient Index + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.r4.model.BaseDateTimeType; import org.hl7.fhir.r4.model.DateTimeType; diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/IEmpiStringMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/IEmpiStringMatcher.java index d99fcc1a897..4922eee4c3a 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/IEmpiStringMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/IEmpiStringMatcher.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.empi.rules.metric.matcher; +/*- + * #%L + * HAPI FHIR - Enterprise Master Patient Index + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + public interface IEmpiStringMatcher { boolean matches(String theLeftString, String theRightString); } diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/MetaphoneStringMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/MetaphoneStringMatcher.java index 7694444322b..0ba8ab3f902 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/MetaphoneStringMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/MetaphoneStringMatcher.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.empi.rules.metric.matcher; +/*- + * #%L + * HAPI FHIR - Enterprise Master Patient Index + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import org.apache.commons.codec.language.Metaphone; public class MetaphoneStringMatcher implements IEmpiStringMatcher { diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/StringEncoderMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/StringEncoderMatcher.java index 1685e97eabe..727550f0221 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/StringEncoderMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/StringEncoderMatcher.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.empi.rules.metric.matcher; +/*- + * #%L + * HAPI FHIR - Enterprise Master Patient Index + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import org.apache.commons.codec.EncoderException; import org.apache.commons.codec.StringEncoder; import org.slf4j.Logger; diff --git a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/SubstringStringMatcher.java b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/SubstringStringMatcher.java index 0dc94d91c4d..77aec7c837c 100644 --- a/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/SubstringStringMatcher.java +++ b/hapi-fhir-server-empi/src/main/java/ca/uhn/fhir/empi/rules/metric/matcher/SubstringStringMatcher.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.empi.rules.metric.matcher; +/*- + * #%L + * HAPI FHIR - Enterprise Master Patient Index + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + public class SubstringStringMatcher implements IEmpiStringMatcher { @Override public boolean matches(String theLeftString, String theRightString) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRetriever.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRetriever.java index 75feb802818..31846aa389d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRetriever.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/ISearchParamRetriever.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.rest.server.util; +/*- + * #%L + * HAPI FHIR - Server Framework + * %% + * Copyright (C) 2014 - 2020 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + import ca.uhn.fhir.context.RuntimeSearchParam; public interface ISearchParamRetriever { From 0d4e12fe58f45f6b111d3a867dbf91a3d202a7a3 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Mon, 15 Jun 2020 09:17:52 -0400 Subject: [PATCH 5/5] Undo accidentally committed file with a weird name --- "\\" | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 "\\" diff --git "a/\\" "b/\\" deleted file mode 100644 index 164a87f7a0f..00000000000 --- "a/\\" +++ /dev/null @@ -1,6 +0,0 @@ ---- -type: fix -issue: 1856 -title: The subscription delivery queue in the JPA server was erroniously keeping both a copy of the serialized and the - deserialized payload in memory for each entry in the queue, doubling the memory requirements. This also caused failures - when delivering XML payloads in some configurations. This has been corrected.