refactoring nickname matching (#4918)

* refactoring 1

* refactoring step 2

* refactoring matches

* cleanup

* fixing some tests

* fixing more tests

* remove dbugcode

* adding the nickname factory

* code review fixes

* review fixes

* put default to prevent breaking

* refactor review fixes

* more review points

* more review points

* review points

* review points

* more review changes

* review points

* review points

* adding bean

---------

Co-authored-by: leif stawnyczy <leifstawnyczy@leifs-MacBook-Pro.local>
Co-authored-by: leif stawnyczy <leifstawnyczy@leifs-mbp.home>
This commit is contained in:
TipzCM 2023-06-02 14:04:30 -04:00 committed by GitHub
parent 9838f8a19a
commit 7a18f17a01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 985 additions and 657 deletions

View File

@ -0,0 +1,7 @@
---
type: fix
issue: 4917
title: "Matching algorithms have been refactored to allow
greater flexibility in setting and defining nicknames
as well as allowing bean injection into matcher classes.
"

View File

@ -17,7 +17,7 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.searchparam.nickname;
package ca.uhn.fhir.jpa.nickname;
import javax.annotation.Nonnull;
import java.io.BufferedReader;
@ -33,19 +33,30 @@ class NicknameMap {
private final Map<String, List<String>> myFormalToNick = new HashMap<>();
private final Map<String, List<String>> myNicknameToFormal = new HashMap<>();
private final List<String> myBadRows = new ArrayList<>();
void load(Reader theReader) throws IOException {
try (BufferedReader reader = new BufferedReader(theReader)) {
String line;
while ((line = reader.readLine()) != null) {
String[] parts = line.split(",");
if (parts.length > 1) {
String key = parts[0];
List<String> values = new ArrayList<>(Arrays.asList(parts).subList(1, parts.length));
add(key, values);
} else {
myBadRows.add(line);
}
}
}
}
private void add(String theKey, List<String> theValues) {
void clear() {
myFormalToNick.clear();
myNicknameToFormal.clear();
}
void add(String theKey, List<String> theValues) {
myFormalToNick.put(theKey, theValues);
for (String value : theValues) {
myNicknameToFormal.putIfAbsent(value, new ArrayList<>());
@ -57,14 +68,22 @@ class NicknameMap {
return myFormalToNick.size();
}
boolean isEmpty() {
return size() == 0;
}
List<String> getBadRows() {
return myBadRows;
}
@Nonnull
List<String> getNicknamesFromFormalName(String theName) {
public List<String> getNicknamesFromFormalName(String theName) {
List<String> result = myFormalToNick.get(theName);
return result == null ? new ArrayList<>() : result;
}
@Nonnull
List<String> getFormalNamesFromNickname(String theNickname) {
public List<String> getFormalNamesFromNickname(String theNickname) {
List<String> result = myNicknameToFormal.get(theNickname);
return result == null ? new ArrayList<>() : result;
}

View File

@ -17,8 +17,12 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.jpa.searchparam.nickname;
package ca.uhn.fhir.jpa.nickname;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.i18n.Msg;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
@ -32,22 +36,40 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Nickname service is used to load nicknames
* via a file that contains rows of comma separated names that are
* "similar" or nicknames of each other.
* -
* If no nickname resource is provided, nicknames/names.csv will be used instead.
* -
* If one is to be provided, it must be provided before nickname svc is invoked
*/
public class NicknameSvc {
private final NicknameMap myNicknameMap = new NicknameMap();
private static final Logger ourLog = LoggerFactory.getLogger(NicknameSvc.class);
private NicknameMap myNicknameMap;
private Resource myNicknameResource;
public NicknameSvc() {
public NicknameSvc() throws IOException {
Resource nicknameCsvResource = new ClassPathResource("/nickname/names.csv");
try (InputStream inputStream = nicknameCsvResource.getInputStream()) {
try (Reader reader = new InputStreamReader(inputStream)) {
myNicknameMap.load(reader);
}
}
public void setNicknameResource(Resource theNicknameResource) {
myNicknameResource = theNicknameResource;
}
public int size() {
ensureMapInitialized();
return myNicknameMap.size();
}
public List<String> getBadRows() {
ensureMapInitialized();
return myNicknameMap.getBadRows();
}
public Collection<String> getEquivalentNames(String theName) {
Set<String> retval = new HashSet<>(getNicknamesFromFormalName(theName));
@ -64,11 +86,34 @@ public class NicknameSvc {
@Nonnull
List<String> getNicknamesFromFormalName(String theName) {
ensureMapInitialized();
return myNicknameMap.getNicknamesFromFormalName(theName);
}
@Nonnull
List<String> getFormalNamesFromNickname(String theNickname) {
ensureMapInitialized();
return myNicknameMap.getFormalNamesFromNickname(theNickname);
}
private void ensureMapInitialized() {
if (myNicknameResource == null) {
myNicknameResource = new ClassPathResource("/nickname/names.csv");
}
if (myNicknameMap == null) {
myNicknameMap = new NicknameMap();
}
if (myNicknameMap.isEmpty()) {
try {
try (InputStream inputStream = myNicknameResource.getInputStream()) {
try (Reader reader = new InputStreamReader(inputStream)) {
myNicknameMap.load(reader);
}
}
} catch (IOException e) {
throw new ConfigurationException(Msg.code(2234) + "Unable to load nicknames", e);
}
}
}
}

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.jpa.searchparam.nickname;
package ca.uhn.fhir.jpa.nickname;
import org.junit.jupiter.api.Test;

View File

@ -1,4 +1,4 @@
package ca.uhn.fhir.jpa.searchparam.nickname;
package ca.uhn.fhir.jpa.nickname;
import org.junit.jupiter.api.Test;

View File

@ -95,8 +95,8 @@ import ca.uhn.fhir.jpa.partition.PartitionLookupSvcImpl;
import ca.uhn.fhir.jpa.partition.PartitionManagementProvider;
import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.provider.DiffProvider;
import ca.uhn.fhir.jpa.provider.ProcessMessageProvider;
import ca.uhn.fhir.jpa.provider.InstanceReindexProvider;
import ca.uhn.fhir.jpa.provider.ProcessMessageProvider;
import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
import ca.uhn.fhir.jpa.provider.ValueSetOperationProvider;
@ -149,7 +149,6 @@ import ca.uhn.fhir.jpa.search.warm.ICacheWarmingSvc;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.config.SearchParamConfig;
import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver;
import ca.uhn.fhir.jpa.searchparam.nickname.NicknameInterceptor;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
import ca.uhn.fhir.jpa.sp.SearchParamPresenceSvcImpl;
@ -198,7 +197,6 @@ import org.springframework.scheduling.concurrent.ScheduledExecutorFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.Date;
@Configuration
@ -772,12 +770,6 @@ public class JpaConfig {
return new UnknownCodeSystemWarningValidationSupport(theFhirContext);
}
@Lazy
@Bean
public NicknameInterceptor nicknameInterceptor() throws IOException {
return new NicknameInterceptor();
}
@Bean
public ISynchronousSearchSvc synchronousSearchSvc() {
return new SynchronousSearchSvcImpl();

View File

@ -20,13 +20,11 @@
package ca.uhn.fhir.jpa.dao.expunge;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.rules.config.MdmSettings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@Service

View File

@ -20,14 +20,21 @@
package ca.uhn.fhir.jpa.mdm.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.nickname.NicknameSvc;
import ca.uhn.fhir.jpa.searchparam.config.NicknameServiceConfig;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.interceptor.MdmSearchExpandingInterceptor;
import ca.uhn.fhir.mdm.rules.config.MdmRuleValidator;
import ca.uhn.fhir.mdm.rules.matcher.IMatcherFactory;
import ca.uhn.fhir.mdm.rules.matcher.MdmMatcherFactory;
import ca.uhn.fhir.mdm.svc.MdmLinkDeleteSvc;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Lazy;
@Import(NicknameServiceConfig.class)
@Configuration
public class MdmCommonConfig {
@Bean
@ -44,4 +51,17 @@ public class MdmCommonConfig {
MdmLinkDeleteSvc mdmLinkDeleteSvc() {
return new MdmLinkDeleteSvc();
}
@Bean
public IMatcherFactory matcherFactory(
FhirContext theFhirContext,
IMdmSettings theSettings,
NicknameSvc theNicknameSvc
) {
return new MdmMatcherFactory(
theFhirContext,
theSettings,
theNicknameSvc
);
}
}

View File

@ -48,6 +48,7 @@ import ca.uhn.fhir.jpa.mdm.svc.candidate.FindCandidateByLinkSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmCandidateSearchCriteriaBuilderSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmCandidateSearchSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc;
import ca.uhn.fhir.mdm.rules.matcher.IMatcherFactory;
import ca.uhn.fhir.mdm.util.MdmPartitionHelper;
import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.subscription.channel.api.IChannelFactory;
@ -205,8 +206,12 @@ public class MdmConsumerConfig {
}
@Bean
MdmResourceMatcherSvc mdmResourceComparatorSvc(FhirContext theFhirContext, IMdmSettings theMdmSettings) {
return new MdmResourceMatcherSvc(theFhirContext, theMdmSettings);
MdmResourceMatcherSvc mdmResourceComparatorSvc(
FhirContext theFhirContext,
IMatcherFactory theIMatcherFactory,
IMdmSettings theMdmSettings
) {
return new MdmResourceMatcherSvc(theFhirContext, theIMatcherFactory, theMdmSettings);
}
@Bean

View File

@ -76,7 +76,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.slf4j.LoggerFactory.getLogger;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {MdmSubmitterConfig.class, MdmConsumerConfig.class, TestMdmConfigR4.class, SubscriptionProcessorConfig.class})
@ContextConfiguration(classes = {
MdmSubmitterConfig.class,
MdmConsumerConfig.class,
TestMdmConfigR4.class,
SubscriptionProcessorConfig.class
})
abstract public class BaseMdmR4Test extends BaseJpaR4Test {
protected static final String PARTITION_1 = "PART-1";

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.mdm.api.IMdmSubmitSvc;
import ca.uhn.fhir.mdm.provider.MdmControllerHelper;
import ca.uhn.fhir.mdm.provider.MdmProviderDstu3Plus;
import ca.uhn.fhir.mdm.rules.config.MdmSettings;
import ca.uhn.fhir.mdm.rules.svc.MdmResourceMatcherSvc;
import ca.uhn.fhir.mdm.util.MessageHelper;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
@ -34,6 +35,8 @@ public abstract class BaseProviderR4Test extends BaseMdmR4Test {
@Autowired
protected MdmSettings myMdmSettings;
@Autowired
protected MdmResourceMatcherSvc myMdmResourceMatcherSvc;
@Autowired
private MdmControllerHelper myMdmHelper;
@Autowired
Batch2JobHelper myBatch2JobHelper;
@ -48,7 +51,7 @@ public abstract class BaseProviderR4Test extends BaseMdmR4Test {
String json = IOUtils.toString(resource.getInputStream(), Charsets.UTF_8);
myMdmSettings.setEnabled(true);
myMdmSettings.setScriptText(json);
myMdmResourceMatcherSvc.setMdmSettings(myMdmSettings);
myMdmResourceMatcherSvc.setMdmRulesJson(myMdmSettings.getMdmRules());
}
@BeforeEach
@ -62,7 +65,7 @@ public abstract class BaseProviderR4Test extends BaseMdmR4Test {
public void after() throws IOException {
super.after();
myMdmSettings.setScriptText(defaultScript);
myMdmResourceMatcherSvc.setMdmSettings(myMdmSettings);
myMdmResourceMatcherSvc.setMdmRulesJson(myMdmSettings.getMdmRules());
}
protected void clearMdmLinks() {

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmCandidateSearchSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.TooManyCandidatesException;
import ca.uhn.fhir.jpa.nickname.NicknameSvc;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.nickname.NicknameInterceptor;
@ -18,7 +19,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
@ -37,11 +37,15 @@ public class MdmCandidateSearchSvcIT extends BaseMdmR4Test {
@Autowired
MatchUrlService myMatchUrlService;
@Autowired
NicknameSvc myNicknameSvc;
private NicknameInterceptor myNicknameInterceptor;
@BeforeEach
public void before() throws IOException {
myNicknameInterceptor = new NicknameInterceptor();
public void before() throws Exception {
super.before();
myNicknameInterceptor = new NicknameInterceptor(myNicknameSvc);
myInterceptorRegistry.registerInterceptor(myNicknameInterceptor);
}

View File

@ -165,8 +165,6 @@
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>

View File

@ -0,0 +1,22 @@
package ca.uhn.fhir.jpa.searchparam.config;
import ca.uhn.fhir.jpa.nickname.NicknameSvc;
import ca.uhn.fhir.jpa.searchparam.nickname.NicknameInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@Configuration
public class NicknameServiceConfig {
@Lazy
@Bean
public NicknameInterceptor nicknameInterceptor(NicknameSvc theNicknameSvc) {
return new NicknameInterceptor(theNicknameSvc);
}
@Bean
public NicknameSvc nicknameSvc() {
return new NicknameSvc();
}
}

View File

@ -47,9 +47,13 @@ import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
@Import({
NicknameServiceConfig.class
})
@Configuration
public class SearchParamConfig {

View File

@ -21,13 +21,13 @@ package ca.uhn.fhir.jpa.searchparam.nickname;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.nickname.NicknameSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.param.StringParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@ -39,8 +39,8 @@ public class NicknameInterceptor {
private final NicknameSvc myNicknameSvc;
public NicknameInterceptor() throws IOException {
myNicknameSvc = new NicknameSvc();
public NicknameInterceptor(NicknameSvc theNicknameSvc) {
myNicknameSvc = theNicknameSvc;
}
@Hook(Pointcut.STORAGE_PRESEARCH_REGISTERED)

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jpa.searchparam.nickname;
import ca.uhn.fhir.jpa.nickname.NicknameSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.StringParam;
import org.junit.jupiter.api.Test;
@ -9,13 +10,18 @@ import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertEquals;
class NicknameInterceptorTest {
private NicknameInterceptor createNicknameInterceptor() {
return new NicknameInterceptor(new NicknameSvc());
}
@Test
public void testExpandForward() throws IOException {
// setup
String formalName = "kenneth";
SearchParameterMap sp = new SearchParameterMap();
sp.add("name", new StringParam(formalName).setNicknameExpand(true));
NicknameInterceptor svc = new NicknameInterceptor();
NicknameInterceptor svc = createNicknameInterceptor();
// execute
svc.expandNicknames(sp);
@ -31,7 +37,7 @@ class NicknameInterceptorTest {
String nickname = "ken";
SearchParameterMap sp = new SearchParameterMap();
sp.add("name", new StringParam(nickname).setNicknameExpand(true));
NicknameInterceptor svc = new NicknameInterceptor();
NicknameInterceptor svc = createNicknameInterceptor();
// execute
svc.expandNicknames(sp);
@ -47,7 +53,7 @@ class NicknameInterceptorTest {
String unusualName = "X Æ A-12";
SearchParameterMap sp = new SearchParameterMap();
sp.add("name", new StringParam(unusualName).setNicknameExpand(true));
NicknameInterceptor svc = new NicknameInterceptor();
NicknameInterceptor svc = createNicknameInterceptor();
// execute
svc.expandNicknames(sp);

View File

@ -19,7 +19,6 @@
*/
package ca.uhn.fhir.mdm.api;
import ca.uhn.fhir.mdm.dao.IMdmLinkImplFactory;
import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
import java.util.stream.Collectors;

View File

@ -139,7 +139,7 @@ public class MdmControllerHelper {
public IBaseBundle getMatchesAndPossibleMatchesForResource(IAnyResource theResource, String theResourceType, RequestDetails theRequestDetails) {
RequestPartitionId requestPartitionId;
ReadPartitionIdRequestDetails details = ReadPartitionIdRequestDetails.forSearchType(theResourceType, null, null);
if (myMdmSettings.getSearchAllPartitionForMatch()){
if (myMdmSettings.getSearchAllPartitionForMatch()) {
requestPartitionId = RequestPartitionId.allPartitions();
} else {
requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, details);

View File

@ -19,21 +19,16 @@
*/
package ca.uhn.fhir.mdm.rules.json;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.api.MdmMatchEvaluation;
import ca.uhn.fhir.mdm.rules.matcher.MdmMatcherEnum;
import ca.uhn.fhir.mdm.rules.matcher.models.MatchTypeEnum;
import ca.uhn.fhir.model.api.IModelJson;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hl7.fhir.instance.model.api.IBase;
import javax.annotation.Nonnull;
/**
* Contains all business data for determining if a match exists on a particular field, given:
* <p></p>
* 1. A {@link MdmMatcherEnum} which determines the actual similarity values.
* 1. A {@link MatchTypeEnum} 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)
*/
@ -87,10 +82,6 @@ public class MdmFieldMatchJson implements IModelJson {
return myMatcher;
}
public boolean isMatcherSupportingEmptyFields() {
return (getMatcher() == null) ? false : getMatcher().isMatchingEmptyFields();
}
public MdmFieldMatchJson setMatcher(MdmMatcherJson theMatcher) {
myMatcher = theMatcher;
return this;
@ -105,17 +96,6 @@ public class MdmFieldMatchJson implements IModelJson {
return this;
}
public MdmMatchEvaluation match(FhirContext theFhirContext, IBase theLeftValue, IBase theRightValue) {
if (myMatcher != null) {
boolean result = myMatcher.match(theFhirContext, theLeftValue, theRightValue);
return new MdmMatchEvaluation(result, result ? 1.0 : 0.0);
}
if (mySimilarity != null) {
return mySimilarity.match(theFhirContext, theLeftValue, theRightValue);
}
throw new InternalErrorException(Msg.code(1522) + "Field Match " + myName + " has neither a matcher nor a similarity.");
}
public String getFhirPath() {
return myFhirPath;
}

View File

@ -19,15 +19,13 @@
*/
package ca.uhn.fhir.mdm.rules.json;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.rules.matcher.MdmMatcherEnum;
import ca.uhn.fhir.mdm.rules.matcher.models.MatchTypeEnum;
import ca.uhn.fhir.model.api.IModelJson;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hl7.fhir.instance.model.api.IBase;
public class MdmMatcherJson implements IModelJson {
@JsonProperty(value = "algorithm", required = true)
MdmMatcherEnum myAlgorithm;
MatchTypeEnum myAlgorithm;
@JsonProperty(value = "identifierSystem", required = false)
String myIdentifierSystem;
@ -38,11 +36,11 @@ public class MdmMatcherJson implements IModelJson {
@JsonProperty(value = "exact")
boolean myExact;
public MdmMatcherEnum getAlgorithm() {
public MatchTypeEnum getAlgorithm() {
return myAlgorithm;
}
public MdmMatcherJson setAlgorithm(MdmMatcherEnum theAlgorithm) {
public MdmMatcherJson setAlgorithm(MatchTypeEnum theAlgorithm) {
myAlgorithm = theAlgorithm;
return this;
}
@ -64,12 +62,4 @@ public class MdmMatcherJson implements IModelJson {
myExact = theExact;
return this;
}
public boolean isMatchingEmptyFields() {
return myAlgorithm.isMatchingEmptyFields();
}
public boolean match(FhirContext theFhirContext, IBase theLeftValue, IBase theRightValue) {
return myAlgorithm.match(theFhirContext, theLeftValue, theRightValue, myExact, myIdentifierSystem);
}
}

View File

@ -1,41 +0,0 @@
/*-
* #%L
* HAPI FHIR - Master Data Management
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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%
*/
package ca.uhn.fhir.mdm.rules.matcher;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.instance.model.api.IBase;
public class HapiDateMatcher implements IMdmFieldMatcher {
private final HapiDateMatcherDstu3 myHapiDateMatcherDstu3 = new HapiDateMatcherDstu3();
private final HapiDateMatcherR4 myHapiDateMatcherR4 = new HapiDateMatcherR4();
@Override
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem) {
switch (theFhirContext.getVersion().getVersion()) {
case DSTU3:
return myHapiDateMatcherDstu3.match(theLeftBase, theRightBase);
case R4:
return myHapiDateMatcherR4.match(theLeftBase, theRightBase);
default:
throw new UnsupportedOperationException(Msg.code(1520) + "Version not supported: " + theFhirContext.getVersion().getVersion());
}
}
}

View File

@ -1,59 +0,0 @@
/*-
* #%L
* HAPI FHIR - Master Data Management
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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%
*/
package ca.uhn.fhir.mdm.rules.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;
}
}

View File

@ -1,61 +0,0 @@
/*-
* #%L
* HAPI FHIR - Master Data Management
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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%
*/
package ca.uhn.fhir.mdm.rules.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;
//Left date is coarser
if (comparison < 0) {
leftPDate = leftDate;
if (rightDate instanceof DateType) {
rightPDate = new DateType(rightDate.getValue(), leftDate.getPrecision());
} else {
rightPDate = new DateTimeType(rightDate.getValue(), leftDate.getPrecision());
}
//Right date is coarser
} else {
rightPDate = rightDate;
if (leftDate instanceof DateType) {
leftPDate = new DateType(leftDate.getValue(), rightDate.getPrecision());
} else {
leftPDate = new DateTimeType(leftDate.getValue(), rightDate.getPrecision());
}
}
return leftPDate.getValueAsString().equals(rightPDate.getValueAsString());
}
return false;
}
}

View File

@ -0,0 +1,12 @@
package ca.uhn.fhir.mdm.rules.matcher;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import ca.uhn.fhir.mdm.rules.matcher.models.MatchTypeEnum;
public interface IMatcherFactory {
/**
* Retrieves the field matcher for the given MatchTypeEnum
*/
IMdmFieldMatcher getFieldMatcherForMatchType(MatchTypeEnum theMdmMatcherEnum);
}

View File

@ -1,24 +0,0 @@
/*-
* #%L
* HAPI FHIR - Master Data Management
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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%
*/
package ca.uhn.fhir.mdm.rules.matcher;
public interface IMdmStringMatcher {
boolean matches(String theLeftString, String theRightString);
}

View File

@ -1,86 +0,0 @@
/*-
* #%L
* HAPI FHIR - Master Data Management
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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%
*/
package ca.uhn.fhir.mdm.rules.matcher;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.phonetic.PhoneticEncoderEnum;
import org.hl7.fhir.instance.model.api.IBase;
/**
* Enum for holding all the known FHIR Element matchers that we support in HAPI. The string matchers first
* encode the string using an Apache Encoder before comparing them.
* https://commons.apache.org/proper/commons-codec/userguide.html
*/
public enum MdmMatcherEnum {
CAVERPHONE1(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.CAVERPHONE1))),
CAVERPHONE2(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.CAVERPHONE2))),
COLOGNE(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.COLOGNE))),
DOUBLE_METAPHONE(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.DOUBLE_METAPHONE))),
MATCH_RATING_APPROACH(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.MATCH_RATING_APPROACH))),
METAPHONE(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.METAPHONE))),
NYSIIS(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.NYSIIS))),
REFINED_SOUNDEX(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.REFINED_SOUNDEX))),
SOUNDEX(new HapiStringMatcher(new PhoneticEncoderMatcher(PhoneticEncoderEnum.SOUNDEX))),
NICKNAME(new HapiStringMatcher(new NicknameMatcher())),
STRING(new HapiStringMatcher()),
SUBSTRING(new HapiStringMatcher(new SubstringStringMatcher())),
DATE(new HapiDateMatcher()),
NAME_ANY_ORDER(new NameMatcher(MdmNameMatchModeEnum.ANY_ORDER)),
NAME_FIRST_AND_LAST(new NameMatcher(MdmNameMatchModeEnum.FIRST_AND_LAST)),
IDENTIFIER(new IdentifierMatcher()),
EMPTY_FIELD(new EmptyFieldMatcher()),
EXTENSION_ANY_ORDER(new ExtensionMatcher()),
NUMERIC(new HapiStringMatcher(new NumericMatcher()));
private final IMdmFieldMatcher myMdmFieldMatcher;
MdmMatcherEnum(IMdmFieldMatcher theMdmFieldMatcher) {
myMdmFieldMatcher = theMdmFieldMatcher;
}
/**
* Determines whether two FHIR elements match according using the provided {@link IMdmFieldMatcher}
*
* @param theFhirContext
* @param theLeftBase left FHIR element to compare
* @param theRightBase right FHIR element to compare
* @param theExact used by String matchers. If "false" then the string is normalized (case, accents) before comparing. If "true" then an exact string comparison is performed.
* @param theIdentifierSystem used optionally by the IDENTIFIER matcher, when present, only matches the identifiers if they belong to this system.
* @return
*/
public boolean match(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem) {
return myMdmFieldMatcher.matches(theFhirContext, theLeftBase, theRightBase, theExact, theIdentifierSystem);
}
/**
* Checks if this matcher supports checks on empty fields
*
* @return
* Returns true of this matcher supports empty fields and false otherwise
*/
public boolean isMatchingEmptyFields() {
return this == EMPTY_FIELD;
}
}

View File

@ -0,0 +1,96 @@
package ca.uhn.fhir.mdm.rules.matcher;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.phonetic.PhoneticEncoderEnum;
import ca.uhn.fhir.jpa.nickname.NicknameSvc;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.EmptyFieldMatcher;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.ExtensionMatcher;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.HapiDateMatcher;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.HapiStringMatcher;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.IdentifierMatcher;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.MdmNameMatchModeEnum;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.NameMatcher;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.NicknameMatcher;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.NumericMatcher;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.PhoneticEncoderMatcher;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.SubstringStringMatcher;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import ca.uhn.fhir.mdm.rules.matcher.models.MatchTypeEnum;
import org.slf4j.Logger;
public class MdmMatcherFactory implements IMatcherFactory {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
private final FhirContext myFhirContext;
private final IMdmSettings myMdmSettings;
private final NicknameSvc myNicknameSvc;
public MdmMatcherFactory(
FhirContext theFhirContext,
IMdmSettings theSettings,
NicknameSvc theNicknameSvc
) {
myFhirContext = theFhirContext;
myMdmSettings = theSettings;
myNicknameSvc = theNicknameSvc;
}
@Override
public IMdmFieldMatcher getFieldMatcherForMatchType(MatchTypeEnum theMdmMatcherEnum) {
String matchTypeName;
if (theMdmMatcherEnum != null) {
switch (theMdmMatcherEnum) {
case CAVERPHONE1:
return new PhoneticEncoderMatcher(PhoneticEncoderEnum.CAVERPHONE1);
case CAVERPHONE2:
return new PhoneticEncoderMatcher(PhoneticEncoderEnum.CAVERPHONE2);
case COLOGNE:
return new PhoneticEncoderMatcher(PhoneticEncoderEnum.COLOGNE);
case DOUBLE_METAPHONE:
return new PhoneticEncoderMatcher(PhoneticEncoderEnum.DOUBLE_METAPHONE);
case MATCH_RATING_APPROACH:
return new PhoneticEncoderMatcher(PhoneticEncoderEnum.MATCH_RATING_APPROACH);
case METAPHONE:
return new PhoneticEncoderMatcher(PhoneticEncoderEnum.METAPHONE);
case NYSIIS:
return new PhoneticEncoderMatcher(PhoneticEncoderEnum.NYSIIS);
case REFINED_SOUNDEX:
return new PhoneticEncoderMatcher(PhoneticEncoderEnum.REFINED_SOUNDEX);
case SOUNDEX:
return new PhoneticEncoderMatcher(PhoneticEncoderEnum.SOUNDEX);
case NICKNAME:
return new NicknameMatcher(myNicknameSvc);
case STRING:
return new HapiStringMatcher();
case SUBSTRING:
return new SubstringStringMatcher();
case DATE:
return new HapiDateMatcher(myFhirContext);
case NAME_ANY_ORDER:
return new NameMatcher(myFhirContext, MdmNameMatchModeEnum.ANY_ORDER);
case NAME_FIRST_AND_LAST:
return new NameMatcher(myFhirContext, MdmNameMatchModeEnum.FIRST_AND_LAST);
case IDENTIFIER:
return new IdentifierMatcher();
case EXTENSION_ANY_ORDER:
return new ExtensionMatcher();
case NUMERIC:
return new NumericMatcher();
case EMPTY_FIELD:
return new EmptyFieldMatcher();
default:
break;
}
matchTypeName = theMdmMatcherEnum.name();
} else {
matchTypeName = "null";
}
// This is odd, but it's a valid code path
ourLog.warn("Unrecognized field type {}. Returning null", matchTypeName);
return null;
}
}

View File

@ -1,27 +0,0 @@
/*-
* #%L
* HAPI FHIR - Master Data Management
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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%
*/
package ca.uhn.fhir.mdm.rules.matcher;
public class SubstringStringMatcher implements IMdmStringMatcher {
@Override
public boolean matches(String theLeftString, String theRightString) {
return theLeftString.startsWith(theRightString) || theRightString.startsWith(theLeftString);
}
}

View File

@ -0,0 +1,56 @@
package ca.uhn.fhir.mdm.rules.matcher.fieldmatchers;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.DateType;
/**
* A wrapper class for datetimes of ambiguous fhir version
*/
public class DateTimeWrapper {
/**
* The precision of this datetime object
*/
private final TemporalPrecisionEnum myPrecision;
/**
* The string value with current precision
*/
private final String myValueAsString;
public DateTimeWrapper(FhirContext theFhirContext, IBase theDate) {
if (theDate instanceof org.hl7.fhir.dstu3.model.BaseDateTimeType) {
org.hl7.fhir.dstu3.model.BaseDateTimeType dstu3Date = (org.hl7.fhir.dstu3.model.BaseDateTimeType) theDate;
myPrecision = dstu3Date.getPrecision();
myValueAsString = dstu3Date.getValueAsString();
} else if (theDate instanceof org.hl7.fhir.r4.model.BaseDateTimeType) {
org.hl7.fhir.r4.model.BaseDateTimeType r4Date = (org.hl7.fhir.r4.model.BaseDateTimeType) theDate;
myPrecision = r4Date.getPrecision();
myValueAsString = r4Date.getValueAsString();
} else {
// we should consider changing this error so we don't need the fhir context at all
throw new UnsupportedOperationException(Msg.code(1520) + "Version not supported: " + theFhirContext.getVersion().getVersion());
}
}
public TemporalPrecisionEnum getPrecision() {
return myPrecision;
}
public String getValueAsStringWithPrecision(TemporalPrecisionEnum thePrecision) {
// we are using an R4 DateTypes because all datetime strings are the same for all fhir versions
// (and so it won't matter here)
// we just want the string at a specific precision
DateTimeType dateTimeType = new DateTimeType(myValueAsString);
dateTimeType.setPrecision(thePrecision);
return dateTimeType.getValueAsString();
}
public String getValueAsString() {
return myValueAsString;
}
}

View File

@ -17,9 +17,10 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.mdm.rules.matcher;
package ca.uhn.fhir.mdm.rules.matcher.fieldmatchers;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import org.hl7.fhir.instance.model.api.IBase;
public class EmptyFieldMatcher implements IMdmFieldMatcher {
@ -28,7 +29,7 @@ public class EmptyFieldMatcher implements IMdmFieldMatcher {
}
@Override
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem) {
public boolean matches(IBase theLeftBase, IBase theRightBase, MdmMatcherJson theParams) {
for (IBase b : new IBase[] {theLeftBase, theRightBase}) {
if (b != null && !b.isEmpty()) {
return false;
@ -36,4 +37,9 @@ public class EmptyFieldMatcher implements IMdmFieldMatcher {
}
return true;
}
@Override
public boolean isMatchingEmptyFields() {
return true;
}
}

View File

@ -17,9 +17,10 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.mdm.rules.matcher;
package ca.uhn.fhir.mdm.rules.matcher.fieldmatchers;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import ca.uhn.fhir.util.ExtensionUtil;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseExtension;
@ -30,7 +31,7 @@ import java.util.List;
public class ExtensionMatcher implements IMdmFieldMatcher {
@Override
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem) {
public boolean matches(IBase theLeftBase, IBase theRightBase, MdmMatcherJson theParams) {
if (!(theLeftBase instanceof IBaseHasExtensions && theRightBase instanceof IBaseHasExtensions)) {
return false;
}

View File

@ -0,0 +1,65 @@
/*-
* #%L
* HAPI FHIR - Master Data Management
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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%
*/
package ca.uhn.fhir.mdm.rules.matcher.fieldmatchers;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import org.hl7.fhir.instance.model.api.IBase;
public class HapiDateMatcher implements IMdmFieldMatcher {
private final FhirContext myFhirContext;
public HapiDateMatcher(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
@Override
public boolean matches(IBase theLeftBase, IBase theRightBase, MdmMatcherJson theParams) {
DateTimeWrapper left = new DateTimeWrapper(myFhirContext, theLeftBase);
DateTimeWrapper right = new DateTimeWrapper(myFhirContext, theRightBase);
/*
* we use the precision to determine how we should equate.
* We should use the same precision (the precision of the less
* precise date)
*/
int comparison = left.getPrecision().compareTo(right.getPrecision());
String leftString;
String rightString;
if (comparison == 0) {
// same precision
leftString = left.getValueAsString();
rightString = right.getValueAsString();
} else if (comparison > 0) {
// left date is more precise than right date
leftString = left.getValueAsStringWithPrecision(right.getPrecision());
rightString = right.getValueAsString();
} else {
// right date is more precise than left date
rightString = right.getValueAsStringWithPrecision(left.getPrecision());
leftString = left.getValueAsString();
}
return leftString.equals(rightString);
}
}

View File

@ -17,33 +17,26 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.mdm.rules.matcher;
package ca.uhn.fhir.mdm.rules.matcher.fieldmatchers;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import ca.uhn.fhir.mdm.rules.matcher.util.StringMatcherUtils;
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 IMdmFieldMatcher {
private final IMdmStringMatcher myStringMatcher;
public HapiStringMatcher(IMdmStringMatcher theStringMatcher) {
myStringMatcher = theStringMatcher;
}
public HapiStringMatcher() {
myStringMatcher = String::equals;
}
public class HapiStringMatcher implements IMdmFieldMatcher {
@Override
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem) {
public boolean matches(IBase theLeftBase, IBase theRightBase, MdmMatcherJson theExtraMatchParams) {
if (theLeftBase instanceof IPrimitiveType && theRightBase instanceof IPrimitiveType) {
String leftString = extractString((IPrimitiveType<?>) theLeftBase, theExact);
String rightString = extractString((IPrimitiveType<?>) theRightBase, theExact);
String leftString = StringMatcherUtils.extractString((IPrimitiveType<?>) theLeftBase, theExtraMatchParams.getExact());
String rightString = StringMatcherUtils.extractString((IPrimitiveType<?>) theRightBase, theExtraMatchParams.getExact());
return myStringMatcher.matches(leftString, rightString);
return leftString.equals(rightString);
}
return false;
}

View File

@ -17,25 +17,34 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.mdm.rules.matcher;
package ca.uhn.fhir.mdm.rules.matcher.fieldmatchers;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.util.CanonicalIdentifier;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import ca.uhn.fhir.mdm.util.IdentifierUtil;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.util.CanonicalIdentifier;
import org.hl7.fhir.instance.model.api.IBase;
public class IdentifierMatcher implements IMdmFieldMatcher {
private boolean isEmpty(StringDt theValue) {
if (theValue == null) {
return true;
}
return theValue.isEmpty();
}
/**
* @return true if the two fhir identifiers are the same. If @param theIdentifierSystem is not null, then the
* matcher only returns true if the identifier systems also match this system.
* @throws UnsupportedOperationException if either Base is not an Identifier instance
*/
@Override
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem) {
public boolean matches(IBase theLeftBase, IBase theRightBase, MdmMatcherJson theParams) {
CanonicalIdentifier left = IdentifierUtil.identifierDtFromIdentifier(theLeftBase);
if (theIdentifierSystem != null) {
if (!theIdentifierSystem.equals(left.getSystemElement().getValueAsString())) {
if (theParams.getIdentifierSystem() != null) {
if (!theParams.getIdentifierSystem().equals(left.getSystemElement().getValueAsString())) {
return false;
}
}
@ -45,11 +54,4 @@ public class IdentifierMatcher implements IMdmFieldMatcher {
}
return left.equals(right);
}
private boolean isEmpty(StringDt theValue) {
if (theValue == null) {
return true;
}
return theValue.isEmpty();
}
}

View File

@ -17,7 +17,7 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.mdm.rules.matcher;
package ca.uhn.fhir.mdm.rules.matcher.fieldmatchers;
public enum MdmNameMatchModeEnum {
ANY_ORDER,

View File

@ -17,9 +17,11 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.mdm.rules.matcher;
package ca.uhn.fhir.mdm.rules.matcher.fieldmatchers;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import ca.uhn.fhir.mdm.util.NameUtil;
import ca.uhn.fhir.util.StringUtil;
import org.apache.commons.lang3.StringUtils;
@ -35,24 +37,27 @@ public class NameMatcher implements IMdmFieldMatcher {
private final MdmNameMatchModeEnum myMatchMode;
public NameMatcher(MdmNameMatchModeEnum theMatchMode) {
private final FhirContext myFhirContext;
public NameMatcher(FhirContext theFhirContext, MdmNameMatchModeEnum theMatchMode) {
myMatchMode = theMatchMode;
myFhirContext = theFhirContext;
}
@Override
public boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem) {
String leftFamilyName = NameUtil.extractFamilyName(theFhirContext, theLeftBase);
String rightFamilyName = NameUtil.extractFamilyName(theFhirContext, theRightBase);
public boolean matches(IBase theLeftBase, IBase theRightBase, MdmMatcherJson theParams) {
String leftFamilyName = NameUtil.extractFamilyName(myFhirContext, theLeftBase);
String rightFamilyName = NameUtil.extractFamilyName(myFhirContext, theRightBase);
if (StringUtils.isEmpty(leftFamilyName) || StringUtils.isEmpty(rightFamilyName)) {
return false;
}
boolean match = false;
List<String> leftGivenNames = NameUtil.extractGivenNames(theFhirContext, theLeftBase);
List<String> rightGivenNames = NameUtil.extractGivenNames(theFhirContext, theRightBase);
List<String> leftGivenNames = NameUtil.extractGivenNames(myFhirContext, theLeftBase);
List<String> rightGivenNames = NameUtil.extractGivenNames(myFhirContext, theRightBase);
if (!theExact) {
if (!theParams.getExact()) {
leftFamilyName = StringUtil.normalizeStringForSearchIndexing(leftFamilyName);
rightFamilyName = StringUtil.normalizeStringForSearchIndexing(rightFamilyName);
leftGivenNames = leftGivenNames.stream().map(StringUtil::normalizeStringForSearchIndexing).collect(Collectors.toList());

View File

@ -17,28 +17,25 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.mdm.rules.matcher;
package ca.uhn.fhir.mdm.rules.matcher.fieldmatchers;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.searchparam.nickname.NicknameSvc;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import ca.uhn.fhir.jpa.nickname.NicknameSvc;
import ca.uhn.fhir.mdm.rules.matcher.util.StringMatcherUtils;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.io.IOException;
import java.util.Collection;
import java.util.Locale;
public class NicknameMatcher implements IMdmStringMatcher {
public class NicknameMatcher implements IMdmFieldMatcher {
private final NicknameSvc myNicknameSvc;
public NicknameMatcher() {
try {
myNicknameSvc = new NicknameSvc();
} catch (IOException e) {
throw new ConfigurationException(Msg.code(2234) + "Unable to load nicknames", e);
}
public NicknameMatcher(NicknameSvc theNicknameSvc) {
myNicknameSvc = theNicknameSvc;
}
@Override
public boolean matches(String theLeftString, String theRightString) {
String leftString = theLeftString.toLowerCase(Locale.ROOT);
String rightString = theRightString.toLowerCase(Locale.ROOT);
@ -51,4 +48,15 @@ public class NicknameMatcher implements IMdmStringMatcher {
Collection<String> rightNames = myNicknameSvc.getEquivalentNames(rightString);
return rightNames.contains(leftString);
}
@Override
public boolean matches(IBase theLeftBase, IBase theRightBase, MdmMatcherJson theParams) {
if (theLeftBase instanceof IPrimitiveType && theRightBase instanceof IPrimitiveType) {
String leftString = StringMatcherUtils.extractString((IPrimitiveType<?>) theLeftBase, theParams.getExact());
String rightString = StringMatcherUtils.extractString((IPrimitiveType<?>) theRightBase, theParams.getExact());
return matches(leftString, rightString);
}
return false;
}
}

View File

@ -17,19 +17,27 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.mdm.rules.matcher;
package ca.uhn.fhir.mdm.rules.matcher.fieldmatchers;
import ca.uhn.fhir.context.phonetic.NumericEncoder;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import ca.uhn.fhir.mdm.rules.matcher.util.StringMatcherUtils;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
// Useful for numerical identifiers like phone numbers, address parts etc.
// This should not be used where decimals are important. A new "quantity matcher" should be added to handle cases like that.
public class NumericMatcher implements IMdmStringMatcher {
public class NumericMatcher implements IMdmFieldMatcher {
private final NumericEncoder encoder = new NumericEncoder();
@Override
public boolean matches(String theLeftString, String theRightString) {
String left = encoder.encode(theLeftString);
String right = encoder.encode(theRightString);
public boolean matches(IBase theLeftBase, IBase theRightBase, MdmMatcherJson theParams) {
if (theLeftBase instanceof IPrimitiveType && theRightBase instanceof IPrimitiveType) {
String left = encoder.encode(StringMatcherUtils.extractString((IPrimitiveType<?>) theLeftBase, theParams.getExact()));
String right = encoder.encode(StringMatcherUtils.extractString((IPrimitiveType<?>) theRightBase, theParams.getExact()));
return left.equals(right);
}
return false;
}
}

View File

@ -17,15 +17,20 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.mdm.rules.matcher;
package ca.uhn.fhir.mdm.rules.matcher.fieldmatchers;
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
import ca.uhn.fhir.context.phonetic.PhoneticEncoderEnum;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import ca.uhn.fhir.mdm.rules.matcher.util.StringMatcherUtils;
import ca.uhn.fhir.util.PhoneticEncoderUtil;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PhoneticEncoderMatcher implements IMdmStringMatcher {
public class PhoneticEncoderMatcher implements IMdmFieldMatcher {
private static final Logger ourLog = LoggerFactory.getLogger(PhoneticEncoderMatcher.class);
private final IPhoneticEncoder myStringEncoder;
@ -34,8 +39,16 @@ public class PhoneticEncoderMatcher implements IMdmStringMatcher {
myStringEncoder = PhoneticEncoderUtil.getEncoder(thePhoneticEnum.name());
}
@Override
public boolean matches(String theLeftString, String theRightString) {
return myStringEncoder.encode(theLeftString).equals(myStringEncoder.encode(theRightString));
}
@Override
public boolean matches(IBase theLeftBase, IBase theRightBase, MdmMatcherJson theParams) {
String leftString = StringMatcherUtils.extractString((IPrimitiveType<?>) theLeftBase, theParams.getExact());
String rightString = StringMatcherUtils.extractString((IPrimitiveType<?>) theRightBase, theParams.getExact());
return matches(leftString, rightString);
}
}

View File

@ -0,0 +1,39 @@
/*-
* #%L
* HAPI FHIR - Master Data Management
* %%
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
* %%
* 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%
*/
package ca.uhn.fhir.mdm.rules.matcher.fieldmatchers;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import ca.uhn.fhir.mdm.rules.matcher.util.StringMatcherUtils;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
public class SubstringStringMatcher implements IMdmFieldMatcher {
@Override
public boolean matches(IBase theLeftBase, IBase theRightBase, MdmMatcherJson theParams) {
if (theLeftBase instanceof IPrimitiveType && theRightBase instanceof IPrimitiveType) {
String left = StringMatcherUtils.extractString((IPrimitiveType<?>) theLeftBase, theParams.getExact());
String right = StringMatcherUtils.extractString((IPrimitiveType<?>) theRightBase, theParams.getExact());
return left.startsWith(right) || right.startsWith(left);
}
return false;
}
}

View File

@ -17,14 +17,27 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.mdm.rules.matcher;
package ca.uhn.fhir.mdm.rules.matcher.models;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import org.hl7.fhir.instance.model.api.IBase;
/**
* Measure how similar two IBase (resource fields) are to one another. 1.0 means identical. 0.0 means completely different.
*/
public interface IMdmFieldMatcher {
boolean matches(FhirContext theFhirContext, IBase theLeftBase, IBase theRightBase, boolean theExact, String theIdentifierSystem);
/**
* Checks if theLeftBase and theRightBase match, returning true if they do
* and false otherwise.
*/
boolean matches(IBase theLeftBase, IBase theRightBase, MdmMatcherJson theParams);
/**
* True if matcher can/will match empty (null) fields,
* false otherwise.
*/
default boolean isMatchingEmptyFields() {
// false because most people are overriding this
return false;
}
}

View File

@ -17,17 +17,37 @@
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.mdm.rules.matcher;
package ca.uhn.fhir.mdm.rules.matcher.models;
import ca.uhn.fhir.util.StringUtil;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
/**
* Enum for holding all the known FHIR Element matchers that we support in HAPI. The string matchers first
* encode the string using an Apache Encoder before comparing them.
* https://commons.apache.org/proper/commons-codec/userguide.html
*/
public enum MatchTypeEnum {
CAVERPHONE1,
CAVERPHONE2,
COLOGNE,
DOUBLE_METAPHONE,
MATCH_RATING_APPROACH,
METAPHONE,
NYSIIS,
REFINED_SOUNDEX,
SOUNDEX,
NICKNAME,
STRING,
SUBSTRING,
DATE,
NAME_ANY_ORDER,
NAME_FIRST_AND_LAST,
IDENTIFIER,
EMPTY_FIELD,
EXTENSION_ANY_ORDER,
NUMERIC;
public abstract class BaseHapiStringMetric {
protected String extractString(IPrimitiveType<?> thePrimitive, boolean theExact) {
String theString = thePrimitive.getValueAsString();
if (theExact) {
return theString;
}
return StringUtil.normalizeStringForSearchIndexing(theString);
}
}

View File

@ -0,0 +1,14 @@
package ca.uhn.fhir.mdm.rules.matcher.util;
import ca.uhn.fhir.util.StringUtil;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
public class StringMatcherUtils {
public static String extractString(IPrimitiveType<?> thePrimitive, boolean theExact) {
String theString = thePrimitive.getValueAsString();
if (theExact) {
return theString;
}
return StringUtil.normalizeStringForSearchIndexing(theString);
}
}

View File

@ -20,7 +20,7 @@
package ca.uhn.fhir.mdm.rules.similarity;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.rules.matcher.BaseHapiStringMetric;
import ca.uhn.fhir.mdm.rules.matcher.util.StringMatcherUtils;
import info.debatty.java.stringsimilarity.interfaces.NormalizedStringSimilarity;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
@ -28,7 +28,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType;
/**
* Similarity measure for two IBase fields whose similarity can be measured by their String representations.
*/
public class HapiStringSimilarity extends BaseHapiStringMetric implements IMdmFieldSimilarity {
public class HapiStringSimilarity implements IMdmFieldSimilarity {
private final NormalizedStringSimilarity myStringSimilarity;
public HapiStringSimilarity(NormalizedStringSimilarity theStringSimilarity) {
@ -38,8 +38,8 @@ public class HapiStringSimilarity extends BaseHapiStringMetric implements IMdmFi
@Override
public double similarity(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);
String leftString = StringMatcherUtils.extractString((IPrimitiveType<?>) theLeftBase, theExact);
String rightString = StringMatcherUtils.extractString((IPrimitiveType<?>) theRightBase, theExact);
return myStringSimilarity.similarity(leftString, rightString);
}

View File

@ -21,9 +21,16 @@ package ca.uhn.fhir.mdm.rules.svc;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.fhirpath.IFhirPath;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.mdm.api.MdmMatchEvaluation;
import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
import ca.uhn.fhir.mdm.rules.json.MdmSimilarityJson;
import ca.uhn.fhir.mdm.rules.matcher.IMatcherFactory;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import ca.uhn.fhir.mdm.rules.matcher.models.MatchTypeEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.util.FhirTerser;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
@ -48,7 +55,16 @@ public class MdmResourceFieldMatcher {
private final String myName;
private final boolean myIsFhirPathExpression;
public MdmResourceFieldMatcher(FhirContext theFhirContext, MdmFieldMatchJson theMdmFieldMatchJson, MdmRulesJson theMdmRulesJson) {
private final IMatcherFactory myIMatcherFactory;
public MdmResourceFieldMatcher(
FhirContext theFhirContext,
IMatcherFactory theIMatcherFactory,
MdmFieldMatchJson theMdmFieldMatchJson,
MdmRulesJson theMdmRulesJson
) {
myIMatcherFactory = theIMatcherFactory;
myFhirContext = theFhirContext;
myMdmFieldMatchJson = theMdmFieldMatchJson;
myResourceType = theMdmFieldMatchJson.getResourceType();
@ -70,7 +86,6 @@ public class MdmResourceFieldMatcher {
* @param theRightResource the second {@link IBaseResource}
* @return A boolean indicating whether they match.
*/
@SuppressWarnings("rawtypes")
public MdmMatchEvaluation match(IBaseResource theLeftResource, IBaseResource theRightResource) {
validate(theLeftResource);
validate(theRightResource);
@ -90,12 +105,12 @@ public class MdmResourceFieldMatcher {
return match(leftValues, rightValues);
}
@SuppressWarnings("rawtypes")
private MdmMatchEvaluation match(List<IBase> theLeftValues, List<IBase> theRightValues) {
MdmMatchEvaluation retval = new MdmMatchEvaluation(false, 0.0);
boolean isMatchingEmptyFieldValues = (theLeftValues.isEmpty() && theRightValues.isEmpty());
if (isMatchingEmptyFieldValues && myMdmFieldMatchJson.isMatcherSupportingEmptyFields()) {
IMdmFieldMatcher matcher = getFieldMatcher();
if (isMatchingEmptyFieldValues && (matcher != null && matcher.isMatchingEmptyFields())) {
return match((IBase) null, (IBase) null);
}
@ -110,7 +125,18 @@ public class MdmResourceFieldMatcher {
}
private MdmMatchEvaluation match(IBase theLeftValue, IBase theRightValue) {
return myMdmFieldMatchJson.match(myFhirContext, theLeftValue, theRightValue);
IMdmFieldMatcher matcher = getFieldMatcher();
if (matcher != null) {
boolean isMatches = matcher.matches(theLeftValue, theRightValue, myMdmFieldMatchJson.getMatcher());
return new MdmMatchEvaluation(isMatches, isMatches ? 1.0 : 0.0);
}
MdmSimilarityJson similarity = myMdmFieldMatchJson.getSimilarity();
if (similarity != null) {
return similarity.match(myFhirContext, theLeftValue, theRightValue);
}
throw new InternalErrorException(Msg.code(1522) + "Field Match " + myName + " has neither a matcher nor a similarity.");
}
private void validate(IBaseResource theResource) {
@ -136,4 +162,17 @@ public class MdmResourceFieldMatcher {
public String getName() {
return myName;
}
private IMdmFieldMatcher getFieldMatcher() {
MdmMatcherJson matcherJson = myMdmFieldMatchJson.getMatcher();
MatchTypeEnum matchTypeEnum = null;
if (matcherJson != null) {
matchTypeEnum = matcherJson.getAlgorithm();
}
if (matchTypeEnum == null) {
return null;
}
return myIMatcherFactory.getFieldMatcherForMatchType(matchTypeEnum);
}
}

View File

@ -30,6 +30,8 @@ import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.log.Logs;
import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson;
import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
import ca.uhn.fhir.mdm.rules.matcher.IMatcherFactory;
import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.springframework.stereotype.Service;
@ -48,16 +50,18 @@ public class MdmResourceMatcherSvc {
private static final Logger ourLog = Logs.getMdmTroubleshootingLog();
private final FhirContext myFhirContext;
private MdmRulesJson myMdmRulesJson;
private final IMatcherFactory myMatcherFactory;
private final List<MdmResourceFieldMatcher> myFieldMatchers = new ArrayList<>();
public MdmResourceMatcherSvc(FhirContext theFhirContext, IMdmSettings theMdmSettings) {
private MdmRulesJson myMdmRulesJson;
public MdmResourceMatcherSvc(
FhirContext theFhirContext,
IMatcherFactory theIMatcherFactory,
IMdmSettings theMdmSettings
) {
myFhirContext = theFhirContext;
setMdmSettings(theMdmSettings);
}
public void setMdmSettings(IMdmSettings theMdmSettings) {
myMatcherFactory = theIMatcherFactory;
myMdmRulesJson = theMdmSettings.getMdmRules();
addFieldMatchers();
@ -69,7 +73,7 @@ public class MdmResourceMatcherSvc {
}
myFieldMatchers.clear();
for (MdmFieldMatchJson matchFieldJson : myMdmRulesJson.getMatchFields()) {
myFieldMatchers.add(new MdmResourceFieldMatcher( myFhirContext, matchFieldJson, myMdmRulesJson));
myFieldMatchers.add(new MdmResourceFieldMatcher(myFhirContext, myMatcherFactory, matchFieldJson, myMdmRulesJson));
}
}
@ -79,7 +83,6 @@ public class MdmResourceMatcherSvc {
*
* @param theLeftResource The first {@link IBaseResource}.
* @param theRightResource The second {@link IBaseResource}
*
* @return an {@link MdmMatchResultEnum} indicating the result of the comparison.
*/
public MdmMatchOutcome getMatchResult(IBaseResource theLeftResource, IBaseResource theRightResource) {
@ -105,11 +108,11 @@ public class MdmResourceMatcherSvc {
* start with a binary representation of the value 0 for long: 0000
* first_name matches, so the value `1` is bitwise-ORed to the current value (0) in right-most position.
* `0001`
*
* <p>
* Next, we look at the second field comparator, and see if it matches. If it does, we left-shift 1 by the index
* of the comparator, in this case also 1.
* `0010`
*
* <p>
* Then, we bitwise-or it with the current retval:
* 0001|0010 = 0011
* The binary string is now `0011`, which when you return it as a long becomes `3`.
@ -146,11 +149,16 @@ public class MdmResourceMatcherSvc {
return retVal;
}
private boolean isValidResourceType(String theResourceType, String theFieldComparatorType) {
return (
theFieldComparatorType.equalsIgnoreCase(MdmConstants.ALL_RESOURCE_SEARCH_PARAM_TYPE)
|| theFieldComparatorType.equalsIgnoreCase(theResourceType)
);
}
@VisibleForTesting
public void setMdmRulesJson(MdmRulesJson theMdmRulesJson) {
myMdmRulesJson = theMdmRulesJson;
addFieldMatchers();
}
}

View File

@ -1,14 +1,19 @@
package ca.uhn.fhir.mdm;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.nickname.NicknameSvc;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.api.MdmMatchOutcome;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.rules.config.MdmRuleValidator;
import ca.uhn.fhir.mdm.rules.config.MdmSettings;
import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
import ca.uhn.fhir.mdm.rules.matcher.IMatcherFactory;
import ca.uhn.fhir.mdm.rules.matcher.MdmMatcherFactory;
import ca.uhn.fhir.mdm.rules.svc.MdmResourceMatcherSvc;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ -20,6 +25,20 @@ public abstract class BaseR4Test {
protected static final FhirContext ourFhirContext = FhirContext.forR4();
protected ISearchParamRegistry mySearchParamRetriever = mock(ISearchParamRegistry.class);
protected IMatcherFactory myIMatcherFactory;
protected IMdmSettings myMdmSettings;
@BeforeEach
public void before() {
myMdmSettings = mock(IMdmSettings.class);
myIMatcherFactory = new MdmMatcherFactory(
ourFhirContext,
myMdmSettings,
new NicknameSvc()
);
}
protected Patient buildJohn() {
Patient patient = new Patient();
patient.addName().addGiven("John");
@ -35,7 +54,10 @@ public abstract class BaseR4Test {
}
protected MdmResourceMatcherSvc buildMatcher(MdmRulesJson theMdmRulesJson) {
return new MdmResourceMatcherSvc(ourFhirContext, new MdmSettings(new MdmRuleValidator(ourFhirContext, mySearchParamRetriever)).setMdmRules(theMdmRulesJson));
return new MdmResourceMatcherSvc(ourFhirContext,
myIMatcherFactory,
new MdmSettings(new MdmRuleValidator(ourFhirContext, mySearchParamRetriever)).setMdmRules(theMdmRulesJson)
);
}
protected void assertMatch(MdmMatchResultEnum theExpectedMatchEnum, MdmMatchOutcome theMatchResult) {

View File

@ -1,7 +1,7 @@
package ca.uhn.fhir.mdm.rules.json;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.rules.matcher.MdmMatcherEnum;
import ca.uhn.fhir.mdm.rules.matcher.models.MatchTypeEnum;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -27,7 +27,7 @@ public class VectorMatchResultMapTest {
@Test
public void testMatchBeforePossibleMatch() {
MdmRulesJson mdmRulesJson = new MdmRulesJson();
MdmMatcherJson matcherJson = new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.STRING);
MdmMatcherJson matcherJson = new MdmMatcherJson().setAlgorithm(MatchTypeEnum.STRING);
mdmRulesJson.addMatchField(new MdmFieldMatchJson().setName("given").setResourceType("Patient").setResourcePath("name.given").setMatcher(matcherJson));
mdmRulesJson.addMatchField(new MdmFieldMatchJson().setName("family").setResourceType("Patient").setResourcePath("name.family").setMatcher(matcherJson));
mdmRulesJson.addMatchField(new MdmFieldMatchJson().setName("prefix").setResourceType("Patient").setResourcePath("name.prefix").setMatcher(matcherJson));

View File

@ -1,7 +1,16 @@
package ca.uhn.fhir.mdm.rules.matcher;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import org.junit.jupiter.api.BeforeEach;
public abstract class BaseMatcherR4Test {
protected static final FhirContext ourFhirContext = FhirContext.forR4();
protected MdmMatcherJson myMdmMatcherJson;
@BeforeEach
public void before() {
myMdmMatcherJson = new MdmMatcherJson();
}
}

View File

@ -1,8 +1,10 @@
package ca.uhn.fhir.mdm.rules.matcher;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.HapiDateMatcher;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.DateType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Calendar;
@ -14,6 +16,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class DateMatcherR4Test extends BaseMatcherR4Test {
private HapiDateMatcher myDateMatcher;
@BeforeEach
public void before() {
super.before();
myDateMatcher = new HapiDateMatcher(ourFhirContext);
}
@Test
public void testExactDatePrecision() {
Calendar cal = new GregorianCalendar(2020, 6, 15);
@ -43,7 +53,8 @@ public class DateMatcherR4Test extends BaseMatcherR4Test {
}
private boolean dateMatch(Date theDate, Date theSameMonth, TemporalPrecisionEnum theTheDay) {
return MdmMatcherEnum.DATE.match(ourFhirContext, new DateType(theDate, theTheDay), new DateType(theSameMonth, theTheDay), true, null);
myMdmMatcherJson.setExact(true);
return myDateMatcher.matches(new DateType(theDate, theTheDay), new DateType(theSameMonth, theTheDay), myMdmMatcherJson);
}
@Test
@ -87,12 +98,11 @@ public class DateMatcherR4Test extends BaseMatcherR4Test {
}
private boolean dateTimeMatch(Date theDate, Date theSecondDate, TemporalPrecisionEnum thePrecision, TemporalPrecisionEnum theSecondPrecision) {
return MdmMatcherEnum.DATE.match(
ourFhirContext,
myMdmMatcherJson.setExact(true);
return myDateMatcher.matches(
new DateTimeType(theDate, thePrecision),
new DateTimeType(theSecondDate, theSecondPrecision),
true,
null
myMdmMatcherJson
);
}
}

View File

@ -2,6 +2,8 @@ package ca.uhn.fhir.mdm.rules.matcher;
import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.EmptyFieldMatcher;
import ca.uhn.fhir.mdm.rules.matcher.models.MatchTypeEnum;
import ca.uhn.fhir.model.primitive.StringDt;
import org.junit.jupiter.api.Test;
@ -16,16 +18,16 @@ public class EmptyFieldMatcherTest extends BaseMatcherR4Test {
StringDt leftEmpty = new StringDt("");
StringDt rightEmpty = new StringDt("");
StringDt right = new StringDt("a value");
MdmMatcherJson matcher = new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.EMPTY_FIELD);
MdmFieldMatchJson fieldMatch = new MdmFieldMatchJson().setMatcher(matcher);
assertTrue(fieldMatch.match(ourFhirContext, null, null).match);
assertTrue(fieldMatch.match(ourFhirContext, null, rightEmpty).match);
assertTrue(fieldMatch.match(ourFhirContext, leftEmpty, null).match);
assertTrue(fieldMatch.match(ourFhirContext, leftEmpty, rightEmpty).match);
assertFalse(fieldMatch.match(ourFhirContext, null, right).match);
assertFalse(fieldMatch.match(ourFhirContext, left, null).match);
assertFalse(fieldMatch.match(ourFhirContext, left, right).match);
EmptyFieldMatcher fieldMatch = new EmptyFieldMatcher();
assertTrue(fieldMatch.matches(null, null, null));
assertTrue(fieldMatch.matches(null, rightEmpty, null));
assertTrue(fieldMatch.matches(leftEmpty, null, null));
assertTrue(fieldMatch.matches(leftEmpty, rightEmpty, null));
assertFalse(fieldMatch.matches(null, right, null));
assertFalse(fieldMatch.matches(left, null, null));
assertFalse(fieldMatch.matches(left, right, null));
}

View File

@ -1,16 +1,28 @@
package ca.uhn.fhir.mdm.rules.matcher;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.ExtensionMatcher;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ExtensionMatcherR4Test extends BaseMatcherR4Test {
private ExtensionMatcher myExtensionMatcher;
@BeforeEach
public void before() {
super.before();
myExtensionMatcher = new ExtensionMatcher();
}
@Test
public void testPatientWithMatchingExtension(){
Patient patient1 = new Patient();
@ -19,7 +31,7 @@ public class ExtensionMatcherR4Test extends BaseMatcherR4Test {
patient1.addExtension("asd",new StringType("Patient1"));
patient2.addExtension("asd",new StringType("Patient1"));
assertTrue(MdmMatcherEnum.EXTENSION_ANY_ORDER.match(ourFhirContext, patient1, patient2, false, null));
assertTrue(match(patient1, patient2));
}
@Test
@ -30,7 +42,7 @@ public class ExtensionMatcherR4Test extends BaseMatcherR4Test {
patient1.addExtension("asd",new StringType("Patient1"));
patient2.addExtension("asd",new StringType("Patient2"));
assertFalse(MdmMatcherEnum.EXTENSION_ANY_ORDER.match(ourFhirContext, patient1, patient2, false, null));
assertFalse(match(patient1, patient2));
}
@Test
@ -41,7 +53,7 @@ public class ExtensionMatcherR4Test extends BaseMatcherR4Test {
patient1.addExtension("asd",new StringType("Patient1"));
patient2.addExtension("asd1",new StringType("Patient1"));
assertFalse(MdmMatcherEnum.EXTENSION_ANY_ORDER.match(ourFhirContext, patient1, patient2, false, null));
assertFalse(match(patient1, patient2));
}
@Test
@ -54,7 +66,7 @@ public class ExtensionMatcherR4Test extends BaseMatcherR4Test {
patient2.addExtension("asd",new StringType("Patient1"));
patient2.addExtension("asdasd", new StringType("some value"));
assertTrue(MdmMatcherEnum.EXTENSION_ANY_ORDER.match(ourFhirContext, patient1, patient2, false, null));
assertTrue(match(patient1, patient2));
}
@Test
@ -65,7 +77,7 @@ public class ExtensionMatcherR4Test extends BaseMatcherR4Test {
patient1.addExtension("asd", new IntegerType(123));
patient2.addExtension("asd", new IntegerType(123));
assertTrue(MdmMatcherEnum.EXTENSION_ANY_ORDER.match(ourFhirContext, patient1, patient2, false, null));
assertTrue(match(patient1, patient2));
}
@Test
@ -73,7 +85,10 @@ public class ExtensionMatcherR4Test extends BaseMatcherR4Test {
Patient patient1 = new Patient();
Patient patient2 = new Patient();
assertFalse(MdmMatcherEnum.EXTENSION_ANY_ORDER.match(ourFhirContext, patient1, patient2, false, null));
assertFalse(match(patient1, patient2));
}
private boolean match(IBase theFirst, IBase theSecond) {
return myExtensionMatcher.matches(theFirst, theSecond, myMdmMatcherJson);
}
}

View File

@ -1,8 +1,9 @@
package ca.uhn.fhir.mdm.rules.matcher;
import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.IdentifierMatcher;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.r4.model.Identifier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
@ -14,15 +15,20 @@ public class IdentifierMatcherR4Test extends BaseMatcherR4Test {
private static final String MATCHING_VALUE = "matchme";
private static final String OTHER_VALUE = "strange";
private IdentifierMatcher myIdentifierMatcher;
@BeforeEach
public void before() {
super.before();
myIdentifierMatcher = new IdentifierMatcher();
}
@Test
public void testIdentifierMatch() {
Identifier left = new Identifier().setSystem(MATCHING_SYSTEM).setValue(MATCHING_VALUE);
Identifier right = new Identifier().setSystem(MATCHING_SYSTEM).setValue(MATCHING_VALUE);
MdmMatcherJson matcher = new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.IDENTIFIER);
MdmFieldMatchJson fieldMatch = new MdmFieldMatchJson().setMatcher(matcher);
assertTrue(fieldMatch.match(ourFhirContext, left, right).match);
assertTrue(match(left, right));
}
@Test
@ -33,17 +39,15 @@ public class IdentifierMatcherR4Test extends BaseMatcherR4Test {
Identifier rightNoSystem = new Identifier().setValue(MATCHING_VALUE);
Identifier rightNoValue = new Identifier().setSystem(MATCHING_SYSTEM);
MdmMatcherJson matcher = new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.IDENTIFIER);
MdmFieldMatchJson fieldMatch = new MdmFieldMatchJson().setMatcher(matcher);
assertFalse(fieldMatch.match(ourFhirContext, left, rightWrongSystem).match);
assertFalse(fieldMatch.match(ourFhirContext, left, rightWrongValue).match);
assertFalse(fieldMatch.match(ourFhirContext, left, rightNoSystem).match);
assertFalse(fieldMatch.match(ourFhirContext, left, rightNoValue).match);
assertFalse(fieldMatch.match(ourFhirContext, rightWrongSystem, left).match);
assertFalse(fieldMatch.match(ourFhirContext, rightWrongValue, left).match);
assertFalse(fieldMatch.match(ourFhirContext, rightNoSystem, left).match);
assertFalse(fieldMatch.match(ourFhirContext, rightNoValue, left).match);
assertFalse(match(left, rightWrongSystem));
assertFalse(match(left, rightWrongValue));
assertFalse(match(left, rightNoSystem));
assertFalse(match(left, rightNoValue));
assertFalse(match(rightWrongSystem, left));
assertFalse(match(rightWrongValue, left));
assertFalse(match(rightNoSystem, left));
assertFalse(match(rightNoValue, left));
}
@Test
@ -51,10 +55,9 @@ public class IdentifierMatcherR4Test extends BaseMatcherR4Test {
Identifier left = new Identifier().setSystem(MATCHING_SYSTEM);
Identifier right = new Identifier().setSystem(MATCHING_SYSTEM);
MdmMatcherJson matcher = new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.IDENTIFIER).setIdentifierSystem(MATCHING_SYSTEM);
MdmFieldMatchJson fieldMatch = new MdmFieldMatchJson().setMatcher(matcher);
myMdmMatcherJson.setIdentifierSystem(MATCHING_SYSTEM);
assertFalse(fieldMatch.match(ourFhirContext, left, right).match);
assertFalse(match(left, right));
}
@Test
@ -62,10 +65,9 @@ public class IdentifierMatcherR4Test extends BaseMatcherR4Test {
Identifier left = new Identifier().setSystem(MATCHING_SYSTEM).setValue(MATCHING_VALUE);
Identifier right = new Identifier().setSystem(MATCHING_SYSTEM).setValue(MATCHING_VALUE);
MdmMatcherJson matcher = new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.IDENTIFIER).setIdentifierSystem(MATCHING_SYSTEM);
MdmFieldMatchJson fieldMatch = new MdmFieldMatchJson().setMatcher(matcher);
myMdmMatcherJson.setIdentifierSystem(MATCHING_SYSTEM);
assertTrue(fieldMatch.match(ourFhirContext, left, right).match);
assertTrue(match(left, right));
}
@Test
@ -73,9 +75,12 @@ public class IdentifierMatcherR4Test extends BaseMatcherR4Test {
Identifier left = new Identifier().setSystem(OTHER_SYSTEM).setValue(MATCHING_VALUE);
Identifier right = new Identifier().setSystem(OTHER_SYSTEM).setValue(MATCHING_VALUE);
MdmMatcherJson matcher = new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.IDENTIFIER).setIdentifierSystem(MATCHING_SYSTEM);
MdmFieldMatchJson fieldMatch = new MdmFieldMatchJson().setMatcher(matcher);
myMdmMatcherJson.setIdentifierSystem(MATCHING_SYSTEM);
assertFalse(fieldMatch.match(ourFhirContext, left, right).match);
assertFalse(match(left, right));
}
private boolean match(IBase theFirst, IBase theSecond) {
return myIdentifierMatcher.matches(theFirst, theSecond, myMdmMatcherJson);
}
}

View File

@ -1,31 +1,48 @@
package ca.uhn.fhir.mdm.rules.matcher;
import ca.uhn.fhir.jpa.nickname.NicknameSvc;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.NicknameMatcher;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class NicknameMatcherTest {
IMdmStringMatcher matcher = new NicknameMatcher();
IMdmFieldMatcher matcher;
NicknameSvc myNicknameSvc = new NicknameSvc();
@BeforeEach
public void begin() {
matcher = new NicknameMatcher(myNicknameSvc);
}
@Test
public void testMatches() {
assertTrue(matcher.matches("Ken", "ken"));
assertTrue(matcher.matches("ken", "Ken"));
assertTrue(matcher.matches("Ken", "Ken"));
assertTrue(matcher.matches("Kenneth", "Ken"));
assertTrue(matcher.matches("Kenneth", "Kenny"));
assertTrue(matcher.matches("Ken", "Kenneth"));
assertTrue(matcher.matches("Kenny", "Kenneth"));
assertTrue(matcher.matches("Jim", "Jimmy"));
assertTrue(matcher.matches("Jimmy", "Jim"));
assertTrue(matcher.matches("Jim", "James"));
assertTrue(matcher.matches("Jimmy", "James"));
assertTrue(matcher.matches("James", "Jimmy"));
assertTrue(matcher.matches("James", "Jim"));
Assertions.assertTrue(match("Ken", "ken"));
Assertions.assertTrue(match("ken", "Ken"));
Assertions.assertTrue(match("Ken", "Ken"));
Assertions.assertTrue(match("Kenneth", "Ken"));
Assertions.assertTrue(match("Kenneth", "Kenny"));
Assertions.assertTrue(match("Ken", "Kenneth"));
Assertions.assertTrue(match("Kenny", "Kenneth"));
Assertions.assertTrue(match("Jim", "Jimmy"));
Assertions.assertTrue(match("Jimmy", "Jim"));
Assertions.assertTrue(match("Jim", "James"));
Assertions.assertTrue(match("Jimmy", "James"));
Assertions.assertTrue(match("James", "Jimmy"));
Assertions.assertTrue(match("James", "Jim"));
assertFalse(matcher.matches("Ken", "Bob"));
Assertions.assertFalse(match("Ken", "Bob"));
// These aren't nickname matches. If you want matches like these use a phonetic matcher
assertFalse(matcher.matches("Allen", "Allan"));
Assertions.assertFalse(match("Allen", "Allan"));
}
private boolean match(String theFirst, String theSecond) {
MdmMatcherJson json = new MdmMatcherJson();
json.setExact(true);
return matcher.matches(new StringType(theFirst), new StringType(theSecond), json);
}
}

View File

@ -1,106 +1,140 @@
package ca.uhn.fhir.mdm.rules.matcher;
import ca.uhn.fhir.jpa.nickname.NicknameSvc;
import ca.uhn.fhir.mdm.api.IMdmSettings;
import ca.uhn.fhir.mdm.rules.matcher.models.IMdmFieldMatcher;
import ca.uhn.fhir.mdm.rules.matcher.models.MatchTypeEnum;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.DateType;
import org.hl7.fhir.r4.model.Enumeration;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
public class StringMatcherR4Test extends BaseMatcherR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(StringMatcherR4Test.class);
public static final String LEFT_NAME = "namadega";
public static final String RIGHT_NAME = "namaedga";
private IMatcherFactory myIMatcherFactory;
private IMdmSettings myMdmSettings;
@BeforeEach
public void before() {
super.before();
myMdmSettings = mock(IMdmSettings.class);
myIMatcherFactory = new MdmMatcherFactory(
ourFhirContext,
myMdmSettings,
new NicknameSvc()
);
}
private @Nonnull IMdmFieldMatcher getFieldMatcher(MatchTypeEnum theMatchTypeEnum) {
return myIMatcherFactory.getFieldMatcherForMatchType(theMatchTypeEnum);
}
@Test
public void testNamadega() {
String left = LEFT_NAME;
String right = RIGHT_NAME;
assertTrue(match(MdmMatcherEnum.COLOGNE, left, right));
assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, left, right));
assertTrue(match(MdmMatcherEnum.MATCH_RATING_APPROACH, left, right));
assertTrue(match(MdmMatcherEnum.METAPHONE, left, right));
assertTrue(match(MdmMatcherEnum.SOUNDEX, left, right));
assertTrue(match(MdmMatcherEnum.METAPHONE, left, right));
assertTrue(match(MatchTypeEnum.COLOGNE, left, right));
assertTrue(match(MatchTypeEnum.DOUBLE_METAPHONE, left, right));
assertTrue(match(MatchTypeEnum.MATCH_RATING_APPROACH, left, right));
assertTrue(match(MatchTypeEnum.METAPHONE, left, right));
assertTrue(match(MatchTypeEnum.SOUNDEX, left, right));
assertFalse(match(MdmMatcherEnum.CAVERPHONE1, left, right));
assertFalse(match(MdmMatcherEnum.CAVERPHONE2, left, right));
assertFalse(match(MdmMatcherEnum.NYSIIS, left, right));
assertFalse(match(MdmMatcherEnum.REFINED_SOUNDEX, left, right));
assertFalse(match(MdmMatcherEnum.STRING, left, right));
assertFalse(match(MdmMatcherEnum.SUBSTRING, left, right));
assertFalse(match(MatchTypeEnum.CAVERPHONE1, left, right));
assertFalse(match(MatchTypeEnum.CAVERPHONE2, left, right));
assertFalse(match(MatchTypeEnum.NYSIIS, left, right));
assertFalse(match(MatchTypeEnum.REFINED_SOUNDEX, left, right));
assertFalse(match(MatchTypeEnum.STRING, left, right));
assertFalse(match(MatchTypeEnum.SUBSTRING, left, right));
}
@Test
public void testNumeric() {
assertTrue(match(MdmMatcherEnum.NUMERIC, "4169671111", "(416) 967-1111"));
assertFalse(match(MdmMatcherEnum.NUMERIC, "5169671111", "(416) 967-1111"));
assertFalse(match(MdmMatcherEnum.NUMERIC, "4169671111", "(416) 967-1111x123"));
assertTrue(match(MatchTypeEnum.NUMERIC, "4169671111", "(416) 967-1111"));
assertFalse(match(MatchTypeEnum.NUMERIC, "5169671111", "(416) 967-1111"));
assertFalse(match(MatchTypeEnum.NUMERIC, "4169671111", "(416) 967-1111x123"));
}
@Test
public void testMetaphone() {
assertTrue(match(MdmMatcherEnum.METAPHONE, "Durie", "dury"));
assertTrue(match(MdmMatcherEnum.METAPHONE, "Balo", "ballo"));
assertTrue(match(MdmMatcherEnum.METAPHONE, "Hans Peter", "Hanspeter"));
assertTrue(match(MdmMatcherEnum.METAPHONE, "Lawson", "Law son"));
assertTrue(match(MatchTypeEnum.METAPHONE, "Durie", "dury"));
assertTrue(match(MatchTypeEnum.METAPHONE, "Balo", "ballo"));
assertTrue(match(MatchTypeEnum.METAPHONE, "Hans Peter", "Hanspeter"));
assertTrue(match(MatchTypeEnum.METAPHONE, "Lawson", "Law son"));
assertFalse(match(MdmMatcherEnum.METAPHONE, "Allsop", "Allsob"));
assertFalse(match(MdmMatcherEnum.METAPHONE, "Gevne", "Geve"));
assertFalse(match(MdmMatcherEnum.METAPHONE, "Bruce", "Bruch"));
assertFalse(match(MdmMatcherEnum.METAPHONE, "Smith", "Schmidt"));
assertFalse(match(MdmMatcherEnum.METAPHONE, "Jyothi", "Jyoti"));
assertFalse(match(MatchTypeEnum.METAPHONE, "Allsop", "Allsob"));
assertFalse(match(MatchTypeEnum.METAPHONE, "Gevne", "Geve"));
assertFalse(match(MatchTypeEnum.METAPHONE, "Bruce", "Bruch"));
assertFalse(match(MatchTypeEnum.METAPHONE, "Smith", "Schmidt"));
assertFalse(match(MatchTypeEnum.METAPHONE, "Jyothi", "Jyoti"));
}
@Test
public void testDoubleMetaphone() {
assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Durie", "dury"));
assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Balo", "ballo"));
assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Hans Peter", "Hanspeter"));
assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Lawson", "Law son"));
assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Allsop", "Allsob"));
assertTrue(match(MatchTypeEnum.DOUBLE_METAPHONE, "Durie", "dury"));
assertTrue(match(MatchTypeEnum.DOUBLE_METAPHONE, "Balo", "ballo"));
assertTrue(match(MatchTypeEnum.DOUBLE_METAPHONE, "Hans Peter", "Hanspeter"));
assertTrue(match(MatchTypeEnum.DOUBLE_METAPHONE, "Lawson", "Law son"));
assertTrue(match(MatchTypeEnum.DOUBLE_METAPHONE, "Allsop", "Allsob"));
assertFalse(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Gevne", "Geve"));
assertFalse(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Bruce", "Bruch"));
assertFalse(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Smith", "Schmidt"));
assertFalse(match(MdmMatcherEnum.DOUBLE_METAPHONE, "Jyothi", "Jyoti"));
assertFalse(match(MatchTypeEnum.DOUBLE_METAPHONE, "Gevne", "Geve"));
assertFalse(match(MatchTypeEnum.DOUBLE_METAPHONE, "Bruce", "Bruch"));
assertFalse(match(MatchTypeEnum.DOUBLE_METAPHONE, "Smith", "Schmidt"));
assertFalse(match(MatchTypeEnum.DOUBLE_METAPHONE, "Jyothi", "Jyoti"));
}
@Test
public void testNormalizeCase() {
assertTrue(match(MdmMatcherEnum.STRING, "joe", "JoE"));
assertTrue(match(MdmMatcherEnum.STRING, "MCTAVISH", "McTavish"));
assertTrue(match(MatchTypeEnum.STRING, "joe", "JoE"));
assertTrue(match(MatchTypeEnum.STRING, "MCTAVISH", "McTavish"));
assertFalse(match(MdmMatcherEnum.STRING, "joey", "joe"));
assertFalse(match(MdmMatcherEnum.STRING, "joe", "joey"));
assertFalse(match(MatchTypeEnum.STRING, "joey", "joe"));
assertFalse(match(MatchTypeEnum.STRING, "joe", "joey"));
}
@Test
public void testExactString() {
assertTrue(MdmMatcherEnum.STRING.match(ourFhirContext, new StringType("Jilly"), new StringType("Jilly"), true, null));
myMdmMatcherJson.setExact(true);
assertFalse(MdmMatcherEnum.STRING.match(ourFhirContext, new StringType("MCTAVISH"), new StringType("McTavish"), true, null));
assertFalse(MdmMatcherEnum.STRING.match(ourFhirContext, new StringType("Durie"), new StringType("dury"), true, null));
assertTrue(getFieldMatcher(MatchTypeEnum.STRING).matches(new StringType("Jilly"), new StringType("Jilly"), myMdmMatcherJson));
assertFalse(getFieldMatcher(MatchTypeEnum.STRING).matches(new StringType("MCTAVISH"), new StringType("McTavish"), myMdmMatcherJson));
assertFalse(getFieldMatcher(MatchTypeEnum.STRING).matches(new StringType("Durie"), new StringType("dury"), myMdmMatcherJson));
}
@Test
public void testExactBoolean() {
assertTrue(MdmMatcherEnum.STRING.match(ourFhirContext, new BooleanType(true), new BooleanType(true), true, null));
myMdmMatcherJson.setExact(true);
assertFalse(MdmMatcherEnum.STRING.match(ourFhirContext, new BooleanType(true), new BooleanType(false), true, null));
assertFalse(MdmMatcherEnum.STRING.match(ourFhirContext, new BooleanType(false), new BooleanType(true), true, null));
assertTrue(getFieldMatcher(MatchTypeEnum.STRING).matches(new BooleanType(true), new BooleanType(true), myMdmMatcherJson));
assertFalse(getFieldMatcher(MatchTypeEnum.STRING).matches(new BooleanType(true), new BooleanType(false), myMdmMatcherJson));
assertFalse(getFieldMatcher(MatchTypeEnum.STRING).matches(new BooleanType(false), new BooleanType(true), myMdmMatcherJson));
}
@Test
public void testExactDateString() {
assertTrue(MdmMatcherEnum.STRING.match(ourFhirContext, new DateType("1965-08-09"), new DateType("1965-08-09"), true, null));
myMdmMatcherJson.setExact(true);
assertFalse(MdmMatcherEnum.STRING.match(ourFhirContext, new DateType("1965-08-09"), new DateType("1965-09-08"), true, null));
assertTrue(getFieldMatcher(MatchTypeEnum.STRING).matches(new DateType("1965-08-09"), new DateType("1965-08-09"), myMdmMatcherJson));
assertFalse(getFieldMatcher(MatchTypeEnum.STRING).matches(new DateType("1965-08-09"), new DateType("1965-09-08"), myMdmMatcherJson));
}
@ -112,52 +146,55 @@ public class StringMatcherR4Test extends BaseMatcherR4Test {
Enumeration<Enumerations.AdministrativeGender> female = new Enumeration<Enumerations.AdministrativeGender>(new Enumerations.AdministrativeGenderEnumFactory());
female.setValue(Enumerations.AdministrativeGender.FEMALE);
assertTrue(MdmMatcherEnum.STRING.match(ourFhirContext, male, male, true, null));
myMdmMatcherJson.setExact(true);
assertFalse(MdmMatcherEnum.STRING.match(ourFhirContext, male, female, true, null));
assertTrue(getFieldMatcher(MatchTypeEnum.STRING).matches(male, male, myMdmMatcherJson));
assertFalse(getFieldMatcher(MatchTypeEnum.STRING).matches(male, female, myMdmMatcherJson));
}
@Test
public void testSoundex() {
assertTrue(match(MdmMatcherEnum.SOUNDEX, "Gail", "Gale"));
assertTrue(match(MdmMatcherEnum.SOUNDEX, "John", "Jon"));
assertTrue(match(MdmMatcherEnum.SOUNDEX, "Thom", "Tom"));
assertTrue(match(MatchTypeEnum.SOUNDEX, "Gail", "Gale"));
assertTrue(match(MatchTypeEnum.SOUNDEX, "John", "Jon"));
assertTrue(match(MatchTypeEnum.SOUNDEX, "Thom", "Tom"));
assertFalse(match(MdmMatcherEnum.SOUNDEX, "Fred", "Frank"));
assertFalse(match(MdmMatcherEnum.SOUNDEX, "Thomas", "Tom"));
assertFalse(match(MatchTypeEnum.SOUNDEX, "Fred", "Frank"));
assertFalse(match(MatchTypeEnum.SOUNDEX, "Thomas", "Tom"));
}
@Test
public void testCaverphone1() {
assertTrue(match(MdmMatcherEnum.CAVERPHONE1, "Gail", "Gael"));
assertTrue(match(MdmMatcherEnum.CAVERPHONE1, "John", "Jon"));
assertTrue(match(MatchTypeEnum.CAVERPHONE1, "Gail", "Gael"));
assertTrue(match(MatchTypeEnum.CAVERPHONE1, "John", "Jon"));
assertFalse(match(MdmMatcherEnum.CAVERPHONE1, "Gail", "Gale"));
assertFalse(match(MdmMatcherEnum.CAVERPHONE1, "Fred", "Frank"));
assertFalse(match(MdmMatcherEnum.CAVERPHONE1, "Thomas", "Tom"));
assertFalse(match(MatchTypeEnum.CAVERPHONE1, "Gail", "Gale"));
assertFalse(match(MatchTypeEnum.CAVERPHONE1, "Fred", "Frank"));
assertFalse(match(MatchTypeEnum.CAVERPHONE1, "Thomas", "Tom"));
}
@Test
public void testCaverphone2() {
assertTrue(match(MdmMatcherEnum.CAVERPHONE2, "Gail", "Gael"));
assertTrue(match(MdmMatcherEnum.CAVERPHONE2, "John", "Jon"));
assertTrue(match(MdmMatcherEnum.CAVERPHONE2, "Gail", "Gale"));
assertTrue(match(MatchTypeEnum.CAVERPHONE2, "Gail", "Gael"));
assertTrue(match(MatchTypeEnum.CAVERPHONE2, "John", "Jon"));
assertTrue(match(MatchTypeEnum.CAVERPHONE2, "Gail", "Gale"));
assertFalse(match(MdmMatcherEnum.CAVERPHONE2, "Fred", "Frank"));
assertFalse(match(MdmMatcherEnum.CAVERPHONE2, "Thomas", "Tom"));
assertFalse(match(MatchTypeEnum.CAVERPHONE2, "Fred", "Frank"));
assertFalse(match(MatchTypeEnum.CAVERPHONE2, "Thomas", "Tom"));
}
@Test
public void testNormalizeSubstring() {
assertTrue(match(MdmMatcherEnum.SUBSTRING, "BILLY", "Bill"));
assertTrue(match(MdmMatcherEnum.SUBSTRING, "Bill", "Billy"));
assertTrue(match(MdmMatcherEnum.SUBSTRING, "FRED", "Frederik"));
assertTrue(match(MatchTypeEnum.SUBSTRING, "BILLY", "Bill"));
assertTrue(match(MatchTypeEnum.SUBSTRING, "Bill", "Billy"));
assertTrue(match(MatchTypeEnum.SUBSTRING, "FRED", "Frederik"));
assertFalse(match(MdmMatcherEnum.SUBSTRING, "Fred", "Friederik"));
assertFalse(match(MatchTypeEnum.SUBSTRING, "Fred", "Friederik"));
}
private boolean match(MdmMatcherEnum theMatcher, String theLeft, String theRight) {
return theMatcher.match(ourFhirContext, new StringType(theLeft), new StringType(theRight), false, null);
private boolean match(MatchTypeEnum theMatcher, String theLeft, String theRight) {
return getFieldMatcher(theMatcher)
.matches(new StringType(theLeft), new StringType(theRight), myMdmMatcherJson);
}
}

View File

@ -29,6 +29,7 @@ public abstract class BaseMdmRulesR4Test extends BaseR4Test {
@BeforeEach
public void before() {
super.before();
myMdmRulesJson = new MdmRulesJson();
ArrayList<String> myLegalMdmTypes = new ArrayList<>();

View File

@ -6,7 +6,7 @@ import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
import ca.uhn.fhir.mdm.rules.matcher.MdmMatcherEnum;
import ca.uhn.fhir.mdm.rules.matcher.models.MatchTypeEnum;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.BeforeAll;
@ -20,7 +20,7 @@ import static org.mockito.Mockito.when;
public class CustomResourceMatcherR4Test extends BaseR4Test {
public static final String FIELD_EXACT_MATCH_NAME = MdmMatcherEnum.NAME_ANY_ORDER.name();
public static final String FIELD_EXACT_MATCH_NAME = MatchTypeEnum.NAME_ANY_ORDER.name();
private static Patient ourJohnHenry;
private static Patient ourJohnHENRY;
private static Patient ourJaneHenry;
@ -32,6 +32,7 @@ public class CustomResourceMatcherR4Test extends BaseR4Test {
@BeforeEach
public void before() {
super.before();
when(mySearchParamRetriever.getActiveSearchParam("Patient", "identifier")).thenReturn(mock(RuntimeSearchParam.class));
when(mySearchParamRetriever.getActiveSearchParam("Practitioner", "identifier")).thenReturn(mock(RuntimeSearchParam.class));
when(mySearchParamRetriever.getActiveSearchParam("Medication", "identifier")).thenReturn(mock(RuntimeSearchParam.class));
@ -40,7 +41,8 @@ public class CustomResourceMatcherR4Test extends BaseR4Test {
@Test
public void testExactNameAnyOrder() {
MdmResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(MdmMatcherEnum.NAME_ANY_ORDER, true));
MdmResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(MatchTypeEnum.NAME_ANY_ORDER, true));
assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
@ -53,7 +55,8 @@ public class CustomResourceMatcherR4Test extends BaseR4Test {
@Test
public void testNormalizedNameAnyOrder() {
MdmResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(MdmMatcherEnum.NAME_ANY_ORDER, false));
MdmResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(MatchTypeEnum.NAME_ANY_ORDER, false));
assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
@ -66,7 +69,8 @@ public class CustomResourceMatcherR4Test extends BaseR4Test {
@Test
public void testExactNameFirstAndLast() {
MdmResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(MdmMatcherEnum.NAME_FIRST_AND_LAST, true));
MdmResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(MatchTypeEnum.NAME_FIRST_AND_LAST, true));
assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatchResult(MdmMatchResultEnum.MATCH, 1L, 1.0, false, false, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
@ -80,7 +84,8 @@ public class CustomResourceMatcherR4Test extends BaseR4Test {
@Test
public void testNormalizedNameFirstAndLast() {
MdmResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(MdmMatcherEnum.NAME_FIRST_AND_LAST, false));
MdmResourceMatcherSvc nameAnyOrderMatcher = buildMatcher(buildNameRules(MatchTypeEnum.NAME_FIRST_AND_LAST, false));
assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourJohnHenry));
assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJohn));
assertMatch(MdmMatchResultEnum.NO_MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourHenryJOHN));
@ -91,7 +96,7 @@ public class CustomResourceMatcherR4Test extends BaseR4Test {
assertMatch(MdmMatchResultEnum.MATCH, nameAnyOrderMatcher.match(ourJohnHenry, ourBillyJohnHenry));
}
private MdmRulesJson buildNameRules(MdmMatcherEnum theAlgorithm, boolean theExact) {
private MdmRulesJson buildNameRules(MatchTypeEnum theAlgorithm, boolean theExact) {
MdmMatcherJson matcherJson = new MdmMatcherJson().setAlgorithm(theAlgorithm).setExact(theExact);
MdmFieldMatchJson nameAnyOrderFieldMatch = new MdmFieldMatchJson()
.setName(FIELD_EXACT_MATCH_NAME)

View File

@ -6,7 +6,7 @@ import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
import ca.uhn.fhir.mdm.rules.matcher.MdmMatcherEnum;
import ca.uhn.fhir.mdm.rules.matcher.models.MatchTypeEnum;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.BeforeEach;
@ -46,10 +46,9 @@ public class FhirPathResourceMatcherR4Test extends BaseMdmRulesR4Test {
}
}
@Test
public void testFhirPathOrderedMatches() {
MdmResourceMatcherSvc matcherSvc = buildMatcher(buildOrderedGivenNameRules(MdmMatcherEnum.STRING));
MdmResourceMatcherSvc matcherSvc = buildMatcher(buildOrderedGivenNameRules(MatchTypeEnum.STRING));
myLeft = new Patient();
HumanName name = myLeft.addName();
@ -63,6 +62,7 @@ public class FhirPathResourceMatcherR4Test extends BaseMdmRulesR4Test {
name2.addGiven("Gary");
myRight.setId("Patient/2");
// test
MdmMatchOutcome result = matcherSvc.match(myLeft, myRight);
assertMatchResult(MdmMatchResultEnum.NO_MATCH, 0L, 0.0, false, false, result);
@ -85,12 +85,12 @@ public class FhirPathResourceMatcherR4Test extends BaseMdmRulesR4Test {
@Test
public void testStringMatchResult() {
MdmResourceMatcherSvc matcherSvc = buildMatcher(buildOrderedGivenNameRules(MdmMatcherEnum.STRING));
MdmResourceMatcherSvc matcherSvc = buildMatcher(buildOrderedGivenNameRules(MatchTypeEnum.STRING));
MdmMatchOutcome result = matcherSvc.match(myLeft, myRight);
assertMatchResult(MdmMatchResultEnum.NO_MATCH, 0L, 0.0, false, false, result);
}
protected MdmRulesJson buildOrderedGivenNameRules(MdmMatcherEnum theMatcherEnum) {
protected MdmRulesJson buildOrderedGivenNameRules(MatchTypeEnum theMatcherEnum) {
MdmFieldMatchJson firstGivenNameMatchField = new MdmFieldMatchJson()
.setName(PATIENT_GIVEN_FIRST)
.setResourceType("Patient")

View File

@ -3,10 +3,8 @@ package ca.uhn.fhir.mdm.rules.svc;
import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
import ca.uhn.fhir.mdm.rules.json.MdmSimilarityJson;
import ca.uhn.fhir.mdm.rules.matcher.MdmMatcherEnum;
import ca.uhn.fhir.mdm.rules.similarity.MdmSimilarityEnum;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.mdm.rules.matcher.fieldmatchers.EmptyFieldMatcher;
import ca.uhn.fhir.mdm.rules.matcher.models.MatchTypeEnum;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.BeforeEach;
@ -14,12 +12,12 @@ import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.StringStartsWith.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
public class MdmResourceFieldMatcherR4Test extends BaseMdmRulesR4Test {
protected MdmResourceFieldMatcher myComparator;
@ -30,7 +28,13 @@ public class MdmResourceFieldMatcherR4Test extends BaseMdmRulesR4Test {
@BeforeEach
public void before() {
super.before();
myComparator = new MdmResourceFieldMatcher(ourFhirContext, myGivenNameMatchField, myMdmRulesJson);
myComparator = new MdmResourceFieldMatcher(
ourFhirContext,
myIMatcherFactory,
myGivenNameMatchField,
myMdmRulesJson
);
myJohn = buildJohn();
myJohny = buildJohny();
}
@ -44,8 +48,13 @@ public class MdmResourceFieldMatcherR4Test extends BaseMdmRulesR4Test {
.setName("empty-given")
.setResourceType("Patient")
.setResourcePath("name.given")
.setMatcher(new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.EMPTY_FIELD));
myComparator = new MdmResourceFieldMatcher(ourFhirContext, myGivenNameMatchField, myMdmRulesJson);
.setMatcher(new MdmMatcherJson().setAlgorithm(MatchTypeEnum.EMPTY_FIELD));
myComparator = new MdmResourceFieldMatcher(
ourFhirContext,
myIMatcherFactory,
myGivenNameMatchField,
myMdmRulesJson
);
assertFalse(myComparator.match(myJohn, myJohny).match);
@ -90,6 +99,9 @@ public class MdmResourceFieldMatcherR4Test extends BaseMdmRulesR4Test {
}
}
// TODO - what is this supposed to test?
// it relies on matcher being null (is this a reasonable assumption?)
// and falls through to similarity check
@Test
public void testMatch() {
assertTrue(myComparator.match(myJohn, myJohny).match);

View File

@ -36,7 +36,6 @@ public class MdmResourceMatcherSvcR4Test extends BaseMdmRulesR4Test {
public void testCompareFirstNameMatch() {
MdmMatchOutcome result = myMdmResourceMatcherSvc.match(myJohn, myJohny);
assertMatchResult(MdmMatchResultEnum.POSSIBLE_MATCH, 1L, 0.816, false, false, result);
}
@Test

View File

@ -6,7 +6,7 @@ import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.rules.json.MdmFieldMatchJson;
import ca.uhn.fhir.mdm.rules.json.MdmMatcherJson;
import ca.uhn.fhir.mdm.rules.json.MdmRulesJson;
import ca.uhn.fhir.mdm.rules.matcher.MdmMatcherEnum;
import ca.uhn.fhir.mdm.rules.matcher.models.MatchTypeEnum;
import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.BeforeEach;
@ -55,19 +55,21 @@ public class ResourceMatcherR4Test extends BaseMdmRulesR4Test {
@Test
public void testMetaphoneMatchResult() {
MdmResourceMatcherSvc matcherSvc = buildMatcher(buildNamePhoneRules(MdmMatcherEnum.METAPHONE));
MdmResourceMatcherSvc matcherSvc = buildMatcher(buildNamePhoneRules(MatchTypeEnum.METAPHONE));
MdmMatchOutcome result = matcherSvc.match(myLeft, myRight);
assertMatchResult(MdmMatchResultEnum.MATCH, 7L, 3.0, false, false, result);
}
@Test
public void testStringMatchResult() {
MdmResourceMatcherSvc matcherSvc = buildMatcher(buildNamePhoneRules(MdmMatcherEnum.STRING));
MdmResourceMatcherSvc matcherSvc = buildMatcher(buildNamePhoneRules(MatchTypeEnum.STRING));
MdmMatchOutcome result = matcherSvc.match(myLeft, myRight);
assertMatchResult(MdmMatchResultEnum.NO_MATCH, 5L, 2.0, false, false, result);
}
protected MdmRulesJson buildNamePhoneRules(MdmMatcherEnum theMatcherEnum) {
protected MdmRulesJson buildNamePhoneRules(MatchTypeEnum theMatcherEnum) {
MdmFieldMatchJson lastNameMatchField = new MdmFieldMatchJson()
.setName(PATIENT_FAMILY)
.setResourceType("Patient")
@ -84,7 +86,7 @@ public class ResourceMatcherR4Test extends BaseMdmRulesR4Test {
.setName(PATIENT_PHONE)
.setResourceType("Patient")
.setResourcePath("telecom.value")
.setMatcher(new MdmMatcherJson().setAlgorithm(MdmMatcherEnum.STRING));
.setMatcher(new MdmMatcherJson().setAlgorithm(MatchTypeEnum.STRING));
MdmRulesJson retval = new MdmRulesJson();
retval.setVersion("test version");

View File

@ -55,11 +55,6 @@
<artifactId>junit-jupiter-params</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>