diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_3_0/4287-mdm-golden-resource-merge-fix.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_3_0/4287-mdm-golden-resource-merge-fix.yaml new file mode 100644 index 00000000000..39daff19694 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_3_0/4287-mdm-golden-resource-merge-fix.yaml @@ -0,0 +1,8 @@ +--- +issue: 4287 +type: fix +title: " + Fixed bug with merging 2 Golden Resources together. + When merging GP1 into GP2, the result was (incorrectly) + GP2 REDIRECT GP1 (instead of GP1 REDIRECT GP2). + " diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_3_0/4287-merge-golden-resource-dangling-link.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_3_0/4287-merge-golden-resource-dangling-link.yaml new file mode 100644 index 00000000000..11aaae899b0 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_3_0/4287-merge-golden-resource-dangling-link.yaml @@ -0,0 +1,10 @@ +--- +issue: 4287 +type: fix +title: " + Fixed bug with merging 2 Golden Resources together. + When merging a POSSIBLE_DUPLICATE of GP1 to GP2, + or GP2 to GP1, depending on the order, a dangling + self-referential link would sometimes remain + (eg, GP1 POSSIBLE_MATCH GP1). + " diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvc.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvc.java index be33dc91775..8831ce6e4a8 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvc.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/dao/MdmLinkDaoSvc.java @@ -93,7 +93,9 @@ public class MdmLinkDaoSvc { } @Nonnull - public IMdmLink getOrCreateMdmLinkByGoldenResourceAndSourceResource(IAnyResource theGoldenResource, IAnyResource theSourceResource) { + public IMdmLink getOrCreateMdmLinkByGoldenResourceAndSourceResource( + IAnyResource theGoldenResource, IAnyResource theSourceResource + ) { ResourcePersistentId goldenResourcePid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theGoldenResource); ResourcePersistentId sourceResourcePid = myIdHelperService.getPidOrNull(RequestPartitionId.allPartitions(), theSourceResource); Optional oExisting = getLinkByGoldenResourcePidAndSourceResourcePid(goldenResourcePid, sourceResourcePid); @@ -125,6 +127,7 @@ public class MdmLinkDaoSvc { * @param theSourceResourcePid The ResourcepersistenceId of the Source resource * @return The {@link IMdmLink} entity that matches these criteria if exists */ + @SuppressWarnings("unchecked") public Optional getLinkByGoldenResourcePidAndSourceResourcePid(ResourcePersistentId theGoldenResourcePid, ResourcePersistentId theSourceResourcePid) { if (theSourceResourcePid == null || theGoldenResourcePid == null) { return Optional.empty(); @@ -132,7 +135,10 @@ public class MdmLinkDaoSvc { IMdmLink link = myMdmLinkFactory.newMdmLink(); link.setSourcePersistenceId(theSourceResourcePid); link.setGoldenResourcePersistenceId(theGoldenResourcePid); + + //TODO - replace the use of example search Example example = Example.of(link); + return myMdmLinkDao.findOne(example); } diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/GoldenResourceMergerSvcImpl.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/GoldenResourceMergerSvcImpl.java index 49492546e9a..d849f9cbb78 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/GoldenResourceMergerSvcImpl.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/GoldenResourceMergerSvcImpl.java @@ -23,13 +23,13 @@ package ca.uhn.fhir.jpa.mdm.svc; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.svc.IIdHelperService; -import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; import ca.uhn.fhir.jpa.mdm.util.MdmPartitionHelper; import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc; import ca.uhn.fhir.mdm.api.IMdmLink; import ca.uhn.fhir.mdm.api.IMdmLinkSvc; import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; +import ca.uhn.fhir.mdm.api.MdmMatchOutcome; import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; import ca.uhn.fhir.mdm.log.Logs; import ca.uhn.fhir.mdm.model.MdmTransactionContext; @@ -95,7 +95,7 @@ public class GoldenResourceMergerSvcImpl implements IGoldenResourceMergerSvc { mergeGoldenResourceLinks(theFromGoldenResource, theToGoldenResource, theFromGoldenResource.getIdElement(), theMdmTransactionContext); //Create the new REDIRECT link - addMergeLink(theToGoldenResource, theFromGoldenResource, resourceType); + addMergeLink(theToGoldenResource, theFromGoldenResource, resourceType, theMdmTransactionContext); //Strip the golden resource tag from the now-deprecated resource. myMdmResourceDaoSvc.removeGoldenResourceTag(theFromGoldenResource, resourceType); @@ -111,25 +111,54 @@ public class GoldenResourceMergerSvcImpl implements IGoldenResourceMergerSvc { return theToGoldenResource; } - private void addMergeLink(IAnyResource theGoldenResource, IAnyResource theTargetResource, String theResourceType) { - IMdmLink mdmLink = myMdmLinkDaoSvc - .getOrCreateMdmLinkByGoldenResourceAndSourceResource(theGoldenResource, theTargetResource); + /** + * This connects 2 golden resources (GR and TR here) + * + * 1 Deletes any current links: TR, ?, ?, GR + * 2 Creates a new link: GR, MANUAL, REDIRECT, TR + * + * Before: + * TR -> GR + * + * After: + * GR -> TR + */ + private void addMergeLink( + IAnyResource theGoldenResource, + IAnyResource theTargetResource, + String theResourceType, + MdmTransactionContext theMdmTransactionContext + ) { + myMdmLinkSvc.deleteLink(theGoldenResource, theTargetResource, + theMdmTransactionContext); - mdmLink - .setMdmSourceType(theResourceType) - .setMatchResult(MdmMatchResultEnum.REDIRECT) - .setLinkSource(MdmLinkSourceEnum.MANUAL); - myMdmLinkDaoSvc.save(mdmLink); + myMdmLinkDaoSvc.createOrUpdateLinkEntity( + theTargetResource, // golden / LHS + theGoldenResource, // source / RHS + new MdmMatchOutcome(null, null).setMatchResultEnum(MdmMatchResultEnum.REDIRECT), + MdmLinkSourceEnum.MANUAL, + theMdmTransactionContext // mdm transaction context + ); } + private RequestPartitionId getPartitionIdForResource(IAnyResource theResource) { + RequestPartitionId partitionId = (RequestPartitionId) theResource.getUserData(Constants.RESOURCE_PARTITION_ID); + // TODO - this seems to be null on the put with + // client id (forced id). Is this a bug? + if (partitionId == null) { + partitionId = RequestPartitionId.allPartitions(); + } + return partitionId; + } /** * Helper method which performs merger of links between resources, and cleans up dangling links afterwards. *

* For each incomingLink, either ignore it, move it, or replace the original one * 1. If the link already exists on the TO resource, and it is an automatic link, ignore the link, and subsequently delete it. - * 2. If the link does not exist on the TO resource, redirect the link from the FROM resource to the TO resource - * 3. If an incoming link is MANUAL, and theres a matching link on the FROM resource which is AUTOMATIC, the manual link supercedes the automatic one. + * 2.a If the link does not exist on the TO resource, redirect the link from the FROM resource to the TO resource + * 2.b If the link does not exist on the TO resource, but is actually self-referential, it will just be removed + * 3. If an incoming link is MANUAL, and there's a matching link on the FROM resource which is AUTOMATIC, the manual link supercedes the automatic one. * 4. Manual link collisions cause invalid request exception. * * @param theFromResource @@ -137,12 +166,26 @@ public class GoldenResourceMergerSvcImpl implements IGoldenResourceMergerSvc { * @param theToResourcePid * @param theMdmTransactionContext */ - private void mergeGoldenResourceLinks(IAnyResource theFromResource, IAnyResource theToResource, IIdType theToResourcePid, MdmTransactionContext theMdmTransactionContext) { - List fromLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theFromResource); // fromLinks - links going to theFromResource - List toLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theToResource); // toLinks - links going to theToResource + private void mergeGoldenResourceLinks( + IAnyResource theFromResource, + IAnyResource theToResource, + IIdType theToResourcePid, + MdmTransactionContext theMdmTransactionContext + ) { + // fromLinks - links from theFromResource to any resource + List fromLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theFromResource); + // toLinks - links from theToResource to any resource + List toLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theToResource); List toDelete = new ArrayList<>(); - ResourcePersistentId goldenResourcePid = myIdHelperService.resolveResourcePersistentIds((RequestPartitionId) theToResource.getUserData(Constants.RESOURCE_PARTITION_ID), theToResource.getIdElement().getResourceType(), theToResource.getIdElement().getIdPart()); + ResourcePersistentId goldenResourcePid = myIdHelperService.resolveResourcePersistentIds( + getPartitionIdForResource(theToResource), + theToResource.getIdElement().getResourceType(), + theToResource.getIdElement().getIdPart() + ); + + // reassign links: + // to <- from for (IMdmLink fromLink : fromLinks) { Optional optionalToLink = findFirstLinkWithMatchingSource(toLinks, fromLink); if (optionalToLink.isPresent()) { @@ -162,17 +205,25 @@ public class GoldenResourceMergerSvcImpl implements IGoldenResourceMergerSvc { } } } else { - //1 + // 1 toDelete.add(fromLink); continue; } } - //2 The original TO links didn't contain this target, so move it over to the toGoldenResource - fromLink.setGoldenResourcePersistenceId(goldenResourcePid); - ourLog.trace("Saving link {}", fromLink); - myMdmLinkDaoSvc.save(fromLink); + + if (fromLink.getSourcePersistenceId().equals(goldenResourcePid)) { + // 2.b if the link is going to be self-referential we'll just delete it + // (ie, do not link back to itself) + myMdmLinkDaoSvc.deleteLink(fromLink); + } else { + // 2.a The original TO links didn't contain this target, so move it over to the toGoldenResource. + fromLink.setGoldenResourcePersistenceId(goldenResourcePid); + ourLog.trace("Saving link {}", fromLink); + myMdmLinkDaoSvc.save(fromLink); + } } - //1 Delete dangling links + + // 1 Delete dangling links toDelete.forEach(link -> myMdmLinkDaoSvc.deleteLink(link)); } 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 77d2f815304..da30f592880 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 @@ -1,26 +1,64 @@ package ca.uhn.fhir.jpa.mdm.helper; -import ca.uhn.fhir.jpa.dao.data.IMdmLinkJpaRepository; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; +import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.entity.MdmLink; +import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; +import ca.uhn.fhir.jpa.mdm.helper.testmodels.MDMLinkResults; +import ca.uhn.fhir.jpa.mdm.helper.testmodels.MDMState; +import ca.uhn.fhir.jpa.mdm.helper.testmodels.MdmTestLinkExpression; +import ca.uhn.fhir.jpa.partition.SystemRequestDetails; +import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; +import ca.uhn.fhir.mdm.api.MdmMatchOutcome; +import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; +import ca.uhn.fhir.mdm.dao.IMdmLinkDao; +import ca.uhn.fhir.mdm.model.MdmTransactionContext; +import ca.uhn.fhir.mdm.util.MdmResourceUtil; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Patient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; @Service public class MdmLinkHelper { private static final Logger ourLog = LoggerFactory.getLogger(MdmLinkHelper.class); + private enum Side { + LHS, // left hand side; practically speaking, this is the GoldenResource of the link + RHS // right hand side; practically speaking, this is the SourceResource of the link + } + @Autowired - IMdmLinkJpaRepository myMdmLinkDao; + private IMdmLinkDao myMdmLinkRepo; + @Autowired + private IFhirResourceDao myPatientDao; + @Autowired + private MdmLinkDaoSvc myMdmLinkDaoSvc; + @SuppressWarnings("rawtypes") + @Autowired + private IMdmLinkDao myMdmLinkDao; + @Autowired + private IdHelperService myIdHelperService; @Transactional public void logMdmLinks() { - List links = myMdmLinkDao.findAll(); + List links = myMdmLinkRepo.findAll(); ourLog.info("All MDM Links:"); for (MdmLink link : links) { IdDt goldenResourceId = link.getGoldenResource().getIdDt().toVersionless(); @@ -28,4 +66,204 @@ public class MdmLinkHelper { ourLog.info("{}: {}, {}, {}, {}", link.getId(), goldenResourceId, targetId, link.getMatchResult(), link.getLinkSource()); } } + + private MdmTransactionContext createContextForCreate(String theResourceType) { + MdmTransactionContext ctx = new MdmTransactionContext(); + ctx.setRestOperation(MdmTransactionContext.OperationType.CREATE_RESOURCE); + ctx.setResourceType(theResourceType); + ctx.setTransactionLogMessages(null); + return ctx; + } + + /** + * Creates all the initial links specified in the state object. + * + * These links will be returned in an MDMLinkResults object, in case + * they are needed. + */ + public MDMLinkResults setup(MDMState theState) { + MDMLinkResults results = new MDMLinkResults(); + + List inputExpressions = theState.getParsedInputState(); + + // create all patients if needed + for (MdmTestLinkExpression inputExpression : inputExpressions) { + createIfNeeded(theState, inputExpression.getLeftSideResourceIdentifier()); + createIfNeeded(theState, inputExpression.getRightSideResourceIdentifier()); + } + + // create all the links + for (MdmTestLinkExpression inputExpression : theState.getParsedInputState()) { + ourLog.info(inputExpression.getLinkExpression()); + + Patient goldenResource = theState.getParameter(inputExpression.getLeftSideResourceIdentifier()); + Patient targetResource = theState.getParameter(inputExpression.getRightSideResourceIdentifier()); + + MdmLinkSourceEnum matchSourceType = MdmLinkSourceEnum.valueOf(inputExpression.getMdmLinkSource()); + MdmMatchResultEnum matchResultType = MdmMatchResultEnum.valueOf(inputExpression.getMdmMatchResult()); + + MdmMatchOutcome matchOutcome = new MdmMatchOutcome( + null, + null + ); + matchOutcome.setMatchResultEnum(matchResultType); + + MdmLink link = (MdmLink) myMdmLinkDaoSvc.createOrUpdateLinkEntity( + goldenResource, // golden + targetResource, // source + matchOutcome, // match outcome + matchSourceType, // link source + createContextForCreate("Patient") // context + ); + + results.addResult(link); + } + + return results; + } + + private void createIfNeeded(MDMState theState, String thePatientId) { + Patient patient = theState.getParameter(thePatientId); + if (patient == null) { + // if it doesn't exist, create it + patient = createPatientAndTags(thePatientId, theState); + theState.addParameter(thePatientId, patient); + } + } + + private Patient createPatientAndTags(String theId, MDMState theState) { + Patient patient = new Patient(); + patient.setActive(true); // all mdm patients must be active + + // we add an identifier and use a forced id + // to make test debugging a little simpler + patient.addIdentifier(new Identifier().setValue(theId)); + patient.setId(theId); + + // Golden patients will be "GP#" + if (theId.length() >= 2 && theId.charAt(0) == 'G') { + // golden resource + MdmResourceUtil.setGoldenResource(patient); + } + MdmResourceUtil.setMdmManaged(patient); + + DaoMethodOutcome outcome = myPatientDao.update(patient, + SystemRequestDetails.forAllPartitions()); + Patient outputPatient = (Patient) outcome.getResource(); + theState.addPID(theId, outcome.getPersistentId()); + return outputPatient; + } + + public void validateResults(MDMState theState) { + List expectedOutputStates = theState.getParsedOutputState(); + + StringBuilder outputStateSB = new StringBuilder(); + + // for every parameter, we'll get all links + for (Map.Entry entrySet : theState.getParameterToValue().entrySet()) { + Patient patient = entrySet.getValue(); + List links = getAllMdmLinks(patient); + for (MdmLink link : links) { + if (!outputStateSB.isEmpty()) { + outputStateSB.append("\n"); + } + outputStateSB.append(createStateFromLink(link, theState)); + theState.addLinksForResource(patient, link); + } + } + + String actualOutputState = outputStateSB.toString(); + ourLog.info("Expected: \n" + theState.getOutputState()); + ourLog.info("Actual: \n" + actualOutputState); + + int totalExpectedLinks = expectedOutputStates.size(); + int totalActualLinks = theState.getActualOutcomeLinks().entries().size(); + + assertEquals(totalExpectedLinks, totalActualLinks, + String.format("Invalid number of links. Expected %d, Actual %d.", + totalExpectedLinks, totalActualLinks) + ); + + for (MdmTestLinkExpression stateExpression : expectedOutputStates) { + ourLog.info(stateExpression.getLinkExpression()); + + Patient leftSideResource = theState.getParameter(stateExpression.getLeftSideResourceIdentifier()); + Collection links = theState.getActualOutcomeLinks().get(leftSideResource); + assertFalse(links.isEmpty(), String.format("No links found, but expected state: %s", stateExpression)); + + MdmLinkSourceEnum matchSourceType = MdmLinkSourceEnum.valueOf(stateExpression.getMdmLinkSource()); + MdmMatchResultEnum matchResultType = MdmMatchResultEnum.valueOf(stateExpression.getMdmMatchResult()); + + Patient rightSideResource = theState.getParameter(stateExpression.getRightSideResourceIdentifier()); + + boolean foundLink = false; + for (MdmLink link : links) { + if (isResourcePartOfLink(link, leftSideResource, Side.LHS, theState) + && isResourcePartOfLink(link, rightSideResource, Side.RHS, theState) + && link.getMatchResult() == matchResultType + && link.getLinkSource() == matchSourceType + ) { + foundLink = true; + break; + } + } + + assertTrue(foundLink, String.format("State: %s - not found", stateExpression)); + } + } + + public List getAllMdmLinks(Patient theGoldenPatient) { + return myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theGoldenPatient).stream() + .map( link -> (MdmLink) link) + .collect(Collectors.toList()); + } + + private boolean isResourcePartOfLink( + MdmLink theLink, + Patient theResource, + Side theSide, + MDMState theState + ) { + ResourcePersistentId resourcePid = theState.getPID(theResource.getIdElement().getIdPart()); + + long linkPid; + if (theSide == Side.LHS) { + // LHS + linkPid = theLink.getGoldenResourcePid(); + } else { + // RHS + linkPid = theLink.getSourcePid(); + } + + return linkPid == resourcePid.getIdAsLong(); + } + + private String createStateFromLink(MdmLink theLink, MDMState theState) { + String LHS = ""; + String RHS = ""; + for (Map.Entry set : theState.getParameterToValue().entrySet()) { + Patient patient = set.getValue(); + if (isResourcePartOfLink(theLink, patient, Side.LHS, theState)) { + LHS = set.getKey(); + } + if (isResourcePartOfLink(theLink, patient, Side.RHS, theState)) { + RHS = set.getKey(); + } + + if (isNotBlank(LHS) && isNotBlank(RHS)) { + boolean selfReferential = LHS.equals(RHS); + + String link = LHS + ", " + + theLink.getLinkSource().name() + ", " + + theLink.getMatchResult().name() + ", " + + RHS; + if (selfReferential) { + link += " <- Invalid Self Referencing link!"; + } + return link; + } + } + + return "INVALID LINK: " + theLink.getId().toString(); + } } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/testmodels/MDMLinkResults.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/testmodels/MDMLinkResults.java new file mode 100644 index 00000000000..f0a15fda1a9 --- /dev/null +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/testmodels/MDMLinkResults.java @@ -0,0 +1,27 @@ +package ca.uhn.fhir.jpa.mdm.helper.testmodels; + +import ca.uhn.fhir.jpa.entity.MdmLink; + +import java.util.ArrayList; +import java.util.List; + +public class MDMLinkResults { + + private List myResults; + + public List getResults() { + if (myResults == null) { + myResults = new ArrayList<>(); + } + return myResults; + } + + public MDMLinkResults addResult(MdmLink theLink) { + getResults().add(theLink); + return this; + } + + public void setResults(List theResults) { + myResults = theResults; + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/testmodels/MDMState.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/testmodels/MDMState.java new file mode 100644 index 00000000000..2ba1c90da30 --- /dev/null +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/testmodels/MDMState.java @@ -0,0 +1,152 @@ +package ca.uhn.fhir.jpa.mdm.helper.testmodels; + +import ca.uhn.fhir.jpa.entity.MdmLink; +import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; +import org.testcontainers.shaded.com.google.common.collect.HashMultimap; +import org.testcontainers.shaded.com.google.common.collect.Multimap; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MDMState { + /** + * Collection of Parameter keys -> values to use + * + * Eg: + * P1 -> (some patient resource) + * PG -> (some golden patient resource) + */ + private Map myParameterToValue; + + /** + * Initial state for test. + * Comma separated lines with: + * Left param value, MdmLinkSourceEnum value, MdmMatchResultEnum value, Right param value + * + * Each input line represents a link state + * + * Eg: + * PG1, MANUAL, MATCH, P1 + * PG1, AUTO, POSSIBLE_MATCH, P2 + */ + private String myInputState; + + /** + * A list of link expressions for input state. + */ + private List myInputLinks; + + /** + * Output state to verify at end of test. + * Same format as input + */ + private String myOutputState; + + /** + * A list of link expressions for output state. + */ + private List myOutputLinks; + + /** + * The actual outcome links. + * Resource (typically a golden resource) -> Link + */ + private Multimap myActualOutcomeLinks; + + /** + * Map of forcedId -> resource persistent id for each resource created + */ + private final Map myForcedIdToPID = new HashMap<>(); + + public void addPID(String theForcedId, ResourcePersistentId thePid) { + assert !myForcedIdToPID.containsKey(theForcedId); + myForcedIdToPID.put(theForcedId, thePid); + } + + public ResourcePersistentId getPID(String theForcedId) { + return myForcedIdToPID.get(theForcedId); + } + + public Multimap getActualOutcomeLinks() { + if (myActualOutcomeLinks == null) { + myActualOutcomeLinks = HashMultimap.create(); + } + return myActualOutcomeLinks; + } + + public MDMState addLinksForResource(T theResource, MdmLink... theLinks) { + for (MdmLink link : theLinks) { + getActualOutcomeLinks().put(theResource, link); + } + return this; + } + + public void setActualOutcomeLinks(Multimap theActualOutcomeLinks) { + myActualOutcomeLinks = theActualOutcomeLinks; + } + + public Map getParameterToValue() { + if (myParameterToValue == null) { + myParameterToValue = new HashMap<>(); + } + return myParameterToValue; + } + + public T getParameter(String theKey) { + return getParameterToValue().get(theKey); + } + + public MDMState addParameter(String myParamKey, T myValue) { + getParameterToValue().put(myParamKey, myValue); + return this; + } + + public void setParameterToValue(Map theParameterToValue) { + myParameterToValue = theParameterToValue; + } + + public String getInputState() { + return myInputState; + } + + public MDMState setInputState(String theInputState) { + myInputState = theInputState; + return this; + } + + public String getOutputState() { + return myOutputState; + } + + public List getParsedInputState() { + if (myInputLinks == null) { + myInputLinks = new ArrayList<>(); + + String[] inputState = myInputState.split("\n"); + for (String is : inputState) { + myInputLinks.add(new MdmTestLinkExpression(is)); + } + } + return myInputLinks; + } + + public List getParsedOutputState() { + if (myOutputLinks == null) { + myOutputLinks = new ArrayList<>(); + String[] states = myOutputState.split("\n"); + for (String os : states) { + myOutputLinks.add(new MdmTestLinkExpression(os)); + } + } + return myOutputLinks; + } + + public MDMState setOutputState(String theOutputState) { + myOutputState = theOutputState; + return this; + } + + +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/testmodels/MdmTestLinkExpression.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/testmodels/MdmTestLinkExpression.java new file mode 100644 index 00000000000..e8c443914a3 --- /dev/null +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/helper/testmodels/MdmTestLinkExpression.java @@ -0,0 +1,57 @@ +package ca.uhn.fhir.jpa.mdm.helper.testmodels; + +import static org.junit.jupiter.api.Assertions.fail; + +public class MdmTestLinkExpression { + + private final String myLinkExpression; + + private final String myLeftSideResourceIdentifier; + private final String myMdmLinkSource; + private final String myMdmMatchResult; + private final String myRightSideResourceIdentifier; + + public MdmTestLinkExpression( + String theInputState + ) { + myLinkExpression = theInputState; + String[] params = parseState(theInputState); + + myLeftSideResourceIdentifier = params[0].trim(); + myMdmLinkSource = params[1].trim(); + myMdmMatchResult = params[2].trim(); + myRightSideResourceIdentifier = params[3].trim(); + } + + private String[] parseState(String theStateString) { + String[] state = theStateString.split(","); + if (state.length != 4) { + // we're using this exclusively in test area + fail( + String.format("%s must contain 4 arguments; found %d", theStateString, state.length) + ); + } + + return state; + } + + public String getLinkExpression() { + return myLinkExpression; + } + + public String getLeftSideResourceIdentifier() { + return myLeftSideResourceIdentifier; + } + + public String getMdmLinkSource() { + return myMdmLinkSource; + } + + public String getMdmMatchResult() { + return myMdmMatchResult; + } + + public String getRightSideResourceIdentifier() { + return myRightSideResourceIdentifier; + } +} diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderMergeGoldenResourcesR4Test.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderMergeGoldenResourcesR4Test.java index 33aaf3007bc..343ecef885e 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderMergeGoldenResourcesR4Test.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderMergeGoldenResourcesR4Test.java @@ -76,8 +76,10 @@ public class MdmProviderMergeGoldenResourcesR4Test extends BaseProviderR4Test { @Test public void testMerge() { - Patient mergedSourcePatient = (Patient) myMdmProvider.mergeGoldenResources(myFromGoldenPatientId, - myToGoldenPatientId, null, myRequestDetails); + Patient mergedSourcePatient = (Patient) myMdmProvider.mergeGoldenResources( + myFromGoldenPatientId, // from + myToGoldenPatientId, // to + null, myRequestDetails); // we do not check setActive anymore - as not all types support that assertTrue(MdmResourceUtil.isGoldenRecord(mergedSourcePatient)); @@ -97,12 +99,12 @@ public class MdmProviderMergeGoldenResourcesR4Test extends BaseProviderR4Test { // Optional redirect = fromSourcePatient.getIdentifier().stream().filter(theIdentifier -> theIdentifier.getSystem().equals("REDIRECT")).findFirst(); // assertThat(redirect.get().getValue(), is(equalTo(myToSourcePatient.getIdElement().toUnqualified().getValue()))); - List links = (List) myMdmLinkDaoSvc.findMdmLinksBySourceResource(myFromGoldenPatient); + List links = (List) myMdmLinkDaoSvc.findMdmLinksBySourceResource(myToGoldenPatient); assertThat(links, hasSize(1)); MdmLink link = links.get(0); - assertEquals(link.getSourcePid(), myFromGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong()); - assertEquals(link.getGoldenResourcePid(), myToGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong()); + assertEquals(link.getSourcePid(), myToGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong()); + assertEquals(link.getGoldenResourcePid(), myFromGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong()); assertEquals(link.getMatchResult(), MdmMatchResultEnum.REDIRECT); assertEquals(link.getLinkSource(), MdmLinkSourceEnum.MANUAL); } @@ -117,8 +119,11 @@ public class MdmProviderMergeGoldenResourcesR4Test extends BaseProviderR4Test { Patient toGoldenPatient = createPatientOnPartition(new Patient(), true, false, requestPartitionId); StringType toGoldenPatientId = new StringType(toGoldenPatient.getIdElement().getValue()); - Patient mergedSourcePatient = (Patient) myMdmProvider.mergeGoldenResources(fromGoldenPatientId, - toGoldenPatientId, null, myRequestDetails); + // test + Patient mergedSourcePatient = (Patient) myMdmProvider.mergeGoldenResources( + fromGoldenPatientId, + toGoldenPatientId, + null, myRequestDetails); assertTrue(MdmResourceUtil.isGoldenRecord(mergedSourcePatient)); assertFalse(MdmResourceUtil.isGoldenRecordRedirected(mergedSourcePatient)); @@ -129,12 +134,12 @@ public class MdmProviderMergeGoldenResourcesR4Test extends BaseProviderR4Test { // 2 from the set-up and only one from this test should be golden resource assertEquals(3, getAllGoldenPatients().size()); - List links = (List) myMdmLinkDaoSvc.findMdmLinksBySourceResource(fromGoldenPatient); + List links = (List) myMdmLinkDaoSvc.findMdmLinksBySourceResource(toGoldenPatient); assertThat(links, hasSize(1)); MdmLink link = links.get(0); - assertEquals(link.getSourcePid(), fromGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong()); - assertEquals(link.getGoldenResourcePid(), toGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong()); + assertEquals(link.getSourcePid(), toGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong()); + assertEquals(link.getGoldenResourcePid(), fromGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong()); assertEquals(link.getMatchResult(), MdmMatchResultEnum.REDIRECT); assertEquals(link.getLinkSource(), MdmLinkSourceEnum.MANUAL); } @@ -164,8 +169,10 @@ public class MdmProviderMergeGoldenResourcesR4Test extends BaseProviderR4Test { Patient patient = TerserUtil.clone(myFhirContext, myFromGoldenPatient); patient.setIdElement(null); - Patient mergedSourcePatient = (Patient) myMdmProvider.mergeGoldenResources(myFromGoldenPatientId, - myToGoldenPatientId, patient, myRequestDetails); + Patient mergedSourcePatient = (Patient) myMdmProvider.mergeGoldenResources( + myFromGoldenPatientId, // from + myToGoldenPatientId, // to + patient, myRequestDetails); assertEquals(myToGoldenPatient.getIdElement(), mergedSourcePatient.getIdElement()); assertThat(mergedSourcePatient, is(sameGoldenResourceAs(myToGoldenPatient))); @@ -176,12 +183,12 @@ public class MdmProviderMergeGoldenResourcesR4Test extends BaseProviderR4Test { assertFalse(MdmResourceUtil.isGoldenRecord(fromSourcePatient)); assertTrue(MdmResourceUtil.isGoldenRecordRedirected(fromSourcePatient)); - List links = (List) myMdmLinkDaoSvc.findMdmLinksBySourceResource(myFromGoldenPatient); + List links = (List) myMdmLinkDaoSvc.findMdmLinksBySourceResource(myToGoldenPatient); assertThat(links, hasSize(1)); MdmLink link = links.get(0); - assertEquals(link.getSourcePid(), myFromGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong()); - assertEquals(link.getGoldenResourcePid(), myToGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong()); + assertEquals(link.getSourcePid(), myToGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong()); + assertEquals(link.getGoldenResourcePid(), myFromGoldenPatient.getIdElement().toUnqualifiedVersionless().getIdPartAsLong()); assertEquals(link.getMatchResult(), MdmMatchResultEnum.REDIRECT); assertEquals(link.getLinkSource(), MdmLinkSourceEnum.MANUAL); } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmGoldenResourceMergerSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmGoldenResourceMergerSvcTest.java index 4373aeb69e9..1a3128dce00 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmGoldenResourceMergerSvcTest.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmGoldenResourceMergerSvcTest.java @@ -1,19 +1,18 @@ package ca.uhn.fhir.jpa.mdm.svc; import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.interceptor.api.IInterceptorService; +import ca.uhn.fhir.jpa.entity.MdmLink; +import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; +import ca.uhn.fhir.jpa.mdm.helper.MdmLinkHelper; +import ca.uhn.fhir.jpa.mdm.helper.testmodels.MDMState; +import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc; import ca.uhn.fhir.mdm.api.IMdmLink; import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; import ca.uhn.fhir.mdm.api.MdmMatchOutcome; import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; -import ca.uhn.fhir.mdm.api.IGoldenResourceMergerSvc; -import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.interceptor.api.IInterceptorService; -import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; -import ca.uhn.fhir.jpa.mdm.helper.MdmLinkHelper; import ca.uhn.fhir.mdm.interceptor.IMdmStorageInterceptor; -import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; +import ca.uhn.fhir.mdm.model.MdmTransactionContext; import ca.uhn.fhir.rest.server.TransactionLogMessages; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -21,12 +20,13 @@ import org.hl7.fhir.r4.model.Address; import org.hl7.fhir.r4.model.DateType; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.HumanName; -import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Example; @@ -39,9 +39,12 @@ import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class MdmGoldenResourceMergerSvcTest extends BaseMdmR4Test { + private static final Logger ourLog = LoggerFactory.getLogger(MdmGoldenResourceMergerSvcTest.class); public static final String GIVEN_NAME = "Jenn"; public static final String FAMILY_NAME = "Chan"; @@ -60,24 +63,16 @@ public class MdmGoldenResourceMergerSvcTest extends BaseMdmR4Test { private Patient myFromGoldenPatient; private Patient myToGoldenPatient; - private Long myFromGoldenPatientPid; - private Long myToGoldenPatientPid; private Patient myTargetPatient1; private Patient myTargetPatient2; - private Patient myTargetPatient3; @BeforeEach public void before() { myFromGoldenPatient = createGoldenPatient(); - IdType fromSourcePatientId = myFromGoldenPatient.getIdElement().toUnqualifiedVersionless(); - myFromGoldenPatientPid = runInTransaction(()->myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), fromSourcePatientId)).getIdAsLong(); myToGoldenPatient = createGoldenPatient(); - IdType toGoldenPatientId = myToGoldenPatient.getIdElement().toUnqualifiedVersionless(); - myToGoldenPatientPid = runInTransaction(()->myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), toGoldenPatientId)).getIdAsLong(); myTargetPatient1 = createPatient(); myTargetPatient2 = createPatient(); - myTargetPatient3 = createPatient(); // Register the mdm storage interceptor after the creates so the delete hook is fired when we merge myInterceptorService.registerInterceptor(myMdmStorageInterceptor); @@ -104,8 +99,19 @@ public class MdmGoldenResourceMergerSvcTest extends BaseMdmR4Test { } private Patient mergeGoldenPatients() { + return mergeGoldenPatientsFlip(false); + } + + private Patient mergeGoldenPatientsFlip(boolean theFlipToAndFromGoldenResources) { assertEquals(0, redirectLinkCount()); - Patient retval = (Patient) myGoldenResourceMergerSvc.mergeGoldenResources(myFromGoldenPatient, null, myToGoldenPatient, createMdmContext()); + Patient from = theFlipToAndFromGoldenResources ? myToGoldenPatient : myFromGoldenPatient; + Patient to = theFlipToAndFromGoldenResources ? myFromGoldenPatient : myToGoldenPatient; + Patient retval = (Patient) myGoldenResourceMergerSvc.mergeGoldenResources( + from, + null, + to, + createMdmContext() + ); assertEquals(1, redirectLinkCount()); return retval; } @@ -123,31 +129,53 @@ public class MdmGoldenResourceMergerSvcTest extends BaseMdmR4Test { } @Test - public void mergeRemovesPossibleDuplicatesLink() { - MdmLink mdmLink = (MdmLink) myMdmLinkDaoSvc.newMdmLink() - .setGoldenResourcePersistenceId(new ResourcePersistentId(myToGoldenPatientPid)) - .setSourcePersistenceId(new ResourcePersistentId(myFromGoldenPatientPid)) - .setMdmSourceType("Patient") - .setMatchResult(MdmMatchResultEnum.POSSIBLE_DUPLICATE) - .setLinkSource(MdmLinkSourceEnum.AUTO); + public void PG1DuplicatesPG2_mergePG2toPG1_PG2RedirectsToPG1() { + // setup + String inputState = """ + GP1, AUTO, POSSIBLE_DUPLICATE, GP2 + """; + MDMState state = new MDMState<>(); + state.setInputState(inputState); + myMdmLinkHelper.setup(state); - saveLink(mdmLink); + // test + mergeGoldenResources( + state.getParameter("GP2"), + state.getParameter("GP1") + ); - { - List foundLinks = myMdmLinkDao.findAll(); - assertEquals(1, foundLinks.size()); - assertEquals(MdmMatchResultEnum.POSSIBLE_DUPLICATE, foundLinks.get(0).getMatchResult()); - } + // verify + String outputState = + """ + GP2, MANUAL, REDIRECT, GP1 + """; + state.setOutputState(outputState); + myMdmLinkHelper.validateResults(state); + } - myMdmLinkHelper.logMdmLinks(); + @Test + public void GP1DuplicatesGP2_mergeGP1toGP2_GP1RedirectsToGP2() { + // setup + String inputState = """ + GP1, AUTO, POSSIBLE_DUPLICATE, GP2 + """; + MDMState state = new MDMState<>(); + state.setInputState(inputState); + myMdmLinkHelper.setup(state); - mergeGoldenPatients(); + // test + mergeGoldenResources( + state.getParameter("GP1"), + state.getParameter("GP2") + ); - { - List foundLinks = myMdmLinkDao.findAll(); - assertEquals(1, foundLinks.size()); - assertEquals(MdmMatchResultEnum.REDIRECT, foundLinks.get(0).getMatchResult()); - } + // verify + String outputState = + """ + GP1, MANUAL, REDIRECT, GP2 + """; + state.setOutputState(outputState); + myMdmLinkHelper.validateResults(state); } @Test @@ -213,19 +241,42 @@ public class MdmGoldenResourceMergerSvcTest extends BaseMdmR4Test { assertThat(mergedSourcePatient, is(possibleLinkedTo(myTargetPatient1))); } + private Patient mergeGoldenResources(Patient theFrom, Patient theTo) { + Patient retval = (Patient) myGoldenResourceMergerSvc.mergeGoldenResources( + theFrom, + null, + theTo, + createMdmContext() + ); + assertEquals(1, redirectLinkCount()); + return retval; + } + @Test public void fromManualLinkOverridesAutoToLink() { - MdmLink fromLink = createMdmLink(myFromGoldenPatient, myTargetPatient1); - fromLink.setLinkSource(MdmLinkSourceEnum.MANUAL); - fromLink.setMatchResult(MdmMatchResultEnum.MATCH); - saveLink(fromLink); + // setup + String inputState = """ + GP2, MANUAL, MATCH, P1 + GP1, AUTO, POSSIBLE_MATCH, P1 + """; + MDMState state = new MDMState<>(); + state.setInputState(inputState); - createMdmLink(myToGoldenPatient, myTargetPatient1); + myMdmLinkHelper.setup(state); - mergeGoldenPatients(); - List links = getNonRedirectLinksByGoldenResource(myToGoldenPatient); - assertEquals(1, links.size()); - assertEquals(MdmLinkSourceEnum.MANUAL, links.get(0).getLinkSource()); + // test + mergeGoldenResources( + state.getParameter("GP2"), // from + state.getParameter("GP1") // to + ); + + // verify + String outputState = """ + GP1, MANUAL, MATCH, P1 + GP2, MANUAL, REDIRECT, GP1 + """; + state.setOutputState(outputState); + myMdmLinkHelper.validateResults(state); } private List getNonRedirectLinksByGoldenResource(Patient theGoldenPatient) { @@ -322,27 +373,44 @@ public class MdmGoldenResourceMergerSvcTest extends BaseMdmR4Test { mergeGoldenPatients(); - assertResourceHasLinkCount(myToGoldenPatient, 3); - assertResourceHasLinkCount(myFromGoldenPatient, 0); + assertResourceHasLinkCount(myToGoldenPatient, 2); + assertResourceHasLinkCount(myFromGoldenPatient, 1); // TODO ENSURE PROPER LINK TYPES assertEquals(3, myMdmLinkDao.count()); } @Test public void from123To1() { - createMdmLink(myFromGoldenPatient, myTargetPatient1); - createMdmLink(myFromGoldenPatient, myTargetPatient2); - createMdmLink(myFromGoldenPatient, myTargetPatient3); - createMdmLink(myToGoldenPatient, myTargetPatient1); + // init + String inputState = """ + GP1, AUTO, POSSIBLE_MATCH, P1 + GP1, AUTO, POSSIBLE_MATCH, P2 + GP1, AUTO, POSSIBLE_MATCH, P3 + GP2, AUTO, POSSIBLE_MATCH, P1 + """; + MDMState state = new MDMState<>(); + state.setInputState(inputState); - mergeGoldenPatients(); + myMdmLinkHelper.setup(state); + + // test + mergeGoldenResources( + state.getParameter("GP1"), // from + state.getParameter("GP2") // to + ); myMdmLinkHelper.logMdmLinks(); - assertThat(myToGoldenPatient, is(possibleLinkedTo(myTargetPatient1, myTargetPatient2, myTargetPatient3))); - assertResourceHasAutoLinkCount(myToGoldenPatient, 3); + // validate + String outputState = """ + GP1, MANUAL, REDIRECT, GP2 + GP2, AUTO, POSSIBLE_MATCH, P1 + GP2, AUTO, POSSIBLE_MATCH, P2 + GP2, AUTO, POSSIBLE_MATCH, P3 + """; + state.setOutputState(outputState); + myMdmLinkHelper.validateResults(state); } - private void assertResourceHasLinkCount(IBaseResource theResource, int theCount) { List links = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theResource); assertEquals(theCount, links.size()); @@ -350,52 +418,95 @@ public class MdmGoldenResourceMergerSvcTest extends BaseMdmR4Test { @Test public void from1To123() { - createMdmLink(myFromGoldenPatient, myTargetPatient1); - createMdmLink(myToGoldenPatient, myTargetPatient1); - createMdmLink(myToGoldenPatient, myTargetPatient2); - createMdmLink(myToGoldenPatient, myTargetPatient3); + // setup + String inputState = """ + GP1, AUTO, POSSIBLE_MATCH, P1 + GP2, AUTO, POSSIBLE_MATCH, P1 + GP2, AUTO, POSSIBLE_MATCH, P2 + GP2, AUTO, POSSIBLE_MATCH, P3 + """; + MDMState state = new MDMState<>(); + state.setInputState(inputState); + myMdmLinkHelper.setup(state); + + // test + mergeGoldenResources( + state.getParameter("GP1"), + state.getParameter("GP2") + ); - mergeGoldenPatients(); myMdmLinkHelper.logMdmLinks(); - assertThat(myToGoldenPatient, is(possibleLinkedTo(myTargetPatient1, myTargetPatient2, myTargetPatient3))); - assertResourceHasAutoLinkCount(myToGoldenPatient, 3); - } - - private void assertResourceHasAutoLinkCount(Patient myToGoldenPatient, int theCount) { - List links = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(myToGoldenPatient); - assertEquals(theCount, links.stream().filter(IMdmLink::isAuto).count()); + // validate + String outputState = """ + GP1, MANUAL, REDIRECT, GP2 + GP2, AUTO, POSSIBLE_MATCH, P1 + GP2, AUTO, POSSIBLE_MATCH, P2 + GP2, AUTO, POSSIBLE_MATCH, P3 + """; + state.setOutputState(outputState); + myMdmLinkHelper.validateResults(state); } @Test public void from123To123() { - createMdmLink(myFromGoldenPatient, myTargetPatient1); - createMdmLink(myFromGoldenPatient, myTargetPatient2); - createMdmLink(myFromGoldenPatient, myTargetPatient3); - createMdmLink(myToGoldenPatient, myTargetPatient1); - createMdmLink(myToGoldenPatient, myTargetPatient2); - createMdmLink(myToGoldenPatient, myTargetPatient3); + // setup + String inputState = """ + GP1, AUTO, POSSIBLE_MATCH, P1 + GP1, AUTO, POSSIBLE_MATCH, P2 + GP1, AUTO, POSSIBLE_MATCH, P3 + GP2, AUTO, POSSIBLE_MATCH, P1 + GP2, AUTO, POSSIBLE_MATCH, P2 + GP2, AUTO, POSSIBLE_MATCH, P3 + """; + MDMState state = new MDMState<>(); + state.setInputState(inputState); + myMdmLinkHelper.setup(state); - mergeGoldenPatients(); + // test + mergeGoldenResources(state.getParameter("GP1"), state.getParameter("GP2")); - assertThat(myToGoldenPatient, is(possibleLinkedTo(myTargetPatient1, myTargetPatient2, myTargetPatient3))); - - assertResourceHasAutoLinkCount(myToGoldenPatient, 3); + // verify + String outputState = """ + GP1, MANUAL, REDIRECT, GP2 + GP2, AUTO, POSSIBLE_MATCH, P1 + GP2, AUTO, POSSIBLE_MATCH, P2 + GP2, AUTO, POSSIBLE_MATCH, P3 + """; + state.setOutputState(outputState); + myMdmLinkHelper.validateResults(state); } @Test public void from12To23() { - createMdmLink(myFromGoldenPatient, myTargetPatient1); - createMdmLink(myFromGoldenPatient, myTargetPatient2); - createMdmLink(myToGoldenPatient, myTargetPatient2); - createMdmLink(myToGoldenPatient, myTargetPatient3); + // setup + String inputState = """ + GP1, AUTO, POSSIBLE_MATCH, P1 + GP1, AUTO, POSSIBLE_MATCH, P2 + GP2, AUTO, POSSIBLE_MATCH, P2 + GP2, AUTO, POSSIBLE_MATCH, P3 + """; + MDMState state = new MDMState<>(); + state.setInputState(inputState); - mergeGoldenPatients(); + myMdmLinkHelper.setup(state); + + // test + mergeGoldenResources( + state.getParameter("GP1"), // from + state.getParameter("GP2") // to + ); myMdmLinkHelper.logMdmLinks(); - assertThat(myToGoldenPatient, is(possibleLinkedTo(myTargetPatient1, myTargetPatient2, myTargetPatient3))); - - assertResourceHasAutoLinkCount(myToGoldenPatient, 3); + // validate + String outputState = """ + GP1, MANUAL, REDIRECT, GP2 + GP2, AUTO, POSSIBLE_MATCH, P1 + GP2, AUTO, POSSIBLE_MATCH, P2 + GP2, AUTO, POSSIBLE_MATCH, P3 + """; + state.setOutputState(outputState); + myMdmLinkHelper.validateResults(state); } @Test @@ -476,4 +587,5 @@ public class MdmGoldenResourceMergerSvcTest extends BaseMdmR4Test { address.setPostalCode(POSTAL_CODE); theSourcePatient.setAddress(Collections.singletonList(address)); } + } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/dao/IMdmLinkDao.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/dao/IMdmLinkDao.java index 1e3dd6eb4bc..4214826a454 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/dao/IMdmLinkDao.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/dao/IMdmLinkDao.java @@ -30,7 +30,6 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import org.hl7.fhir.instance.model.api.IIdType; import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import java.util.Date; diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/MdmResourceUtil.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/MdmResourceUtil.java index 7ad4c1b0ecf..4867743aefb 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/MdmResourceUtil.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/MdmResourceUtil.java @@ -93,7 +93,7 @@ public final class MdmResourceUtil { /** * Sets the MDM-managed tag, indicating the MDM system has ownership of this - * Resource. No changes are made if resource is already maanged by MDM. + * Resource. No changes are made if resource is already managed by MDM. * * @param theBaseResource resource to set the tag for * @return Returns resource with the tag set. diff --git a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/HapiWorkerContext.java b/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/HapiWorkerContext.java index 9007b6d528d..904f80cd841 100644 --- a/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/HapiWorkerContext.java +++ b/hapi-fhir-structures-r5/src/main/java/org/hl7/fhir/r5/hapi/ctx/HapiWorkerContext.java @@ -9,28 +9,23 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.sl.cache.Cache; import ca.uhn.fhir.sl.cache.CacheFactory; import ca.uhn.fhir.system.HapiSystemProperties; -import ca.uhn.fhir.util.CoverageIgnore; import org.apache.commons.lang3.Validate; import org.fhir.ucum.UcumService; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.TerminologyServiceException; import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.context.IWorkerContextManager; -import org.hl7.fhir.r5.formats.IParser; -import org.hl7.fhir.r5.formats.ParserType; import org.hl7.fhir.r5.model.CanonicalResource; import org.hl7.fhir.r5.model.CodeSystem; import org.hl7.fhir.r5.model.CodeSystem.ConceptDefinitionComponent; import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.Coding; -import org.hl7.fhir.r5.model.ConceptMap; import org.hl7.fhir.r5.model.ElementDefinition.ElementDefinitionBindingComponent; import org.hl7.fhir.r5.model.NamingSystem; import org.hl7.fhir.r5.model.Parameters; import org.hl7.fhir.r5.model.Resource; import org.hl7.fhir.r5.model.ResourceType; import org.hl7.fhir.r5.model.StructureDefinition; -import org.hl7.fhir.r5.model.StructureMap; import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent; import org.hl7.fhir.r5.terminologies.ValueSetExpander;