Fix NPE in MDM post-mdm-clear on UPDATE operations. (#3851)

* Fix bug, imlpementation, changelog

* wip

* wip

* wip

* Add test

* wip

* wip
This commit is contained in:
Tadgh 2023-09-07 15:51:12 -07:00 committed by GitHub
parent a676506fc5
commit 57f6a705a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 73 additions and 23 deletions

View File

@ -0,0 +1,4 @@
---
type: fix
issue: 3786
title: "Previously, when executing an update on a resource that had to undergo MDM, a nullpointer could occur. This has been fixed."

View File

@ -19,7 +19,6 @@
*/ */
package ca.uhn.fhir.jpa.mdm.svc; package ca.uhn.fhir.jpa.mdm.svc;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc; import ca.uhn.fhir.jpa.mdm.dao.MdmLinkDaoSvc;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MatchedGoldenResourceCandidate; import ca.uhn.fhir.jpa.mdm.svc.candidate.MatchedGoldenResourceCandidate;
import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc; import ca.uhn.fhir.jpa.mdm.svc.candidate.MdmGoldenResourceFindingSvc;
@ -35,7 +34,6 @@ import ca.uhn.fhir.mdm.model.MdmTransactionContext;
import ca.uhn.fhir.mdm.util.EIDHelper; import ca.uhn.fhir.mdm.util.EIDHelper;
import ca.uhn.fhir.mdm.util.GoldenResourceHelper; import ca.uhn.fhir.mdm.util.GoldenResourceHelper;
import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -82,6 +80,7 @@ public class MdmEidUpdateService {
myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource( myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(
theTargetResource, updateContext.getMatchedGoldenResource(), theMdmTransactionContext); theTargetResource, updateContext.getMatchedGoldenResource(), theMdmTransactionContext);
IAnyResource theOldGoldenResource = updateContext.getExistingGoldenResource();
if (updateContext.isRemainsMatchedToSameGoldenResource()) { if (updateContext.isRemainsMatchedToSameGoldenResource()) {
// Copy over any new external EIDs which don't already exist. // Copy over any new external EIDs which don't already exist.
if (!updateContext.isIncomingResourceHasAnEid() || updateContext.isHasEidsInCommon()) { if (!updateContext.isIncomingResourceHasAnEid() || updateContext.isHasEidsInCommon()) {
@ -96,29 +95,35 @@ public class MdmEidUpdateService {
handleNoEidsInCommon( handleNoEidsInCommon(
theTargetResource, theMatchedGoldenResourceCandidate, theMdmTransactionContext, updateContext); theTargetResource, theMatchedGoldenResourceCandidate, theMdmTransactionContext, updateContext);
} }
} else if (theOldGoldenResource == null) {
// If we are in an update, and there is no existing golden resource, it is likely due to a clear operation,
// and we need to start from scratch.
myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(
theTargetResource, updateContext.getMatchedGoldenResource(), theMdmTransactionContext);
myMdmResourceDaoSvc.upsertGoldenResource(
updateContext.getMatchedGoldenResource(), theMdmTransactionContext.getResourceType());
myMdmLinkSvc.updateLink(
updateContext.getMatchedGoldenResource(),
theTargetResource,
theMatchedGoldenResourceCandidate.getMatchResult(),
MdmLinkSourceEnum.AUTO,
theMdmTransactionContext);
} else { } else {
// This is a new linking scenario. we have to break the existing link and link to the new Golden Resource. // This is a new linking scenario. we have to break the existing link and link to the new Golden Resource.
// For now, we create duplicate. // For now, we create duplicate.
// updated patient has an EID that matches to a new candidate. Link them, and set the Golden Resources // updated patient has an EID that matches to a new candidate. Link them, and set the Golden Resources
// possible duplicates // possible duplicates
IAnyResource theOldGoldenResource = updateContext.getExistingGoldenResource(); linkToNewGoldenResourceAndFlagAsDuplicate(
if (theOldGoldenResource == null) { theTargetResource,
throw new InternalErrorException( theMatchedGoldenResourceCandidate.getMatchResult(),
Msg.code(2362) theOldGoldenResource,
+ "Old golden resource was null while updating MDM links with new golden resource. It is likely that a $mdm-clear was performed without a $mdm-submit. Link will not be updated."); updateContext.getMatchedGoldenResource(),
} else { theMdmTransactionContext);
linkToNewGoldenResourceAndFlagAsDuplicate(
theTargetResource,
theMatchedGoldenResourceCandidate.getMatchResult(),
theOldGoldenResource,
updateContext.getMatchedGoldenResource(),
theMdmTransactionContext);
myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource( myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(
theTargetResource, updateContext.getMatchedGoldenResource(), theMdmTransactionContext); theTargetResource, updateContext.getMatchedGoldenResource(), theMdmTransactionContext);
myMdmResourceDaoSvc.upsertGoldenResource( myMdmResourceDaoSvc.upsertGoldenResource(
updateContext.getMatchedGoldenResource(), theMdmTransactionContext.getResourceType()); updateContext.getMatchedGoldenResource(), theMdmTransactionContext.getResourceType());
}
} }
} }
@ -283,5 +288,9 @@ public class MdmEidUpdateService {
public boolean isRemainsMatchedToSameGoldenResource() { public boolean isRemainsMatchedToSameGoldenResource() {
return myRemainsMatchedToSameGoldenResource; return myRemainsMatchedToSameGoldenResource;
} }
public boolean hasExistingGoldenResource() {
return myExistingGoldenResource != null;
}
} }
} }

View File

@ -626,6 +626,14 @@ abstract public class BaseMdmR4Test extends BaseJpaR4Test {
return retval; return retval;
} }
@Nonnull
protected MdmTransactionContext buildUpdateResourceMdmTransactionContext() {
MdmTransactionContext retval = new MdmTransactionContext();
retval.setResourceType("Patient");
retval.setRestOperation(MdmTransactionContext.OperationType.UPDATE_RESOURCE);
return retval;
}
protected MdmLink createGoldenPatientAndLinkToSourcePatient(Long thePatientPid, MdmMatchResultEnum theMdmMatchResultEnum) { protected MdmLink createGoldenPatientAndLinkToSourcePatient(Long thePatientPid, MdmMatchResultEnum theMdmMatchResultEnum) {
Patient patient = createPatient(); Patient patient = createPatient();

View File

@ -1,5 +1,8 @@
package ca.uhn.fhir.jpa.mdm.interceptor; package ca.uhn.fhir.jpa.mdm.interceptor;
import ca.uhn.fhir.batch2.api.StepExecutionDetails;
import ca.uhn.fhir.batch2.jobs.chunk.ResourceIdListWorkChunkJson;
import ca.uhn.fhir.batch2.model.JobInstance;
import ca.uhn.fhir.jpa.entity.MdmLink; import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
import ca.uhn.fhir.jpa.mdm.helper.MdmHelperConfig; import ca.uhn.fhir.jpa.mdm.helper.MdmHelperConfig;
@ -7,7 +10,11 @@ import ca.uhn.fhir.jpa.mdm.helper.MdmHelperR4;
import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkEvent; import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkEvent;
import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkJson; import ca.uhn.fhir.mdm.model.mdmevents.MdmLinkJson;
import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; import ca.uhn.fhir.mdm.api.MdmMatchResultEnum;
import ca.uhn.fhir.mdm.batch2.clear.MdmClearJobParameters;
import ca.uhn.fhir.mdm.batch2.clear.MdmClearStep;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.server.messaging.ResourceOperationMessage; import ca.uhn.fhir.rest.server.messaging.ResourceOperationMessage;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
@ -20,7 +27,11 @@ import org.springframework.data.domain.Example;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.TestPropertySource;
import javax.annotation.Nonnull;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;

View File

@ -1,6 +1,8 @@
package ca.uhn.fhir.jpa.mdm.svc; package ca.uhn.fhir.jpa.mdm.svc;
import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.entity.MdmLink; import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test;
import ca.uhn.fhir.jpa.mdm.config.BaseTestMdmConfig; import ca.uhn.fhir.jpa.mdm.config.BaseTestMdmConfig;
@ -80,7 +82,6 @@ public class MdmMatchLinkSvcTest {
private static final Logger ourLog = getLogger(MdmMatchLinkSvcTest.class); private static final Logger ourLog = getLogger(MdmMatchLinkSvcTest.class);
@Nested @Nested
public class NoBlockLinkTest extends BaseMdmR4Test { public class NoBlockLinkTest extends BaseMdmR4Test {
@Autowired @Autowired
@ -231,9 +232,26 @@ public class MdmMatchLinkSvcTest {
assertLinksMatchVector(null, null, null); assertLinksMatchVector(null, null, null);
} }
@Test @Test
public void testWhenPOSSIBLE_MATCHOccursOnGoldenResourceThatHasBeenManuallyNOMATCHedThatItIsBlocked() { public void updateMdmLinksForMdmSource_singleCandidateDuringUpdate_DoesNotNullPointer() {
Patient originalJane = createPatientAndUpdateLinks(buildJanePatient());
//Given: A patient exists with a matched golden resource.
Patient jane = createPatientAndUpdateLinks(buildJanePatient());
Patient goldenJane = getGoldenResourceFromTargetResource(jane);
//When: A patient who has no existing MDM links comes in as an update
Patient secondaryJane = createPatient(buildJanePatient(), false, false);
secondaryJane.setActive(true);
IAnyResource resource = (IAnyResource) myPatientDao.update(secondaryJane).getResource();
//Then: The secondary jane should link to the first jane.
myMdmMatchLinkSvc.updateMdmLinksForMdmSource(resource, buildUpdateResourceMdmTransactionContext());
assertThat(secondaryJane, is(sameGoldenResourceAs(jane)));
}
@Test
public void testWhenPOSSIBLE_MATCHOccursOnGoldenResourceThatHasBeenManuallyNOMATCHedThatItIsBlocked() {
Patient originalJane = createPatientAndUpdateLinks(buildJanePatient());
IBundleProvider search = myPatientDao.search(buildGoldenRecordSearchParameterMap()); IBundleProvider search = myPatientDao.search(buildGoldenRecordSearchParameterMap());
IAnyResource janeGoldenResource = (IAnyResource) search.getResources(0, 1).get(0); IAnyResource janeGoldenResource = (IAnyResource) search.getResources(0, 1).get(0);