diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5141-updating-resources-will-link-to-existing-resources.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5141-updating-resources-will-link-to-existing-resources.yaml new file mode 100644 index 00000000000..0f6545d688e --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5141-updating-resources-will-link-to-existing-resources.yaml @@ -0,0 +1,10 @@ +--- +issue: 5141 +type: add +title: "Previously, updating an existing resource (resource A) to match a resource that + it didn't match to before (resource B) would update only the already + existing links on resource A. + This behaviour has been changed. Now such an update will also add additional links, + if necessary, from resource A to resource B, including adding a POSSIBLE_DUPLICATE + between golden resource A and golden resource B. +" diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/broker/MdmMessageHandler.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/broker/MdmMessageHandler.java index 2a266400ff5..ae7a9b9a50e 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/broker/MdmMessageHandler.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/broker/MdmMessageHandler.java @@ -81,10 +81,10 @@ public class MdmMessageHandler implements MessageHandler { ResourceModifiedMessage msg = ((ResourceModifiedJsonMessage) theMessage).getPayload(); try { - IBaseResource sourceResource = msg.getNewPayload(myFhirContext); - if (myMdmResourceFilteringSvc.shouldBeProcessed((IAnyResource) sourceResource)) { + boolean toProcess = myMdmResourceFilteringSvc.shouldBeProcessed((IAnyResource) sourceResource); + if (toProcess) { matchMdmAndUpdateLinks(sourceResource, msg); } } catch (TooManyCandidatesException e) { diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/models/FindGoldenResourceCandidatesParams.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/models/FindGoldenResourceCandidatesParams.java new file mode 100644 index 00000000000..6c484c26cdb --- /dev/null +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/models/FindGoldenResourceCandidatesParams.java @@ -0,0 +1,30 @@ +package ca.uhn.fhir.jpa.mdm.models; + +import ca.uhn.fhir.mdm.model.MdmTransactionContext; +import org.hl7.fhir.instance.model.api.IAnyResource; + +public class FindGoldenResourceCandidatesParams { + + /** + * The resource to find matches for + */ + private final IAnyResource myResource; + + /** + * The mdm context + */ + private final MdmTransactionContext myContext; + + public FindGoldenResourceCandidatesParams(IAnyResource theResource, MdmTransactionContext theContext) { + myResource = theResource; + myContext = theContext; + } + + public IAnyResource getResource() { + return myResource; + } + + public MdmTransactionContext getContext() { + return myContext; + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java index 34b562a5d3d..8dd8abca747 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java @@ -19,6 +19,7 @@ */ package ca.uhn.fhir.jpa.mdm.svc; +import ca.uhn.fhir.jpa.mdm.models.FindGoldenResourceCandidatesParams; import ca.uhn.fhir.jpa.mdm.svc.candidate.CandidateList; import ca.uhn.fhir.jpa.mdm.svc.candidate.CandidateStrategyEnum; import ca.uhn.fhir.jpa.mdm.svc.candidate.MatchedGoldenResourceCandidate; @@ -93,7 +94,7 @@ public class MdmMatchLinkSvc { // we require a candidatestrategy, but it doesn't matter // because empty lists are effectively no matches // (and so the candidate strategy doesn't matter) - CandidateList candidateList = new CandidateList(CandidateStrategyEnum.LINK); + CandidateList candidateList = new CandidateList(CandidateStrategyEnum.ANY); /* * If a resource is blocked, we will not conduct @@ -103,7 +104,9 @@ public class MdmMatchLinkSvc { boolean isResourceBlocked = myBlockRuleEvaluationSvc.isMdmMatchingBlocked(theResource); if (!isResourceBlocked) { - candidateList = myMdmGoldenResourceFindingSvc.findGoldenResourceCandidates(theResource); + FindGoldenResourceCandidatesParams params = + new FindGoldenResourceCandidatesParams(theResource, theMdmTransactionContext); + candidateList = myMdmGoldenResourceFindingSvc.findGoldenResourceCandidates(params); } if (isResourceBlocked || candidateList.isEmpty()) { diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/BaseCandidateFinder.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/BaseCandidateFinder.java index be27dd2f758..8c608c983e2 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/BaseCandidateFinder.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/BaseCandidateFinder.java @@ -35,7 +35,7 @@ public abstract class BaseCandidateFinder { CandidateList findCandidates(IAnyResource theTarget) { CandidateList candidateList = new CandidateList(getStrategy()); - candidateList.addAll(findMatchGoldenResourceCandidates(theTarget)); + candidateList.addAll(getStrategy(), findMatchGoldenResourceCandidates(theTarget)); return candidateList; } diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateList.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateList.java index 5ee2d019531..cac9ad469ee 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateList.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateList.java @@ -19,17 +19,29 @@ */ package ca.uhn.fhir.jpa.mdm.svc.candidate; +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; + import java.util.ArrayList; -import java.util.Collections; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import java.util.stream.Stream; public class CandidateList { private final CandidateStrategyEnum myStrategy; - private final List myList = new ArrayList<>(); + + // no multimap - ordering matters + private final Map> myStrategyToCandidateList = + new HashMap<>(); public CandidateList(CandidateStrategyEnum theStrategy) { myStrategy = theStrategy; + myStrategyToCandidateList.put(CandidateStrategyEnum.EID, new ArrayList<>()); + myStrategyToCandidateList.put(CandidateStrategyEnum.LINK, new ArrayList<>()); + myStrategyToCandidateList.put(CandidateStrategyEnum.SCORE, new ArrayList<>()); } public CandidateStrategyEnum getStrategy() { @@ -37,32 +49,75 @@ public class CandidateList { } public boolean isEmpty() { - return myList.isEmpty(); + return size() == 0; } - public void addAll(List theList) { - myList.addAll(theList); + public void addAll(CandidateStrategyEnum theStrategy, List theList) { + switch (theStrategy) { + case EID: + case LINK: + case SCORE: + myStrategyToCandidateList.get(theStrategy).addAll(theList); + break; + default: + throw new InternalErrorException( + Msg.code(2424) + " Existing resources cannot be added for strategy " + theStrategy.name()); + } } public MatchedGoldenResourceCandidate getOnlyMatch() { - assert myList.size() == 1; - return myList.get(0); + assert size() == 1; + return getCandidates().get(0); } public boolean exactlyOneMatch() { - return myList.size() == 1; + return size() == 1; } + /** + * Returns a stream of all types. + * If multiple streams are present, + * they will be ordered by strategy type + */ public Stream stream() { - return myList.stream(); + return Stream.concat( + myStrategyToCandidateList.get(CandidateStrategyEnum.EID).stream(), + Stream.concat( + myStrategyToCandidateList.get(CandidateStrategyEnum.LINK).stream(), + myStrategyToCandidateList.get(CandidateStrategyEnum.SCORE).stream())); + } + + public Stream stream(CandidateStrategyEnum theStrategy) { + return myStrategyToCandidateList.get(theStrategy).stream(); } public List getCandidates() { - return Collections.unmodifiableList(myList); + switch (myStrategy) { + case LINK: + case EID: + case SCORE: + return new ArrayList<>(myStrategyToCandidateList.get(myStrategy)); + default: + return Stream.of( + myStrategyToCandidateList.get(CandidateStrategyEnum.EID), + myStrategyToCandidateList.get(CandidateStrategyEnum.LINK), + myStrategyToCandidateList.get(CandidateStrategyEnum.SCORE)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } } public MatchedGoldenResourceCandidate getFirstMatch() { - return myList.get(0); + assert size() > 0; + + switch (myStrategy) { + case EID: + case LINK: + case SCORE: + return myStrategyToCandidateList.get(myStrategy).get(0); + default: + return getCandidates().get(0); + } } public boolean isEidMatch() { @@ -70,6 +125,19 @@ public class CandidateList { } public int size() { - return myList.size(); + switch (myStrategy) { + case EID: + case LINK: + case SCORE: + return myStrategyToCandidateList.get(myStrategy).size(); + default: + return myStrategyToCandidateList.get(CandidateStrategyEnum.EID).size() + + myStrategyToCandidateList + .get(CandidateStrategyEnum.LINK) + .size() + + myStrategyToCandidateList + .get(CandidateStrategyEnum.SCORE) + .size(); + } } } diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateStrategyEnum.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateStrategyEnum.java index 6b83b59a418..35887435491 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateStrategyEnum.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateStrategyEnum.java @@ -25,7 +25,11 @@ public enum CandidateStrategyEnum { /** Find Golden Resource candidates based on a link already existing for the source resource */ LINK, /** Find Golden Resource candidates based on other sources that match the incoming source using the MDM Matching rules */ - SCORE; + SCORE, + /** + * Find golden resource candidates that are EID, LINK, or SCORE. + */ + ANY; public boolean isEidMatch() { return this == EID; diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmGoldenResourceFindingSvc.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmGoldenResourceFindingSvc.java index b72d3addea3..073ca1ced2b 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmGoldenResourceFindingSvc.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmGoldenResourceFindingSvc.java @@ -19,8 +19,10 @@ */ package ca.uhn.fhir.jpa.mdm.svc.candidate; +import ca.uhn.fhir.jpa.mdm.models.FindGoldenResourceCandidatesParams; import ca.uhn.fhir.jpa.mdm.svc.MdmResourceDaoSvc; import ca.uhn.fhir.mdm.log.Logs; +import ca.uhn.fhir.mdm.model.MdmTransactionContext; import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -56,23 +58,47 @@ public class MdmGoldenResourceFindingSvc { * 4. If none are found, attempt to find Golden Resources that are linked to sources that are similar to our incoming resource based on the MDM rules and * field matchers. * - * @param theResource the {@link IBaseResource} we are attempting to find matching candidate Golden Resources for. + * @param theParams Params hold the {@link IBaseResource} for which we are attempting to find matching candidate Golden Resources, + * as well as the mdm context. * @return A list of {@link MatchedGoldenResourceCandidate} indicating all potential Golden Resource matches. */ - public CandidateList findGoldenResourceCandidates(IAnyResource theResource) { - CandidateList matchedGoldenResourceCandidates = myFindCandidateByEidSvc.findCandidates(theResource); + public CandidateList findGoldenResourceCandidates(FindGoldenResourceCandidatesParams theParams) { + IAnyResource resource = theParams.getResource(); - if (matchedGoldenResourceCandidates.isEmpty()) { - matchedGoldenResourceCandidates = myFindCandidateByLinkSvc.findCandidates(theResource); + CandidateList eidGoldenResources = myFindCandidateByEidSvc.findCandidates(resource); + + // if we have matches from eid, we'll return only these + if (!eidGoldenResources.isEmpty()) { + return eidGoldenResources; } - if (matchedGoldenResourceCandidates.isEmpty()) { - // OK, so we have not found any links in the MdmLink table with us as a source. Next, let's find - // possible Golden Resources matches by following MDM rules. - matchedGoldenResourceCandidates = myFindCandidateByExampleSvc.findCandidates(theResource); + boolean isUpdate = + theParams.getContext().getRestOperation() == MdmTransactionContext.OperationType.UPDATE_RESOURCE; + + // find MdmLinks that have theResource as the source + // (these are current golden resources matching this resource) + CandidateList linkGoldenResources = myFindCandidateByLinkSvc.findCandidates(resource); + + if (!linkGoldenResources.isEmpty() && !isUpdate) { + return linkGoldenResources; } - return matchedGoldenResourceCandidates; + // if we're updating, we might have existing resources that could *also* match + // find other golden resources that could be matching to this resource + // (we only need to do this for updates because otherwise they would already have matching resources + CandidateList anyGoldenResources = myFindCandidateByExampleSvc.findCandidates(resource); + + if (linkGoldenResources.isEmpty()) { + // only other resources are available - we'll return this + return anyGoldenResources; + } + + // else, we will combine the lists + CandidateList matches = new CandidateList(CandidateStrategyEnum.ANY); + matches.addAll(CandidateStrategyEnum.LINK, linkGoldenResources.getCandidates()); + matches.addAll(CandidateStrategyEnum.SCORE, anyGoldenResources.getCandidates()); + + return matches; } public IAnyResource getGoldenResourceFromMatchedGoldenResourceCandidate( diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/MdmLinkHelper.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/MdmLinkHelper.java index 7d1c0bfb885..544984ab0c6 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/MdmLinkHelper.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/MdmLinkHelper.java @@ -208,7 +208,7 @@ public class MdmLinkHelper { } } - assertTrue(foundLink, String.format("State: %s - not found", stateExpression)); + assertTrue(foundLink, String.format("State: %s - not found", stateExpression.getLinkExpression())); } } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmControllerSvcImplTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmControllerSvcImplTest.java index 25f39dde818..845fc26d8ca 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmControllerSvcImplTest.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmControllerSvcImplTest.java @@ -7,7 +7,6 @@ import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.jpa.interceptor.UserRequestRetryVersionConflictsInterceptor; import ca.uhn.fhir.jpa.mdm.provider.BaseLinkR4Test; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; -import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.jpa.test.Batch2JobHelper; import ca.uhn.fhir.mdm.api.IMdmControllerSvc; import ca.uhn.fhir.mdm.api.MdmLinkJson; @@ -18,6 +17,7 @@ import ca.uhn.fhir.mdm.api.paging.MdmPageRequest; import ca.uhn.fhir.mdm.batch2.clear.MdmClearStep; import ca.uhn.fhir.mdm.model.MdmTransactionContext; import ca.uhn.fhir.mdm.rules.config.MdmSettings; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java index d436173b0a0..599db86336a 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java @@ -3,7 +3,10 @@ package ca.uhn.fhir.jpa.mdm.svc; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.entity.MdmLink; import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; +import ca.uhn.fhir.jpa.mdm.config.BaseTestMdmConfig; import ca.uhn.fhir.jpa.mdm.config.BlockListConfig; +import ca.uhn.fhir.jpa.mdm.helper.MdmLinkHelper; +import ca.uhn.fhir.jpa.mdm.helper.testmodels.MDMState; import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.mdm.api.IMdmLink; @@ -31,14 +34,18 @@ import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -61,8 +68,17 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.when; +import static org.slf4j.LoggerFactory.getLogger; +/** + * These tests use the rules defined in mdm-rules.json + * See {@link BaseTestMdmConfig} + */ public class MdmMatchLinkSvcTest { + + private static final Logger ourLog = getLogger(MdmMatchLinkSvcTest.class); + + @Nested public class NoBlockLinkTest extends BaseMdmR4Test { @Autowired @@ -74,6 +90,9 @@ public class MdmMatchLinkSvcTest { @Autowired private IMdmLinkUpdaterSvc myMdmLinkUpdaterSvc; + @Autowired + private MdmLinkHelper myLinkHelper; + @Test public void testAddPatientLinksToNewGoldenResourceIfNoneFound() { createPatientAndUpdateLinks(buildJanePatient()); @@ -98,6 +117,64 @@ public class MdmMatchLinkSvcTest { assertLinksMatchVector((Long) null); } + @Test + @RepeatedTest(20) + public void testUpdatingAResourceToMatchACurrentlyUnmatchedResource_resultsInUpdatedLinksForBoth() { + // setup + MDMState state = new MDMState<>(); + String startingState = """ + GP1, AUTO, MATCH, P1 + GP2, AUTO, MATCH, P2 + """; + + Map idToResource = new HashMap<>(); + + // we're creating our patients manually, + // because we're testing mdm rules and how candidates are found + // so the patient info matters + Patient jane = buildJanePatient(); + Long id; + { + Patient p = createPatient(jane); + idToResource.put("P1", p); + Long patientId = p.getIdElement().getIdPartAsLong(); + state.addPID(patientId.toString(), JpaPid.fromIdAndResourceType(patientId, "Patient")); + } + { + Patient yui = buildJanePatient(); + yui.setName(new ArrayList<>()); + yui.addName() + .addGiven("Yui") + .setFamily("Hirasawa"); + Patient retVal = createPatient(yui); + id = retVal.getIdElement().getIdPartAsLong(); + idToResource.put("P2", retVal); + state.addPID(String.valueOf(id), JpaPid.fromIdAndResourceType(id, "Patient")); + } + + // initialize our links + state.setInputState(startingState); + state.setParameterToValue(idToResource); + myLinkHelper.setup(state); + + // test + Patient toUpdate = buildJanePatient(); + toUpdate.setId("Patient/" + id.longValue()); + updatePatientAndUpdateLinks(toUpdate); + + // verify + String endState = """ + GP1, AUTO, MATCH, P1 + GP2, AUTO, POSSIBLE_MATCH, P2 + GP1, AUTO, POSSIBLE_MATCH, P2 + GP2, AUTO, POSSIBLE_DUPLICATE, GP1 + """; + state.setParameterToValue(idToResource); + state.setOutputState(endState); + myLinkHelper.validateResults(state); + myLinkHelper.logMdmLinks(); + } + @Test public void testAddPatientLinksToNewlyCreatedResourceIfNoMatch() { Patient patient1 = createPatientAndUpdateLinks(buildJanePatient()); diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceDaoSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceDaoSvcTest.java index 44496f650d7..89b39ce3fb8 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceDaoSvcTest.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmResourceDaoSvcTest.java @@ -4,8 +4,8 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; import ca.uhn.fhir.jpa.model.config.PartitionSettings; -import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.mdm.util.MdmResourceUtil; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.AfterEach; diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateListTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateListTest.java new file mode 100644 index 00000000000..54f91a95f98 --- /dev/null +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/candidate/CandidateListTest.java @@ -0,0 +1,121 @@ +package ca.uhn.fhir.jpa.mdm.svc.candidate; + +import ca.uhn.fhir.mdm.api.MdmMatchOutcome; +import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +public class CandidateListTest { + + private List getCandidatesList(int theSize) { + List candidatesToAdd = new ArrayList<>(); + + for (int i = 0; i < theSize; i++) { + MatchedGoldenResourceCandidate candidate = new MatchedGoldenResourceCandidate( + mock(IResourcePersistentId.class), + MdmMatchOutcome.POSSIBLE_MATCH + ); + candidatesToAdd.add(candidate); + } + + return candidatesToAdd; + } + + @ParameterizedTest + @EnumSource(CandidateStrategyEnum.class) + public void addAll_withVariousStrategies_behaviourTest(CandidateStrategyEnum theStrategyEnum) { + // setup + int total = 3; + List candidatesToAdd = getCandidatesList(total); + + // test + CandidateList list = new CandidateList(theStrategyEnum); + + // verify + if (theStrategyEnum == CandidateStrategyEnum.ANY) { + assertThrows(InternalErrorException.class, () -> { + list.addAll(theStrategyEnum, candidatesToAdd); + }); + } else { + list.addAll(theStrategyEnum, candidatesToAdd); + assertEquals(total, list.size()); + } + } + + @ParameterizedTest + @EnumSource(CandidateStrategyEnum.class) + public void stream_forVariousStrategies_returnsJointStream(CandidateStrategyEnum theStrategy) { + // setup + int size = 3; + CandidateList candidateList = new CandidateList(theStrategy); + + // we need some values first + size = populateCandidateList(theStrategy, size, candidateList); + + // test + assertEquals(size, candidateList.stream().count()); + } + + private int populateCandidateList(CandidateStrategyEnum theStrategy, int theSize, CandidateList theCandidateList) { + if (theStrategy == CandidateStrategyEnum.ANY) { + int realTotal = 0; + for (CandidateStrategyEnum strat : CandidateStrategyEnum.values()) { + if (strat == theStrategy) { + continue; + } + + theCandidateList.addAll(strat, getCandidatesList(theSize)); + realTotal += theSize; + } + theSize = realTotal; + } else { + theCandidateList.addAll(theStrategy, getCandidatesList(theSize)); + } + return theSize; + } + + @ParameterizedTest + @EnumSource(CandidateStrategyEnum.class) + public void singleElement_CandidateList_Tests(CandidateStrategyEnum theStrategy) { + // setup + CandidateList candidate = new CandidateList(theStrategy); + + if (theStrategy == CandidateStrategyEnum.ANY) { + candidate.addAll(CandidateStrategyEnum.LINK, getCandidatesList(1)); + } else { + candidate.addAll(theStrategy, getCandidatesList(1)); + } + + // tests + assertFalse(candidate.isEmpty()); + assertTrue(candidate.exactlyOneMatch()); + assertEquals(1, candidate.size()); + assertNotNull(candidate.getFirstMatch()); + assertNotNull(candidate.getOnlyMatch()); + } + + @ParameterizedTest + @EnumSource(CandidateStrategyEnum.class) + public void getCandidates_variousStrategies_returnsExpectedResults(CandidateStrategyEnum theStrategy) { + // setup + CandidateList candidateList = new CandidateList(theStrategy); + + int size = populateCandidateList(theStrategy, 10, candidateList); + + // tests + assertEquals(size, candidateList.size()); + List candidates = candidateList.getCandidates(); + assertEquals(size, candidates.size()); + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmGoldenResourceFindingSvcIT.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmGoldenResourceFindingSvcIT.java index 5b4cf0180fe..e4d049325dd 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmGoldenResourceFindingSvcIT.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/candidate/MdmGoldenResourceFindingSvcIT.java @@ -4,9 +4,12 @@ import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.entity.MdmLink; import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; +import ca.uhn.fhir.jpa.mdm.models.FindGoldenResourceCandidatesParams; import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; +import ca.uhn.fhir.mdm.model.MdmTransactionContext; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -49,7 +52,8 @@ class MdmGoldenResourceFindingSvcIT extends BaseMdmR4Test { myMdmLinkDaoSvc.save(link); // the NO_MATCH golden resource should not be a candidate - CandidateList candidateList = myMdmGoldenResourceFindingSvc.findGoldenResourceCandidates(jane); + CandidateList candidateList = myMdmGoldenResourceFindingSvc.findGoldenResourceCandidates( + createFindGoldenResourceCandidateParams(jane)); assertEquals(0, candidateList.size()); } @@ -131,7 +135,8 @@ class MdmGoldenResourceFindingSvcIT extends BaseMdmR4Test { } // test - CandidateList candidateList = myMdmGoldenResourceFindingSvc.findGoldenResourceCandidates(candidate); + CandidateList candidateList = myMdmGoldenResourceFindingSvc.findGoldenResourceCandidates( + createFindGoldenResourceCandidateParams(candidate)); // verify assertNotNull(candidateList); @@ -154,4 +159,12 @@ class MdmGoldenResourceFindingSvcIT extends BaseMdmR4Test { return (Patient) daoOutcome.getResource(); } + + private FindGoldenResourceCandidatesParams createFindGoldenResourceCandidateParams(IAnyResource theResource) { + FindGoldenResourceCandidatesParams params = new FindGoldenResourceCandidatesParams( + theResource, + new MdmTransactionContext() + ); + return params; + } } diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java index 30baac3eaf7..28a44f5baa7 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java @@ -19,6 +19,7 @@ */ package ca.uhn.fhir.jpa.test; +import ca.uhn.fhir.batch2.jobs.export.BulkDataExportProvider; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.interceptor.api.IInterceptorService; @@ -37,7 +38,6 @@ import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.binary.interceptor.BinaryStorageInterceptor; import ca.uhn.fhir.jpa.binary.provider.BinaryAccessProvider; import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportJobSchedulingHelper; -import ca.uhn.fhir.batch2.jobs.export.BulkDataExportProvider; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; import ca.uhn.fhir.jpa.dao.data.IMdmLinkJpaRepository; diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java index 14eabaccd4b..13c2078244c 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java @@ -260,7 +260,7 @@ public abstract class BaseJpaTest extends BaseTest { private IForcedIdDao myForcedIdDao; @Autowired private DaoRegistry myDaoRegistry; - private List myRegisteredInterceptors = new ArrayList<>(1); + private final List myRegisteredInterceptors = new ArrayList<>(1); @SuppressWarnings("BusyWait") public static void waitForSize(int theTarget, List theList) {