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 extends IMdmLink> 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 extends IMdmLink> 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 extends IMdmLink> 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 extends IMdmLink> fromLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theFromResource); // fromLinks - links going to theFromResource
- List extends IMdmLink> 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 extends IMdmLink> fromLinks = myMdmLinkDaoSvc.findMdmLinksByGoldenResource(theFromResource);
+ // toLinks - links from theToResource to any resource
+ List extends IMdmLink> 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 extends IMdmLink> 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 extends IMdmLink> 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 extends IMdmLink> 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;