Skip to content

Commit

Permalink
create merge provenance resource
Browse files Browse the repository at this point in the history
  • Loading branch information
mrdnctrk committed Jan 7, 2025
1 parent b51ccdc commit 02dd324
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Provenance;
import org.hl7.fhir.r4.model.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -84,13 +85,18 @@ public ResourceMergeService(
myBatch2TaskHelper = theBatch2TaskHelper;
myFhirContext = myPatientDao.getContext();
myHapiTransactionService = theHapiTransactionService;
myMergeResourceHelper = new MergeResourceHelper(myPatientDao);
IFhirResourceDao<Provenance> provenanceDao = theDaoRegistry.getResourceDao(Provenance.class);
myMergeResourceHelper = new MergeResourceHelper(myPatientDao, provenanceDao);
myMergeValidationService = new MergeValidationService(myFhirContext, theDaoRegistry);
}

/**
* Perform the $merge operation. If the number of resources to be changed exceeds the provided batch size,
* then switch to async mode. See the <a href="https://build.fhir.org/patient-operation-merge.html">Patient $merge spec</a>
* Perform the $merge operation. Operation can be performed synchronously or asynchronously depending on
* the prefer-async request header.
* If the operation is requested to be performed synchronously and the number of
* resources to be changed exceeds the provided batch size,
* and error is returned indicating that operation needs to be performed asynchronously. See the
* <a href="https://build.fhir.org/patient-operation-merge.html">Patient $merge spec</a>
* for details on what the difference is between synchronous and asynchronous mode.
*
* @param theMergeOperationParameters the merge operation parameters
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ public void before() throws Exception {

myTestHelper = new ReplaceReferencesTestHelper(myFhirContext, myDaoRegistry);
myTestHelper.beforeEach();
// keep the version on Provenance.target fields to verify that Provenance resources were saved
// with versioned target references
myFhirContext.getParserOptions()
.setDontStripVersionsFromReferencesAtPaths("Provenance.target");


mySrd.setRequestPartitionId(RequestPartitionId.allPartitions());
}
Expand Down Expand Up @@ -84,6 +89,8 @@ public void testHappyPath(boolean theDeleteSource, boolean theWithResultResource
myTestHelper.assertSourcePatientUpdatedOrDeleted(theDeleteSource);
myTestHelper.assertTargetPatientUpdated(theDeleteSource,
myTestHelper.getExpectedIdentifiersForTargetAfterMerge(theWithResultResource));

myTestHelper.assertMergeProvenance(theDeleteSource);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ public void before() throws Exception {
myStorageSettings.setReuseCachedSearchResultsForMillis(null);
myStorageSettings.setAllowMultipleDelete(true);
myFhirContext.setParserErrorHandler(new StrictErrorHandler());
// keep the version on Provenance.target fields to verify that Provenance resources were saved
// with versioned target references
myFhirContext.getParserOptions()
.setDontStripVersionsFromReferencesAtPaths("Provenance.target");

myTestHelper = new ReplaceReferencesTestHelper(myFhirContext, myDaoRegistry);
myTestHelper.beforeEach();
Expand Down Expand Up @@ -106,7 +110,6 @@ public void before() throws Exception {
})
public void testMerge(boolean withDelete, boolean withInputResultPatient, boolean withPreview, boolean isAsync) {
// setup

ReplaceReferencesTestHelper.PatientMergeInputParameters inParams = new ReplaceReferencesTestHelper.PatientMergeInputParameters();
myTestHelper.setSourceAndTarget(inParams);
inParams.deleteSource = withDelete;
Expand Down Expand Up @@ -225,7 +228,11 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea
myTestHelper.assertAllReferencesUpdated(withDelete);
myTestHelper.assertSourcePatientUpdatedOrDeleted(withDelete);
myTestHelper.assertTargetPatientUpdated(withDelete, expectedIdentifiersOnTargetAfterMerge);
myTestHelper.assertMergeProvenance(withDelete);
}



}

@Test
Expand Down Expand Up @@ -361,8 +368,7 @@ private Parameters callMergeOperation(Parameters inParameters, boolean isAsync)
class MyExceptionHandler implements TestExecutionExceptionHandler {
@Override
public void handleTestExecutionException(ExtensionContext theExtensionContext, Throwable theThrowable) throws Throwable {
if (theThrowable instanceof BaseServerResponseException) {
BaseServerResponseException ex = (BaseServerResponseException) theThrowable;
if (theThrowable instanceof BaseServerResponseException ex) {
String message = extractFailureMessage(ex);
throw ex.getClass().getDeclaredConstructor(String.class, Throwable.class).newInstance(message, ex);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient;
import ca.uhn.fhir.jpa.api.dao.PatientEverythingParameters;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInputAndPartialOutput;
import ca.uhn.fhir.rest.param.ReferenceParam;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
import ca.uhn.fhir.util.JsonUtil;
Expand All @@ -49,6 +51,7 @@
import org.hl7.fhir.r4.model.Organization;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Provenance;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType;
Expand All @@ -57,6 +60,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -96,6 +100,7 @@ public class ReplaceReferencesTestHelper {
private final IFhirResourceDao<Encounter> myEncounterDao;
private final IFhirResourceDao<CarePlan> myCarePlanDao;
private final IFhirResourceDao<Observation> myObservationDao;
private final IFhirResourceDao<Provenance> myProvenanceDao;

private IIdType myOrgId;
private IIdType mySourcePatientId;
Expand All @@ -117,6 +122,7 @@ public ReplaceReferencesTestHelper(FhirContext theFhirContext, DaoRegistry theDa
myEncounterDao = theDaoRegistry.getResourceDao(Encounter.class);
myCarePlanDao = theDaoRegistry.getResourceDao(CarePlan.class);
myObservationDao = theDaoRegistry.getResourceDao(Observation.class);
myProvenanceDao = theDaoRegistry.getResourceDao(Provenance.class);
}

public void beforeEach() throws Exception {
Expand Down Expand Up @@ -203,6 +209,45 @@ public IIdType getTargetPatientId() {
return myTargetPatientId;
}

public List<IBaseResource> searchProvenance(String targetId) {
SearchParameterMap map = new SearchParameterMap();
map.add("target", new ReferenceParam(targetId));
IBundleProvider searchBundle = myProvenanceDao.search(map, mySrd);
return searchBundle.getAllResources();
}

public void assertMergeProvenance(boolean theDeleteSource) {
List<IBaseResource> provenances = searchProvenance(myTargetPatientId.getValue());
assertThat(provenances).hasSize(1);
Provenance provenance = (Provenance) provenances.get(0);

// assert targets
assertThat(provenance.getTarget()).hasSize(theDeleteSource ? 1 : 2);
// the first target reference should be the target patient
String targetPatientReference = provenance.getTarget().get(0).getReference();
assertThat(targetPatientReference).isEqualTo(myTargetPatientId.getValue() + "/_history/2");
if (!theDeleteSource) {
// the second target reference should be the source patient, if it wasn't deleted
String sourcePatientReference = provenance.getTarget().get(1).getReference();
assertThat(sourcePatientReference).isEqualTo(mySourcePatientId.getValue() + "/_history/2");
}

assertThat(provenance.getRecorded()).isCloseTo(Instant.now(), 60000);

// validate provenance.reason
assertThat(provenance.getReason()).hasSize(1);
Coding reasonCoding = provenance.getReason().get(0).getCodingFirstRep();
assertThat(reasonCoding).isNotNull();
assertThat(reasonCoding.getSystem()).isEqualTo("http://terminology.hl7.org/CodeSystem/v3-ActReason");
assertThat(reasonCoding.getCode()).isEqualTo("PATADMIN");

// validate provenance.activity
Coding activityCoding = provenance.getActivity().getCodingFirstRep();
assertThat(activityCoding).isNotNull();
assertThat(activityCoding.getSystem()).isEqualTo("http://terminology.hl7.org/CodeSystem/iso-21089-lifecycle");
assertThat(activityCoding.getCode()).isEqualTo("merge");
}

private Set<IIdType> getTargetEverythingResourceIds() {
PatientEverythingParameters everythingParams = new PatientEverythingParameters();
everythingParams.setCount(new IntegerType(100));
Expand Down Expand Up @@ -432,7 +477,7 @@ private void validateJobReport(JobInstance theJobInstance, IIdType theTaskId) {

public List<Identifier> getExpectedIdentifiersForTargetAfterMerge(boolean theWithInputResultPatient) {

List<Identifier> expectedIdentifiersOnTargetAfterMerge = null;
List<Identifier> expectedIdentifiersOnTargetAfterMerge;
if (theWithInputResultPatient) {
expectedIdentifiersOnTargetAfterMerge =
List.of(new Identifier().setSystem("SYS1A").setValue("VAL1A"));
Expand All @@ -450,7 +495,7 @@ public List<Identifier> getExpectedIdentifiersForTargetAfterMerge(boolean theWit

public void assertSourcePatientUpdatedOrDeleted(boolean withDelete) {
if (withDelete) {
assertThrows(ResourceGoneException.class, () -> readSourcePatient());
assertThrows(ResourceGoneException.class, this::readSourcePatient);
} else {
Patient source = readSourcePatient();
assertThat(source.getLink()).hasSize(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@
import jakarta.annotation.Nullable;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Provenance;
import org.hl7.fhir.r4.model.Reference;

import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

Expand All @@ -43,10 +46,17 @@
*/
public class MergeResourceHelper {

private static final String ACTIVITY_CODE_SYSTEM = "http://terminology.hl7.org/CodeSystem/iso-21089-lifecycle";
private static final String ACTIVITY_CODE_MERGE = "merge";
private static final String ACT_REASON_CODE_SYSTEM = "http://terminology.hl7.org/CodeSystem/v3-ActReason";
private static final String ACT_REASON_PATIENT_ADMINISTRATION_CODE = "PATADMIN";

private final IFhirResourceDao<Patient> myPatientDao;
private final IFhirResourceDao<Provenance> myProvenceDao;

public MergeResourceHelper(IFhirResourceDao<Patient> theDao) {
myPatientDao = theDao;
public MergeResourceHelper(IFhirResourceDao<Patient> thePatientDao, IFhirResourceDao<Provenance> theProvenanceDao) {
myPatientDao = thePatientDao;
myProvenceDao = theProvenanceDao;
}

public static int setResourceLimitFromParameter(
Expand Down Expand Up @@ -100,6 +110,8 @@ public Patient updateMergedResourcesAfterReferencesReplaced(
prepareSourcePatientForUpdate(theSourceResource, theTargetResource);
updateResource(theSourceResource, theRequestDetails);
}

createProvenance(theSourceResource, targetPatientAfterUpdate.get(), theDeleteSource, theRequestDetails);
});

return targetPatientAfterUpdate.get();
Expand Down Expand Up @@ -184,4 +196,33 @@ private Patient updateResource(Patient theResource, RequestDetails theRequestDet
private void deleteResource(Patient theResource, RequestDetails theRequestDetails) {
myPatientDao.delete(theResource.getIdElement(), theRequestDetails);
}

private void createProvenance(
Patient theSourcePatient,
Patient theTargetPatient,
boolean theDeleteSource,
RequestDetails theRequestDetails) {

Provenance provenance = new Provenance();
provenance.addTarget().setReference(theTargetPatient.getIdElement().getValue());
if (!theDeleteSource) {
provenance.addTarget().setReference(theSourcePatient.getIdElement().getValue());
}

provenance.setRecorded(new Date());

CodeableConcept activityCodeableConcept = new CodeableConcept();
activityCodeableConcept.addCoding().setSystem(ACTIVITY_CODE_SYSTEM).setCode(ACTIVITY_CODE_MERGE);
provenance.setActivity(activityCodeableConcept);

CodeableConcept activityReasonCodeableConcept = new CodeableConcept();
activityReasonCodeableConcept
.addCoding()
.setSystem(ACT_REASON_CODE_SYSTEM)
.setCode(ACT_REASON_PATIENT_ADMINISTRATION_CODE);
provenance.addReason(activityReasonCodeableConcept);

// TODO Emre: should we add agent
myProvenceDao.create(provenance, theRequestDetails);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Provenance;

public class MergeUpdateTaskReducerStep extends ReplaceReferenceUpdateTaskReducerStep<MergeJobParameters> {
private final IHapiTransactionService myHapiTransactionService;
Expand Down Expand Up @@ -59,8 +60,8 @@ public RunOutcome run(
}

IFhirResourceDao<Patient> patientDao = myDaoRegistry.getResourceDao(Patient.class);

MergeResourceHelper helper = new MergeResourceHelper(patientDao);
IFhirResourceDao<Provenance> provenanceDao = myDaoRegistry.getResourceDao(Provenance.class);
MergeResourceHelper helper = new MergeResourceHelper(patientDao, provenanceDao);

helper.updateMergedResourcesAfterReferencesReplaced(
myHapiTransactionService,
Expand Down

0 comments on commit 02dd324

Please sign in to comment.