From 054f7e667894e8f508287ca49f7380cb08d10ad5 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 20 Nov 2024 08:50:06 -0500 Subject: [PATCH 001/148] begin with failing test --- .../jpa/provider/r4/PatientMergeR4Test.java | 180 ++++++++++++++++++ .../server/provider/ProviderConstants.java | 14 ++ 2 files changed, 194 insertions(+) create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java new file mode 100644 index 000000000000..4b85376b2c4e --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -0,0 +1,180 @@ +package ca.uhn.fhir.jpa.provider.r4; + +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; +import ca.uhn.fhir.parser.StrictErrorHandler; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.util.BundleUtil; +import com.google.common.base.Charsets; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPut; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.Encounter.EncounterStatus; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Observation.ObservationStatus; +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.Reference; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Task; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class PatientMergeR4Test extends BaseResourceProviderR4Test { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PatientMergeR4Test.class); + private String orgId; + private String sourcePatId; + private String taskId; + private String encId1; + private String encId2; + private ArrayList myObsIds; + private String targetPatId; + private String targetEnc1; + + @BeforeEach + public void beforeDisableResultReuse() { + myStorageSettings.setReuseCachedSearchResultsForMillis(null); + } + + @Override + @AfterEach + public void after() throws Exception { + super.after(); + + myStorageSettings.setReuseCachedSearchResultsForMillis(new JpaStorageSettings().getReuseCachedSearchResultsForMillis()); + } + + @Override + @BeforeEach + public void before() throws Exception { + super.before(); + myFhirContext.setParserErrorHandler(new StrictErrorHandler()); + + myStorageSettings.setAllowMultipleDelete(true); + + Organization org = new Organization(); + org.setName("an org"); + orgId = myClient.create().resource(org).execute().getId().toUnqualifiedVersionless().getValue(); + ourLog.info("OrgId: {}", orgId); + + Patient patient = new Patient(); + patient.getManagingOrganization().setReference(orgId); + sourcePatId = myClient.create().resource(patient).execute().getId().toUnqualifiedVersionless().getValue(); + + Patient patient2 = new Patient(); + patient2.getManagingOrganization().setReference(orgId); + targetPatId = myClient.create().resource(patient2).execute().getId().toUnqualifiedVersionless().getValue(); + + Encounter enc1 = new Encounter(); + enc1.setStatus(EncounterStatus.CANCELLED); + enc1.getSubject().setReference(sourcePatId); + enc1.getServiceProvider().setReference(orgId); + encId1 = myClient.create().resource(enc1).execute().getId().toUnqualifiedVersionless().getValue(); + + Encounter enc2 = new Encounter(); + enc2.setStatus(EncounterStatus.ARRIVED); + enc2.getSubject().setReference(sourcePatId); + enc2.getServiceProvider().setReference(orgId); + encId2 = myClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless().getValue(); + + Task task = new Task(); + task.setStatus(Task.TaskStatus.COMPLETED); + task.getOwner().setReference(sourcePatId); + taskId = myClient.create().resource(task).execute().getId().toUnqualifiedVersionless().getValue(); + + Encounter targetEnc1 = new Encounter(); + targetEnc1.setStatus(EncounterStatus.ARRIVED); + targetEnc1.getSubject().setReference(targetPatId); + targetEnc1.getServiceProvider().setReference(orgId); + this.targetEnc1 = myClient.create().resource(targetEnc1).execute().getId().toUnqualifiedVersionless().getValue(); + + myObsIds = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + Observation obs = new Observation(); + obs.getSubject().setReference(sourcePatId); + obs.setStatus(ObservationStatus.FINAL); + String obsId = myClient.create().resource(obs).execute().getId().toUnqualifiedVersionless().getValue(); + myObsIds.add(obsId); + } + + } + + @Test + public void testMerge() throws Exception { + Parameters inParams = new Parameters(); + inParams.addParameter().setName(OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER).setValue(new Reference(sourcePatId)); + inParams.addParameter().setName(OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER).setValue(new Reference(targetPatId)); + + IGenericClient client = myFhirContext.newRestfulGenericClient(myServerBase); + Parameters outParams = client.operation() + .onType("Patient") + .named(OPERATION_MERGE) + .withParameters(inParams) + .returnResourceType(Parameters.class) + .execute(); + + // FIXME KHS validate outParams + + Bundle bundle = fetchBundle(myServerBase + "/" + targetPatId + "/$everything?_format=json&_count=100", EncodingEnum.JSON); + + assertNull(bundle.getLink("next")); + + Set actual = new TreeSet<>(); + for (BundleEntryComponent nextEntry : bundle.getEntry()) { + actual.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue()); + } + + ourLog.info("Found IDs: {}", actual); + + assertThat(actual).doesNotContain(sourcePatId); + assertThat(actual).contains(encId1); + assertThat(actual).contains(encId2); + assertThat(actual).contains(orgId); + assertThat(actual).contains(taskId); + assertThat(actual).contains(myObsIds.toArray(new String[0])); + assertThat(actual).contains(targetPatId); + assertThat(actual).contains(targetEnc1); + } + + // FIXME KHS look at PatientEverythingR4Test for ideas for other tests + + private Bundle fetchBundle(String theUrl, EncodingEnum theEncoding) throws IOException { + Bundle bundle; + HttpGet get = new HttpGet(theUrl); + CloseableHttpResponse resp = ourHttpClient.execute(get); + try { + assertEquals(theEncoding.getResourceContentTypeNonLegacy(), resp.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue().replaceAll(";.*", "")); + bundle = theEncoding.newParser(myFhirContext).parseResource(Bundle.class, IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8)); + } finally { + IOUtils.closeQuietly(resp); + } + + return bundle; + } + +} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index 09a6137206ee..4691c13b98b8 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -243,4 +243,18 @@ public class ProviderConstants { * Operation name for the "$export" operation */ public static final String OPERATION_EXPORT = "$export"; + /** + * Operation name for the Resource "$merge" operation + * Hapi-fhir use is based on https://www.hl7.org/fhir/patient-operation-merge.html + */ + public static final String OPERATION_MERGE = "$merge"; + /** + * Patient $merge operation parameters + */ + public static final String OPERATION_MERGE_SOURCE_PATIENT = "source-patient"; + public static final String OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER = "source-patient-identifier"; + public static final String OPERATION_MERGE_TARGET_PATIENT = "target-patient"; + public static final String OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER = "target-patient-identifier"; + public static final String OPERATION_MERGE_RESULT_PATIENT = "result-patient"; + public static final String OPERATION_MERGE_PREVIEW = "preview"; } From 97ea3c99d516a6e2c8b945fb75935432ae5e2103 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Tue, 26 Nov 2024 09:28:10 -0500 Subject: [PATCH 002/148] wpi merge operation provider --- .../BaseJpaResourceProviderPatient.java | 100 ++++++++++++ .../ca/uhn/fhir/mdm/util/IdentifierUtil.java | 19 +-- .../server/provider/ProviderConstants.java | 1 + .../dao/merge/MergeOperationParameters.java | 94 +++++++++++ .../jpa/dao/merge/ResourceMergeService.java | 130 +++++++++++++++ .../java/ca/uhn/fhir/util/IdentifierUtil.java | 49 ++++++ .../dao/merge/ResourceMergeServiceTest.java | 149 ++++++++++++++++++ 7 files changed, 524 insertions(+), 18 deletions(-) create mode 100644 hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java create mode 100644 hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java create mode 100644 hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/IdentifierUtil.java create mode 100644 hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index d48e77668753..9fc1b2aa1c0a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -19,8 +19,11 @@ */ package ca.uhn.fhir.jpa.provider; +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.dao.PatientEverythingParameters; +import ca.uhn.fhir.jpa.dao.merge.MergeOperationParameters; +import ca.uhn.fhir.jpa.dao.merge.ResourceMergeService; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.primitive.IdDt; @@ -39,12 +42,25 @@ import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenOrListParam; import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.provider.ProviderConstants; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.util.CanonicalIdentifier; +import ca.uhn.fhir.util.IdentifierUtil; +import ca.uhn.fhir.util.ParametersUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Patient; +import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -240,6 +256,90 @@ public IBundleProvider patientTypeEverything( } } + /** + * /Patient/$merge + */ + @Operation( + name = ProviderConstants.OPERATION_MERGE, + canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge") + public void patientMerge( + HttpServletRequest theServletRequest, + HttpServletResponse theServletResponse, + ServletRequestDetails theRequestDetails, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER) + List theSourcePatientIdentifier, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER) + List theTargetPatientIdentifier, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT, max = 1) + IBaseReference theSourcePatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT, max = 1) + IBaseReference theTargetPatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PREVIEW, typeName = "boolean", max = 1) + IPrimitiveType thePreview, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_RESULT_PATIENT, max = 1) + IBaseResource theResultPatient) + throws IOException { + + startRequest(theServletRequest); + try { + MergeOperationParameters mergeOperationParameters = createMergeOperationParameters( + theSourcePatientIdentifier, + theTargetPatientIdentifier, + theSourcePatient, + theTargetPatient, + thePreview, + theResultPatient); + + IFhirResourceDaoPatient dao = (IFhirResourceDaoPatient) getDao(); + ResourceMergeService resourceMergeService = new ResourceMergeService(dao); + + FhirContext fhirContext = dao.getContext(); + + ResourceMergeService.MergeOutcome mergeOutcome = + resourceMergeService.merge(mergeOperationParameters, theRequestDetails); + + IBaseParameters retVal = ParametersUtil.newInstance(fhirContext); + ParametersUtil.addParameterToParameters(fhirContext, retVal, "outcome", mergeOutcome.getOperationOutcome()); + + theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); + // we are writing the response to directly, otherwise the response status we set above is ignored. + fhirContext + .newJsonParser() + .setPrettyPrint(true) + .encodeResourceToWriter(retVal, theServletResponse.getWriter()); + theServletResponse.getWriter().close(); + } finally { + endRequest(theServletRequest); + } + } + + private MergeOperationParameters createMergeOperationParameters( + List theSourcePatientIdentifier, + List theTargetPatientIdentifier, + IBaseReference theSourcePatient, + IBaseReference theTargetPatient, + IPrimitiveType thePreview, + IBaseResource theResultPatient) { + MergeOperationParameters mergeOperationParameters = new MergeOperationParameters(); + if (theSourcePatientIdentifier != null) { + List sourceResourceIdentifiers = theSourcePatientIdentifier.stream() + .map(IdentifierUtil::identifierDtFromIdentifier) + .collect(Collectors.toList()); + mergeOperationParameters.setSourceResourceIdentifiers(sourceResourceIdentifiers); + } + if (theTargetPatientIdentifier != null) { + List targetResourceIdentifiers = theTargetPatientIdentifier.stream() + .map(IdentifierUtil::identifierDtFromIdentifier) + .collect(Collectors.toList()); + mergeOperationParameters.setTargetResourceIdentifiers(targetResourceIdentifiers); + } + mergeOperationParameters.setSourceResource(theSourcePatient); + mergeOperationParameters.setTargetResource(theTargetPatient); + mergeOperationParameters.setPreview(thePreview != null && thePreview.getValue()); + mergeOperationParameters.setResultPatient((Patient) theResultPatient); + return mergeOperationParameters; + } + /** * Given a list of string types, return only the ID portions of any parameters passed in. */ diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/IdentifierUtil.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/IdentifierUtil.java index 212716bc02f7..8c2aea9a5cb2 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/IdentifierUtil.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/IdentifierUtil.java @@ -22,7 +22,6 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.mdm.model.CanonicalEID; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.CanonicalIdentifier; import org.hl7.fhir.instance.model.api.IBase; @@ -31,23 +30,7 @@ public final class IdentifierUtil { private IdentifierUtil() {} public static CanonicalIdentifier identifierDtFromIdentifier(IBase theIdentifier) { - CanonicalIdentifier retval = new CanonicalIdentifier(); - - // TODO add other fields like "use" etc - if (theIdentifier instanceof org.hl7.fhir.dstu3.model.Identifier) { - org.hl7.fhir.dstu3.model.Identifier ident = (org.hl7.fhir.dstu3.model.Identifier) theIdentifier; - retval.setSystem(ident.getSystem()).setValue(ident.getValue()); - } else if (theIdentifier instanceof org.hl7.fhir.r4.model.Identifier) { - org.hl7.fhir.r4.model.Identifier ident = (org.hl7.fhir.r4.model.Identifier) theIdentifier; - retval.setSystem(ident.getSystem()).setValue(ident.getValue()); - } else if (theIdentifier instanceof org.hl7.fhir.r5.model.Identifier) { - org.hl7.fhir.r5.model.Identifier ident = (org.hl7.fhir.r5.model.Identifier) theIdentifier; - retval.setSystem(ident.getSystem()).setValue(ident.getValue()); - } else { - throw new InternalErrorException(Msg.code(1486) + "Expected 'Identifier' type but was '" - + theIdentifier.getClass().getName() + "'"); - } - return retval; + return ca.uhn.fhir.util.IdentifierUtil.identifierDtFromIdentifier(theIdentifier); } /** diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index 4691c13b98b8..74026d663941 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -252,6 +252,7 @@ public class ProviderConstants { * Patient $merge operation parameters */ public static final String OPERATION_MERGE_SOURCE_PATIENT = "source-patient"; + public static final String OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER = "source-patient-identifier"; public static final String OPERATION_MERGE_TARGET_PATIENT = "target-patient"; public static final String OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER = "target-patient-identifier"; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java new file mode 100644 index 000000000000..1c32166554be --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java @@ -0,0 +1,94 @@ +/*- + * #%L + * HAPI FHIR Storage api + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.dao.merge; + +import ca.uhn.fhir.util.CanonicalIdentifier; +import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.r4.model.Patient; + +import java.util.List; + +public class MergeOperationParameters { + + private List mySourceResourceIdentifiers; + private List myTargetResourceIdentifiers; + private IBaseReference mySourceResource; + private IBaseReference myTargetResource; + private boolean myPreview; + + // TODO: this can be changed to a generic resource to support other resources + private Patient myResultResource; + + public List getSourceIdentifiers() { + return mySourceResourceIdentifiers; + } + + public boolean hasAtLeastOneSourceIdentifier() { + return mySourceResourceIdentifiers != null && !mySourceResourceIdentifiers.isEmpty(); + } + + public void setSourceResourceIdentifiers(List theSourceIdentifiers) { + this.mySourceResourceIdentifiers = theSourceIdentifiers; + } + + public List getTargetIdentifiers() { + return myTargetResourceIdentifiers; + } + + public boolean hasAtLeastOneTargetIdentifier() { + return myTargetResourceIdentifiers != null && !myTargetResourceIdentifiers.isEmpty(); + } + + public void setTargetResourceIdentifiers(List theTargetIdentifiers) { + this.myTargetResourceIdentifiers = theTargetIdentifiers; + } + + public boolean isPreview() { + return myPreview; + } + + public void setPreview(boolean thePreview) { + this.myPreview = thePreview; + } + + public Patient getResultPatient() { + return myResultResource; + } + + public void setResultPatient(Patient theResultPatient) { + this.myResultResource = theResultPatient; + } + + public IBaseReference getSourceResource() { + return mySourceResource; + } + + public void setSourceResource(IBaseReference theSourceResource) { + this.mySourceResource = theSourceResource; + } + + public IBaseReference getTargetResource() { + return myTargetResource; + } + + public void setTargetResource(IBaseReference theTargetResource) { + this.myTargetResource = theTargetResource; + } +} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java new file mode 100644 index 000000000000..6113476f7418 --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -0,0 +1,130 @@ +/*- + * #%L + * HAPI FHIR Storage api + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.dao.merge; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.util.OperationOutcomeUtil; +import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; + +import java.util.ArrayList; +import java.util.List; + +import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_400_BAD_REQUEST; + +public class ResourceMergeService { + + IFhirResourceDaoPatient myDao; + FhirContext myFhirContext; + + public ResourceMergeService(IFhirResourceDaoPatient thePatientDao) { + myDao = thePatientDao; + myFhirContext = myDao.getContext(); + } + + /** + * Implemention of the $merge operation for resources + * @param theMergeOperationParameters the merge operation parameters + * @param theRequestDetails the request details + * @return the merge outcome containing OperationOutcome and HTTP status code + */ + public MergeOutcome merge(MergeOperationParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + + MergeOutcome mergeOutcome = new MergeOutcome(); + IBaseOperationOutcome outcome = OperationOutcomeUtil.newInstance(myFhirContext); + mergeOutcome.setOperationOutcome(outcome); + + if (!validateMergeOperationParameters(theMergeOperationParameters, outcome)) { + mergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); + return mergeOutcome; + } + + return mergeOutcome; + } + + /** + * Validates the merge operation parameters and adds validation errors to the outcome + * @param theMergeOperationParameters the merge operation parameters + * @param theOutcome the outcome to add validation errors to + * @return true if the parameters are valid, false otherwise + */ + private boolean validateMergeOperationParameters( + MergeOperationParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { + List errorMessages = new ArrayList<>(); + if (!theMergeOperationParameters.hasAtLeastOneSourceIdentifier() + && theMergeOperationParameters.getSourceResource() == null) { + errorMessages.add("There are no source resource parameters provided, include either a source-patient, " + + "source-patient-identifier parameter."); + } + + // Spec has conflicting information about this case + if (theMergeOperationParameters.hasAtLeastOneSourceIdentifier() + && theMergeOperationParameters.getSourceResource() != null) { + errorMessages.add( + "Source patient must be provided either by source-patient-identifier or by source-resource, not both."); + } + + if (!theMergeOperationParameters.hasAtLeastOneTargetIdentifier() + && theMergeOperationParameters.getTargetResource() == null) { + errorMessages.add("There are no target resource parameters provided, include either a target-patient, " + + "target-patient-identifier parameter."); + } + + // Spec has conflicting information about this case + if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() + && theMergeOperationParameters.getTargetResource() != null) { + errorMessages.add("Target patient must be provided either by target-patient-identifier or by " + + "target-resource, not both."); + } + + if (!errorMessages.isEmpty()) { + for (String validationError : errorMessages) { + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "error", validationError, null, null); + } + // there are validation errors + return false; + } + + // no validation errors + return true; + } + + public static class MergeOutcome { + private IBaseOperationOutcome myOperationOutcome; + private int myHttpStatusCode; + + public IBaseOperationOutcome getOperationOutcome() { + return myOperationOutcome; + } + + public void setOperationOutcome(IBaseOperationOutcome theOperationOutcome) { + this.myOperationOutcome = theOperationOutcome; + } + + public int getHttpStatusCode() { + return myHttpStatusCode; + } + + public void setHttpStatusCode(int theHttpStatusCode) { + this.myHttpStatusCode = theHttpStatusCode; + } + } +} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/IdentifierUtil.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/IdentifierUtil.java new file mode 100644 index 000000000000..97a707773407 --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/IdentifierUtil.java @@ -0,0 +1,49 @@ +/*- + * #%L + * HAPI FHIR Storage api + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.util; + +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.hl7.fhir.instance.model.api.IBase; + +public final class IdentifierUtil { + + private IdentifierUtil() {} + + public static CanonicalIdentifier identifierDtFromIdentifier(IBase theIdentifier) { + CanonicalIdentifier retval = new CanonicalIdentifier(); + + // TODO add other fields like "use" etc + if (theIdentifier instanceof org.hl7.fhir.dstu3.model.Identifier) { + org.hl7.fhir.dstu3.model.Identifier ident = (org.hl7.fhir.dstu3.model.Identifier) theIdentifier; + retval.setSystem(ident.getSystem()).setValue(ident.getValue()); + } else if (theIdentifier instanceof org.hl7.fhir.r4.model.Identifier) { + org.hl7.fhir.r4.model.Identifier ident = (org.hl7.fhir.r4.model.Identifier) theIdentifier; + retval.setSystem(ident.getSystem()).setValue(ident.getValue()); + } else if (theIdentifier instanceof org.hl7.fhir.r5.model.Identifier) { + org.hl7.fhir.r5.model.Identifier ident = (org.hl7.fhir.r5.model.Identifier) theIdentifier; + retval.setSystem(ident.getSystem()).setValue(ident.getValue()); + } else { + throw new InternalErrorException(Msg.code(1486) + "Expected 'Identifier' type but was '" + + theIdentifier.getClass().getName() + "'"); + } + return retval; + } +} diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java new file mode 100644 index 000000000000..abab5b172b8e --- /dev/null +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -0,0 +1,149 @@ +package ca.uhn.fhir.jpa.dao.merge; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.util.CanonicalIdentifier; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Reference; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ResourceMergeServiceTest { + + @Mock + private IFhirResourceDaoPatient myDaoMock; + @Mock + RequestDetails myRequestDetailsMock; + + private ResourceMergeService myResourceMergeService; + + private final FhirContext myFhirContext = FhirContext.forR4Cached(); + + @BeforeEach + void setup() { + when(myDaoMock.getContext()).thenReturn(myFhirContext); + myResourceMergeService = new ResourceMergeService(myDaoMock); + } + + @Test + void testValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorInOutcomeWith400Status() { + // Given + MergeOperationParameters mergeOperationParameters = new MergeOperationParameters(); + mergeOperationParameters.setTargetResource(new Reference("Patient/123")); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); + assertThat(operationOutcome.getIssue()).hasSize(1); + assertThat(operationOutcome.getIssueFirstRep().getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(operationOutcome.getIssueFirstRep().getDiagnostics()).contains("There are no source resource parameters provided, include either a source-patient, " + + "source-patient-identifier parameter."); + + verifyNoMoreInteractions(myDaoMock); + } + + + @Test + void testValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorInOutcomeWith400Status() { + // Given + MergeOperationParameters mergeOperationParameters = new MergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); + + assertThat(operationOutcome.getIssue()).hasSize(1); + assertThat(operationOutcome.getIssueFirstRep().getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(operationOutcome.getIssueFirstRep().getDiagnostics()).contains("There are no target resource " + + "parameters provided, include either a target-patient, target-patient-identifier parameter."); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ReturnsErrorsInOutcomeWith400Status() { + // Given + MergeOperationParameters mergeOperationParameters = new MergeOperationParameters(); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); + assertThat(operationOutcome.getIssue()).hasSize(2); + assertThat(operationOutcome.getIssue().get(0).getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(operationOutcome.getIssue().get(0).getDiagnostics()).contains("There are no source resource " + + "parameters provided, include either a source-patient, source-patient-identifier parameter."); + assertThat(operationOutcome.getIssue().get(1).getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(operationOutcome.getIssue().get(1).getDiagnostics()).contains("There are no target resource " + + "parameters provided, include either a target-patient, target-patient-identifier parameter."); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testValidatesInputParameters_BothSourceResourceParamsProvided_ReturnsErrorInOutcomeWith400Status() { + // Given + MergeOperationParameters mergeOperationParameters = new MergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); + mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); + + assertThat(operationOutcome.getIssue()).hasSize(1); + assertThat(operationOutcome.getIssueFirstRep().getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(operationOutcome.getIssueFirstRep().getDiagnostics()).contains("Source patient must be provided " + + "either by source-patient-identifier or by source-resource, not both."); + + + verifyNoMoreInteractions(myDaoMock); + } + + + @Test + void testValidatesInputParameters_BothTargetResourceParamsProvided_ReturnsErrorInOutcomeWith400Status() { + // Given + MergeOperationParameters mergeOperationParameters = new MergeOperationParameters(); + mergeOperationParameters.setTargetResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); + mergeOperationParameters.setSourceResource(new Reference("Patient/345")); + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); + + assertThat(operationOutcome.getIssue()).hasSize(1); + assertThat(operationOutcome.getIssueFirstRep().getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(operationOutcome.getIssueFirstRep().getDiagnostics()).contains("Target patient must be provided " + + "either by target-patient-identifier or by target-resource, not both."); + + verifyNoMoreInteractions(myDaoMock); + } + +} From 9fd4ebc146c9a9532cc50f39afa75985b3ae979f Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Wed, 27 Nov 2024 16:22:30 -0500 Subject: [PATCH 003/148] wip code resolve references and some refactoring --- .../BaseJpaResourceProviderPatient.java | 12 +- .../jpa/provider/r4/PatientMergeR4Test.java | 73 ++++++-- .../dao/merge/MergeOperationParameters.java | 20 +- .../PatientMergeOperationParameters.java | 28 +++ .../jpa/dao/merge/ResourceMergeService.java | 177 +++++++++++++++++- .../dao/merge/ResourceMergeServiceTest.java | 68 ++++--- 6 files changed, 322 insertions(+), 56 deletions(-) create mode 100644 hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 9fc1b2aa1c0a..f045292bcef0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -23,6 +23,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.dao.PatientEverythingParameters; import ca.uhn.fhir.jpa.dao.merge.MergeOperationParameters; +import ca.uhn.fhir.jpa.dao.merge.PatientMergeOperationParameters; import ca.uhn.fhir.jpa.dao.merge.ResourceMergeService; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.model.api.annotation.Description; @@ -55,7 +56,6 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Identifier; -import org.hl7.fhir.r4.model.Patient; import java.io.IOException; import java.util.Arrays; @@ -302,7 +302,10 @@ public void patientMerge( ParametersUtil.addParameterToParameters(fhirContext, retVal, "outcome", mergeOutcome.getOperationOutcome()); theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); - // we are writing the response to directly, otherwise the response status we set above is ignored. + // TODO Emre: we are writing the response to directly, otherwise the response status we set above is + // ignored. CDA Import operation does it this way too, but what if the client requests xml response? + // there needs to be a better way to do this + theServletResponse.setContentType(Constants.CT_JSON); fhirContext .newJsonParser() .setPrettyPrint(true) @@ -320,7 +323,7 @@ private MergeOperationParameters createMergeOperationParameters( IBaseReference theTargetPatient, IPrimitiveType thePreview, IBaseResource theResultPatient) { - MergeOperationParameters mergeOperationParameters = new MergeOperationParameters(); + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); if (theSourcePatientIdentifier != null) { List sourceResourceIdentifiers = theSourcePatientIdentifier.stream() .map(IdentifierUtil::identifierDtFromIdentifier) @@ -336,7 +339,8 @@ private MergeOperationParameters createMergeOperationParameters( mergeOperationParameters.setSourceResource(theSourcePatient); mergeOperationParameters.setTargetResource(theTargetPatient); mergeOperationParameters.setPreview(thePreview != null && thePreview.getValue()); - mergeOperationParameters.setResultPatient((Patient) theResultPatient); + mergeOperationParameters.setResultResource(theResultPatient); + return mergeOperationParameters; } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 4b85376b2c4e..d1773477774a 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -5,14 +5,13 @@ import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.EncodingEnum; -import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.client.api.IGenericClient; -import ca.uhn.fhir.util.BundleUtil; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPut; +import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.Encounter; @@ -23,26 +22,22 @@ import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Type; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; import java.util.ArrayList; -import java.util.List; import java.util.Set; import java.util.TreeSet; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; public class PatientMergeR4Test extends BaseResourceProviderR4Test { @@ -126,15 +121,15 @@ public void before() throws Exception { @Test public void testMerge() throws Exception { - Parameters inParams = new Parameters(); - inParams.addParameter().setName(OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER).setValue(new Reference(sourcePatId)); - inParams.addParameter().setName(OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER).setValue(new Reference(targetPatId)); + OperationParameters params = new OperationParameters(); + params.sourcePatient = new Reference().setReference(sourcePatId); + params.targetPatient = new Reference().setReference(targetPatId); IGenericClient client = myFhirContext.newRestfulGenericClient(myServerBase); Parameters outParams = client.operation() .onType("Patient") .named(OPERATION_MERGE) - .withParameters(inParams) + .withParameters(params.asParametersResource()) .returnResourceType(Parameters.class) .execute(); @@ -161,6 +156,23 @@ public void testMerge() throws Exception { assertThat(actual).contains(targetEnc1); } + @Test + void test_MissingRequiredParameters_Returns400BadRequest() { + Parameters inParams = new Parameters(); + + IGenericClient client = myFhirContext.newRestfulGenericClient(myServerBase); + + InvalidRequestException thrown = assertThrows(InvalidRequestException.class, () -> client.operation() + .onType("Patient") + .named(OPERATION_MERGE) + .withParameters(inParams) + .returnResourceType(Parameters.class) + .execute() + ); + + assertThat(thrown.getStatusCode()).isEqualTo(400); + } + // FIXME KHS look at PatientEverythingR4Test for ideas for other tests private Bundle fetchBundle(String theUrl, EncodingEnum theEncoding) throws IOException { @@ -177,4 +189,39 @@ private Bundle fetchBundle(String theUrl, EncodingEnum theEncoding) throws IOExc return bundle; } + + + private static class OperationParameters { + Type sourcePatient; + Type sourcePatientIdentifier; + Type targetPatient; + Type targetPatientIdentifier; + Patient resultResource; + Boolean preview; + + public Parameters asParametersResource() { + Parameters inParams = new Parameters(); + if (sourcePatient != null) { + inParams.addParameter().setName("source-patient").setValue(sourcePatient); + } + if (sourcePatientIdentifier!= null) { + inParams.addParameter().setName("source-patient-identifier").setValue(sourcePatientIdentifier); + } + if (targetPatient != null) { + inParams.addParameter().setName("target-patient").setValue(targetPatient); + } + if (targetPatientIdentifier != null) { + inParams.addParameter().setName("target-patient-identifier").setValue(targetPatientIdentifier); + } + if (resultResource != null) { + inParams.addParameter().setName("result-patient").setResource(resultResource); + } + if (preview != null) { + inParams.addParameter().setName("preview").setValue(new BooleanType(preview)); + } + return inParams; + } + } + + } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java index 1c32166554be..52d3fe1931fe 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java @@ -21,20 +21,26 @@ import ca.uhn.fhir.util.CanonicalIdentifier; import org.hl7.fhir.instance.model.api.IBaseReference; -import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.instance.model.api.IBaseResource; import java.util.List; -public class MergeOperationParameters { +public abstract class MergeOperationParameters { private List mySourceResourceIdentifiers; private List myTargetResourceIdentifiers; private IBaseReference mySourceResource; private IBaseReference myTargetResource; private boolean myPreview; + private IBaseResource myResultResource; - // TODO: this can be changed to a generic resource to support other resources - private Patient myResultResource; + public abstract String getSourceResourceParameterName(); + + public abstract String getTargetResourceParameterName(); + + public abstract String getSourceIdentifiersParameterName(); + + public abstract String getTargetIdentifiersParameterName(); public List getSourceIdentifiers() { return mySourceResourceIdentifiers; @@ -68,12 +74,12 @@ public void setPreview(boolean thePreview) { this.myPreview = thePreview; } - public Patient getResultPatient() { + public IBaseResource getResultResource() { return myResultResource; } - public void setResultPatient(Patient theResultPatient) { - this.myResultResource = theResultPatient; + public void setResultResource(IBaseResource theResultResource) { + this.myResultResource = theResultResource; } public IBaseReference getSourceResource() { diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java new file mode 100644 index 000000000000..4005f38aab7a --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java @@ -0,0 +1,28 @@ +package ca.uhn.fhir.jpa.dao.merge; + +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER; + +public class PatientMergeOperationParameters extends MergeOperationParameters { + @Override + public String getSourceResourceParameterName() { + return OPERATION_MERGE_SOURCE_PATIENT; + } + + @Override + public String getTargetResourceParameterName() { + return OPERATION_MERGE_TARGET_PATIENT; + } + + @Override + public String getSourceIdentifiersParameterName() { + return OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER; + } + + @Override + public String getTargetIdentifiersParameterName() { + return OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER; + } +} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 6113476f7418..e075dc0e630a 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -21,14 +21,27 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.util.CanonicalIdentifier; +import ca.uhn.fhir.util.IdentifierUtil; import ca.uhn.fhir.util.OperationOutcomeUtil; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Reference; import java.util.ArrayList; import java.util.List; +import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_200_OK; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_400_BAD_REQUEST; +import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY; public class ResourceMergeService { @@ -51,12 +64,31 @@ public MergeOutcome merge(MergeOperationParameters theMergeOperationParameters, MergeOutcome mergeOutcome = new MergeOutcome(); IBaseOperationOutcome outcome = OperationOutcomeUtil.newInstance(myFhirContext); mergeOutcome.setOperationOutcome(outcome); + // default to 200 OK, would be changed to another code during processing as required + mergeOutcome.setHttpStatusCode(STATUS_HTTP_200_OK); if (!validateMergeOperationParameters(theMergeOperationParameters, outcome)) { mergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); return mergeOutcome; } + IBaseResource sourceResource = resolveSourceResource(theMergeOperationParameters, theRequestDetails, outcome); + + if (sourceResource == null) { + mergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); + return mergeOutcome; + } + + IBaseResource targetResource = resolveTargetResource(theMergeOperationParameters, theRequestDetails, outcome); + + if (targetResource == null) { + mergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); + return mergeOutcome; + } + + // TODO Emre: do the actual merge + + addInfoToOperationOutcome(outcome, "Merge operation completed successfully."); return mergeOutcome; } @@ -71,33 +103,45 @@ private boolean validateMergeOperationParameters( List errorMessages = new ArrayList<>(); if (!theMergeOperationParameters.hasAtLeastOneSourceIdentifier() && theMergeOperationParameters.getSourceResource() == null) { - errorMessages.add("There are no source resource parameters provided, include either a source-patient, " - + "source-patient-identifier parameter."); + String msg = String.format( + "There are no source resource parameters provided, include either a %s, " + "or a %s parameter.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); + errorMessages.add(msg); } // Spec has conflicting information about this case if (theMergeOperationParameters.hasAtLeastOneSourceIdentifier() && theMergeOperationParameters.getSourceResource() != null) { - errorMessages.add( - "Source patient must be provided either by source-patient-identifier or by source-resource, not both."); + String msg = String.format( + "Source resource must be provided either by %s or by %s, not both.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); + errorMessages.add(msg); } if (!theMergeOperationParameters.hasAtLeastOneTargetIdentifier() && theMergeOperationParameters.getTargetResource() == null) { - errorMessages.add("There are no target resource parameters provided, include either a target-patient, " - + "target-patient-identifier parameter."); + String msg = String.format( + "There are no target resource parameters provided, include either a %s, " + "or a %s parameter.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); + errorMessages.add(msg); } // Spec has conflicting information about this case if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() && theMergeOperationParameters.getTargetResource() != null) { - errorMessages.add("Target patient must be provided either by target-patient-identifier or by " - + "target-resource, not both."); + String msg = String.format( + "Target resource must be provided either by %s or by %s, not both.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); + errorMessages.add(msg); } if (!errorMessages.isEmpty()) { for (String validationError : errorMessages) { - OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "error", validationError, null, null); + addErrorToOperationOutcome(theOutcome, validationError, "required"); } // there are validation errors return false; @@ -107,6 +151,121 @@ private boolean validateMergeOperationParameters( return true; } + private IBaseResource resolveSourceResource( + MergeOperationParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { + return resolveResource( + theOperationParameters.getSourceResource(), + theOperationParameters.getSourceIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getSourceResourceParameterName(), + theOperationParameters.getSourceIdentifiersParameterName()); + } + + private IBaseResource resolveTargetResource( + MergeOperationParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { + return resolveResource( + theOperationParameters.getTargetResource(), + theOperationParameters.getTargetIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getTargetResourceParameterName(), + theOperationParameters.getTargetIdentifiersParameterName()); + } + + private IBaseResource resolveResourceByIdentifiers( + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { + + SearchParameterMap searchParameterMap = new SearchParameterMap(); + TokenAndListParam tokenAndListParam = new TokenAndListParam(); + for (CanonicalIdentifier identifier : theIdentifiers) { + TokenParam tokenParam = new TokenParam( + identifier.getSystemElement().getValueAsString(), + identifier.getValueElement().getValueAsString()); + tokenAndListParam.addAnd(tokenParam); + } + searchParameterMap.add("identifier", tokenAndListParam); + searchParameterMap.setCount(2); + + IBundleProvider bundle = myDao.search(searchParameterMap, theRequestDetails); + List resources = bundle.getAllResources(); + if (resources.isEmpty()) { + String msg = String.format( + "No resources found matching the identifier(s) specified in %s", theOperationParameterName); + addErrorToOperationOutcome(theOutcome, msg, "not-found"); + return null; + } + if (resources.size() > 1) { + String msg = String.format( + "Multiple resources found matching the identifier(s) specified in %s", theOperationParameterName); + addErrorToOperationOutcome(theOutcome, msg, "multiple-matches"); + return null; + } + + return resources.get(0); + } + + private IBaseResource resolveResourceByReference( + IBaseReference theReference, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { + // TODO Emre: why does IBaseReference not have getIdentifier or hasReference methods? + // casting it to r4.Reference for now + Reference r4ref = (Reference) theReference; + + if (r4ref.hasReferenceElement()) { + try { + return myDao.read(r4ref.getReferenceElement(), theRequestDetails); + } catch (ResourceNotFoundException e) { + String msg = String.format( + "Resource not found for the reference specified in %s", theOperationParameterName); + addErrorToOperationOutcome(theOutcome, msg, "not-found"); + return null; + } + } + + // reference may have a identifier + if (r4ref.hasIdentifier()) { + Identifier identifier = r4ref.getIdentifier(); + CanonicalIdentifier canonicalIdentifier = IdentifierUtil.identifierDtFromIdentifier(identifier); + return resolveResourceByIdentifiers( + List.of(canonicalIdentifier), theRequestDetails, theOutcome, theOperationParameterName); + } + return null; + } + + private IBaseResource resolveResource( + IBaseReference theReference, + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationReferenceParameterName, + String theOperationIdentifiersParameterName) { + if (theReference != null) { + return resolveResourceByReference( + theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); + } + + return resolveResourceByIdentifiers( + theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); + } + + private void addInfoToOperationOutcome(IBaseOperationOutcome theOutcome, String theMsg) { + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theMsg, null, null); + } + + private void addErrorToOperationOutcome(IBaseOperationOutcome theOutcome, String theMsg, String theCode) { + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "error", theMsg, null, theCode); + } + public static class MergeOutcome { private IBaseOperationOutcome myOperationOutcome; private int myHttpStatusCode; diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index abab5b172b8e..8aae98ffb9aa 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -21,6 +21,15 @@ @ExtendWith(MockitoExtension.class) public class ResourceMergeServiceTest { + private static final String MISSING_SOURCE_PARAMS_MSG = + "There are no source resource parameters provided, include either a source-patient, or a source-patient-identifier parameter."; + private static final String MISSING_TARGET_PARAMS_MSG = + "There are no target resource parameters provided, include either a target-patient, or a target-patient-identifier parameter."; + private static final String BOTH_SOURCE_PARAMS_PROVIDED_MSG = + "Source resource must be provided either by source-patient or by source-patient-identifier, not both."; + private static final String BOTH_TARGET_PARAMS_PROVIDED_MSG = + "Target resource must be provided either by target-patient or by target-patient-identifier, not both."; + @Mock private IFhirResourceDaoPatient myDaoMock; @Mock @@ -36,10 +45,12 @@ void setup() { myResourceMergeService = new ResourceMergeService(myDaoMock); } + + @Test void testValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorInOutcomeWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new MergeOperationParameters(); + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setTargetResource(new Reference("Patient/123")); // When @@ -49,9 +60,11 @@ void testValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorInOutco OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); assertThat(operationOutcome.getIssue()).hasSize(1); - assertThat(operationOutcome.getIssueFirstRep().getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); - assertThat(operationOutcome.getIssueFirstRep().getDiagnostics()).contains("There are no source resource parameters provided, include either a source-patient, " + - "source-patient-identifier parameter."); + + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains(MISSING_SOURCE_PARAMS_MSG); + assertThat(issue.getCode().toCode()).isEqualTo("required"); verifyNoMoreInteractions(myDaoMock); } @@ -60,7 +73,7 @@ void testValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorInOutco @Test void testValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorInOutcomeWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new MergeOperationParameters(); + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); // When @@ -71,9 +84,11 @@ void testValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorInOutco assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); assertThat(operationOutcome.getIssue()).hasSize(1); - assertThat(operationOutcome.getIssueFirstRep().getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); - assertThat(operationOutcome.getIssueFirstRep().getDiagnostics()).contains("There are no target resource " + - "parameters provided, include either a target-patient, target-patient-identifier parameter."); + + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains(MISSING_TARGET_PARAMS_MSG); + assertThat(issue.getCode().toCode()).isEqualTo("required"); verifyNoMoreInteractions(myDaoMock); } @@ -81,7 +96,7 @@ void testValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorInOutco @Test void testValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ReturnsErrorsInOutcomeWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new MergeOperationParameters(); + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); // When ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -90,12 +105,15 @@ void testValidatesInputParameters_MissingBothSourceAndTargetPatientParams_Return OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); assertThat(operationOutcome.getIssue()).hasSize(2); - assertThat(operationOutcome.getIssue().get(0).getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); - assertThat(operationOutcome.getIssue().get(0).getDiagnostics()).contains("There are no source resource " + - "parameters provided, include either a source-patient, source-patient-identifier parameter."); - assertThat(operationOutcome.getIssue().get(1).getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); - assertThat(operationOutcome.getIssue().get(1).getDiagnostics()).contains("There are no target resource " + - "parameters provided, include either a target-patient, target-patient-identifier parameter."); + + OperationOutcome.OperationOutcomeIssueComponent issue1 = operationOutcome.getIssue().get(0); + OperationOutcome.OperationOutcomeIssueComponent issue2 = operationOutcome.getIssue().get(1); + assertThat(issue1.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue1.getDiagnostics()).contains(MISSING_SOURCE_PARAMS_MSG); + assertThat(issue1.getCode().toCode()).isEqualTo("required"); + assertThat(issue2.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue2.getDiagnostics()).contains(MISSING_TARGET_PARAMS_MSG); + assertThat(issue2.getCode().toCode()).isEqualTo("required"); verifyNoMoreInteractions(myDaoMock); } @@ -103,7 +121,7 @@ void testValidatesInputParameters_MissingBothSourceAndTargetPatientParams_Return @Test void testValidatesInputParameters_BothSourceResourceParamsProvided_ReturnsErrorInOutcomeWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new MergeOperationParameters(); + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); @@ -115,9 +133,11 @@ void testValidatesInputParameters_BothSourceResourceParamsProvided_ReturnsErrorI assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); assertThat(operationOutcome.getIssue()).hasSize(1); - assertThat(operationOutcome.getIssueFirstRep().getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); - assertThat(operationOutcome.getIssueFirstRep().getDiagnostics()).contains("Source patient must be provided " + - "either by source-patient-identifier or by source-resource, not both."); + + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains(BOTH_SOURCE_PARAMS_PROVIDED_MSG); + assertThat(issue.getCode().toCode()).isEqualTo("required"); verifyNoMoreInteractions(myDaoMock); @@ -127,7 +147,7 @@ void testValidatesInputParameters_BothSourceResourceParamsProvided_ReturnsErrorI @Test void testValidatesInputParameters_BothTargetResourceParamsProvided_ReturnsErrorInOutcomeWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new MergeOperationParameters(); + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setTargetResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); mergeOperationParameters.setSourceResource(new Reference("Patient/345")); @@ -139,9 +159,11 @@ void testValidatesInputParameters_BothTargetResourceParamsProvided_ReturnsErrorI assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); assertThat(operationOutcome.getIssue()).hasSize(1); - assertThat(operationOutcome.getIssueFirstRep().getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); - assertThat(operationOutcome.getIssueFirstRep().getDiagnostics()).contains("Target patient must be provided " + - "either by target-patient-identifier or by target-resource, not both."); + + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains(BOTH_TARGET_PARAMS_PROVIDED_MSG); + assertThat(issue.getCode().toCode()).isEqualTo("required"); verifyNoMoreInteractions(myDaoMock); } From 08e151712d35b76eab82df3b8817052b227142ec Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Thu, 28 Nov 2024 10:24:47 -0500 Subject: [PATCH 004/148] respect status code set in providers --- .../BaseJpaResourceProviderPatient.java | 17 ++++------------- .../BaseResourceReturningMethodBinding.java | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index f045292bcef0..df86efa8b6f6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -57,7 +57,6 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Identifier; -import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; @@ -262,7 +261,7 @@ public IBundleProvider patientTypeEverything( @Operation( name = ProviderConstants.OPERATION_MERGE, canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge") - public void patientMerge( + public IBaseParameters patientMerge( HttpServletRequest theServletRequest, HttpServletResponse theServletResponse, ServletRequestDetails theRequestDetails, @@ -277,8 +276,7 @@ public void patientMerge( @OperationParam(name = ProviderConstants.OPERATION_MERGE_PREVIEW, typeName = "boolean", max = 1) IPrimitiveType thePreview, @OperationParam(name = ProviderConstants.OPERATION_MERGE_RESULT_PATIENT, max = 1) - IBaseResource theResultPatient) - throws IOException { + IBaseResource theResultPatient) { startRequest(theServletRequest); try { @@ -302,15 +300,8 @@ public void patientMerge( ParametersUtil.addParameterToParameters(fhirContext, retVal, "outcome", mergeOutcome.getOperationOutcome()); theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); - // TODO Emre: we are writing the response to directly, otherwise the response status we set above is - // ignored. CDA Import operation does it this way too, but what if the client requests xml response? - // there needs to be a better way to do this - theServletResponse.setContentType(Constants.CT_JSON); - fhirContext - .newJsonParser() - .setPrettyPrint(true) - .encodeResourceToWriter(retVal, theServletResponse.getWriter()); - theServletResponse.getWriter().close(); + + return retVal; } finally { endRequest(theServletRequest); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java index 0042aadcbdd9..b7df1fac2302 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java @@ -277,16 +277,27 @@ public Object invokeServer(IRestfulServer theServer, RequestDetails theReques When we write directly to an HttpServletResponse, the invocation returns null. However, we still want to invoke the SERVER_OUTGOING_RESPONSE pointcut. */ + + // if the response status code is set by the method, respect it. Otherwise, use the default 200. + int responseCode = Constants.STATUS_HTTP_200_OK; + if (theRequest instanceof ServletRequestDetails) { + HttpServletResponse servletResponse = ((ServletRequestDetails) theRequest).getServletResponse(); + if (servletResponse != null && servletResponse.getStatus() > 0) { + responseCode = servletResponse.getStatus(); + } + } + if (response == null) { ResponseDetails responseDetails = new ResponseDetails(); - responseDetails.setResponseCode(Constants.STATUS_HTTP_200_OK); + responseDetails.setResponseCode(responseCode); callOutgoingResponseHook(theRequest, responseDetails); return null; } else { Set summaryMode = RestfulServerUtils.determineSummaryMode(theRequest); ResponseDetails responseDetails = new ResponseDetails(); responseDetails.setResponseResource(response); - responseDetails.setResponseCode(Constants.STATUS_HTTP_200_OK); + responseDetails.setResponseCode(responseCode); + if (!callOutgoingResponseHook(theRequest, responseDetails)) { return null; } From 16961a8dcab7d345d0c860dfb5d5e1f5ab88f0bb Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Fri, 29 Nov 2024 15:45:09 -0500 Subject: [PATCH 005/148] more validation and code to update resources after refs are updated --- .../BaseJpaResourceProviderPatient.java | 3 +- .../BaseResourceReturningMethodBinding.java | 2 +- .../dao/merge/MergeOperationParameters.java | 2 + .../PatientMergeOperationParameters.java | 6 + .../jpa/dao/merge/ResourceMergeService.java | 298 ++++++++++- .../dao/merge/ResourceMergeServiceTest.java | 474 +++++++++++++++++- 6 files changed, 747 insertions(+), 38 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index df86efa8b6f6..9cc900a1a9c7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -56,6 +56,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Patient; import java.util.Arrays; import java.util.List; @@ -288,7 +289,7 @@ public IBaseParameters patientMerge( thePreview, theResultPatient); - IFhirResourceDaoPatient dao = (IFhirResourceDaoPatient) getDao(); + IFhirResourceDaoPatient dao = (IFhirResourceDaoPatient) getDao(); ResourceMergeService resourceMergeService = new ResourceMergeService(dao); FhirContext fhirContext = dao.getContext(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java index b7df1fac2302..6635ec031ff8 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java @@ -278,7 +278,7 @@ public Object invokeServer(IRestfulServer theServer, RequestDetails theReques the SERVER_OUTGOING_RESPONSE pointcut. */ - // if the response status code is set by the method, respect it. Otherwise, use the default 200. + // if the response status code is set by the method, respect it. Otherwise, use the default 200. int responseCode = Constants.STATUS_HTTP_200_OK; if (theRequest instanceof ServletRequestDetails) { HttpServletResponse servletResponse = ((ServletRequestDetails) theRequest).getServletResponse(); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java index 52d3fe1931fe..c8ea098f0ed7 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java @@ -42,6 +42,8 @@ public abstract class MergeOperationParameters { public abstract String getTargetIdentifiersParameterName(); + public abstract String getResultResourceParameterName(); + public List getSourceIdentifiers() { return mySourceResourceIdentifiers; } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java index 4005f38aab7a..048350cbe414 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.dao.merge; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_RESULT_PATIENT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT; @@ -25,4 +26,9 @@ public String getSourceIdentifiersParameterName() { public String getTargetIdentifiersParameterName() { return OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER; } + + @Override + public String getResultResourceParameterName() { + return OPERATION_MERGE_RESULT_PATIENT; + } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index e075dc0e630a..65c816630928 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -26,35 +26,41 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.TokenAndListParam; import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.CanonicalIdentifier; import ca.uhn.fhir.util.IdentifierUtil; import ca.uhn.fhir.util.OperationOutcomeUtil; +import com.google.common.annotations.VisibleForTesting; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_200_OK; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_400_BAD_REQUEST; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY; +import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_500_INTERNAL_ERROR; public class ResourceMergeService { - IFhirResourceDaoPatient myDao; + IFhirResourceDaoPatient myDao; FhirContext myFhirContext; - public ResourceMergeService(IFhirResourceDaoPatient thePatientDao) { + public ResourceMergeService(IFhirResourceDaoPatient thePatientDao) { myDao = thePatientDao; myFhirContext = myDao.getContext(); } - /** - * Implemention of the $merge operation for resources + * Implementation of the $merge operation for resources * @param theMergeOperationParameters the merge operation parameters * @param theRequestDetails the request details * @return the merge outcome containing OperationOutcome and HTTP status code @@ -62,34 +68,257 @@ public ResourceMergeService(IFhirResourceDaoPatient thePatientDao) { public MergeOutcome merge(MergeOperationParameters theMergeOperationParameters, RequestDetails theRequestDetails) { MergeOutcome mergeOutcome = new MergeOutcome(); - IBaseOperationOutcome outcome = OperationOutcomeUtil.newInstance(myFhirContext); - mergeOutcome.setOperationOutcome(outcome); + IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); + mergeOutcome.setOperationOutcome(operationOutcome); // default to 200 OK, would be changed to another code during processing as required mergeOutcome.setHttpStatusCode(STATUS_HTTP_200_OK); - if (!validateMergeOperationParameters(theMergeOperationParameters, outcome)) { - mergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); - return mergeOutcome; + try { + doMerge(theMergeOperationParameters, theRequestDetails, mergeOutcome); + } catch (Exception e) { + if (e instanceof BaseServerResponseException) { + mergeOutcome.setHttpStatusCode(((BaseServerResponseException) e).getStatusCode()); + } else { + mergeOutcome.setHttpStatusCode(STATUS_HTTP_500_INTERNAL_ERROR); + } + OperationOutcomeUtil.addIssue(myFhirContext, operationOutcome, "error", e.getMessage(), null, "exception"); + } + return mergeOutcome; + } + + private void doMerge( + MergeOperationParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOutcome theMergeOutcome) { + + IBaseOperationOutcome operationOutcome = theMergeOutcome.getOperationOutcome(); + + if (!validateMergeOperationParameters(theMergeOperationParameters, operationOutcome)) { + theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); + return; } - IBaseResource sourceResource = resolveSourceResource(theMergeOperationParameters, theRequestDetails, outcome); + // cast to Patient, since we only support merging Patient resources for now + Patient sourceResource = + (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (sourceResource == null) { - mergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); - return mergeOutcome; + theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); + return; } - IBaseResource targetResource = resolveTargetResource(theMergeOperationParameters, theRequestDetails, outcome); + // cast to Patient, since we only support merging Patient resources for now + Patient targetResource = + (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (targetResource == null) { - mergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); - return mergeOutcome; + theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); + return; } - // TODO Emre: do the actual merge + if (!validateSourceAndTargetAreMergable(sourceResource, targetResource, operationOutcome)) { + theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); + return; + } - addInfoToOperationOutcome(outcome, "Merge operation completed successfully."); - return mergeOutcome; + if (!validateResultResourceIfExists( + theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { + theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); + return; + } + + if (theMergeOperationParameters.isPreview()) { + addInfoToOperationOutcome(operationOutcome, "Preview only merge operation - no issues detected"); + // TODO we should also return the resulting target patient in the response + return; + } + + // TODO Emre: do the actual ref updates + + // update resources after the refs update is completed + if (theMergeOperationParameters.getResultResource() != null) { + Patient resultResource = (Patient) theMergeOperationParameters.getResultResource(); + updateTargetResourceBasedOnResultResource(resultResource, theRequestDetails); + } else { + updateTargetResourceAfterRefsUpdated(targetResource, sourceResource, theRequestDetails); + } + + updateSourceResourceAfterRefsUpdated(sourceResource, targetResource, theRequestDetails); + + addInfoToOperationOutcome(operationOutcome, "Merge operation completed successfully."); + } + + private void updateTargetResourceBasedOnResultResource(Patient resultResource, RequestDetails theRequestDetails) { + myDao.update(resultResource, theRequestDetails); + } + + private boolean validateResultResourceIfExists( + MergeOperationParameters theMergeOperationParameters, + Patient theResolvedTargetResource, + Patient theResolvedSourceResource, + IBaseOperationOutcome theOperationOutcome) { + + if (theMergeOperationParameters.getResultResource() == null) { + // result resource is not provided, no further validation is needed + return true; + } + + Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); + + // validate the result resource's id as same as the target resource + if (!theResolvedTargetResource.getIdElement().toVersionless().equals(theResultResource.getIdElement())) { + String msg = String.format( + "'%s' must have the same versionless id as the actual resolved target resource. " + + "The actual resolved target resource's id is: '%s'", + theResolvedTargetResource.getIdElement().toVersionless().getValue(), + theMergeOperationParameters.getResultResourceParameterName()); + addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); + return false; + } + + // validate the result resource contains the identifiers provided in the target identifiers param + if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() + && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { + String msg = String.format( + "'%s' must have all the identifiers provided in %s", + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); + addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); + return false; + } + + if (!validateResultResourceHasReplacesLinkToSourceResource( + theResultResource, + theResolvedSourceResource, + theMergeOperationParameters.getResultResourceParameterName(), + theOperationOutcome)) { + return false; + } + + return true; + } + + private boolean hasAllIdentifiers(Patient theResource, List theIdentifiers) { + + List identifiersInResource = theResource.getIdentifier(); + for (CanonicalIdentifier identifier : theIdentifiers) { + boolean identifierFound = identifiersInResource.stream() + .anyMatch(i -> i.getSystem() + .equals(identifier.getSystemElement().getValueAsString()) + && i.getValue().equals(identifier.getValueElement().getValueAsString())); + + if (!identifierFound) { + return false; + } + } + return true; + } + + protected boolean validateResultResourceHasReplacesLinkToSourceResource( + Patient theResultResource, + Patient theResolvedSourceResource, + String theResultResourceParameterName, + IBaseOperationOutcome theOperationOutcome) { + // the result resource must have the replaces link set to the source resource + List replacesLinks = getLinksOfType(theResultResource, Patient.LinkType.REPLACES); + List replacesLinkToSourceResource = replacesLinks.stream() + .filter(r -> r.getReference() != null && r.getReference().equals(theResolvedSourceResource.getId())) + .collect(Collectors.toList()); + + if (replacesLinkToSourceResource.isEmpty()) { + String msg = String.format( + "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); + addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); + return false; + } + + if (replacesLinkToSourceResource.size() > 1) { + String msg = String.format( + "'%s' has multiple 'replaces' links to the source resource. There should be only one.", + theResultResourceParameterName); + addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); + return false; + } + return true; + } + + protected List getLinksOfType(Patient theResource, Patient.LinkType theLinkType) { + List links = new ArrayList<>(); + if (theResource.hasLink()) { + for (Patient.PatientLinkComponent link : theResource.getLink()) { + if (theLinkType.equals(link.getType()) && link.hasOther()) { + links.add(link.getOther()); + } + } + } + return links; + } + + @VisibleForTesting + protected boolean hasReplacedByLink(Patient theResource) { + if (theResource.hasLink()) { + for (Patient.PatientLinkComponent link : theResource.getLink()) { + if (Patient.LinkType.REPLACEDBY.equals(link.getType())) { + if (link.hasOther()) { + String otherReference = link.getOther().getReference(); + return otherReference != null; + } + } + } + } + return false; + } + + private boolean validateSourceAndTargetAreMergable( + Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { + + if (theSourceResource.getId().equalsIgnoreCase(theTargetResource.getId())) { + String msg = "Source and target resources are the same resource."; + // What is the right code to use in these cases? + addErrorToOperationOutcome(outcome, msg, "invalid"); + return false; + } + + if (theTargetResource.hasActive() && !theTargetResource.getActive()) { + String msg = "Target resource is not active, it must be active to be the target of a merge operation."; + addErrorToOperationOutcome(outcome, msg, "invalid"); + return false; + } + + if (hasReplacedByLink(theTargetResource)) { + String msg = "Target resource was previously replaced by another resource, it is not a suitable target " + + "for merging."; + addErrorToOperationOutcome(outcome, msg, "invalid"); + return false; + } + + // how about the source patient? should we check it active status and whether it was merged previously as well? + return true; + } + + private void updateTargetResourceAfterRefsUpdated( + Patient theTargetResource, Patient theSourceResource, RequestDetails theRequestDetails) { + theTargetResource + .addLink() + .setType(Patient.LinkType.REPLACES) + .setOther(new Reference(theSourceResource.getId())); + + myDao.update(theTargetResource, theRequestDetails); + } + + private void deleteSourceResourceAfterRefsUpdated(Patient theSourceResource, RequestDetails theRequestDetails) { + // TODO: handle errors + myDao.delete(theSourceResource.getIdElement(), theRequestDetails); + } + + private void updateSourceResourceAfterRefsUpdated( + Patient theSourceResource, Patient theTargetResource, RequestDetails theRequestDetails) { + theSourceResource.setActive(false); + theSourceResource + .addLink() + .setType(Patient.LinkType.REPLACEDBY) + .setOther(new Reference(theTargetResource.getId())); + myDao.update(theSourceResource, theRequestDetails); } /** @@ -104,7 +333,7 @@ private boolean validateMergeOperationParameters( if (!theMergeOperationParameters.hasAtLeastOneSourceIdentifier() && theMergeOperationParameters.getSourceResource() == null) { String msg = String.format( - "There are no source resource parameters provided, include either a %s, " + "or a %s parameter.", + "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", theMergeOperationParameters.getSourceResourceParameterName(), theMergeOperationParameters.getSourceIdentifiersParameterName()); errorMessages.add(msg); @@ -114,7 +343,7 @@ private boolean validateMergeOperationParameters( if (theMergeOperationParameters.hasAtLeastOneSourceIdentifier() && theMergeOperationParameters.getSourceResource() != null) { String msg = String.format( - "Source resource must be provided either by %s or by %s, not both.", + "Source resource must be provided either by '%s' or by '%s', not both.", theMergeOperationParameters.getSourceResourceParameterName(), theMergeOperationParameters.getSourceIdentifiersParameterName()); errorMessages.add(msg); @@ -123,7 +352,7 @@ private boolean validateMergeOperationParameters( if (!theMergeOperationParameters.hasAtLeastOneTargetIdentifier() && theMergeOperationParameters.getTargetResource() == null) { String msg = String.format( - "There are no target resource parameters provided, include either a %s, " + "or a %s parameter.", + "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", theMergeOperationParameters.getTargetResourceParameterName(), theMergeOperationParameters.getTargetIdentifiersParameterName()); errorMessages.add(msg); @@ -133,7 +362,7 @@ private boolean validateMergeOperationParameters( if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() && theMergeOperationParameters.getTargetResource() != null) { String msg = String.format( - "Target resource must be provided either by %s or by %s, not both.", + "Target resource must be provided either by '%s' or by '%s', not both.", theMergeOperationParameters.getTargetResourceParameterName(), theMergeOperationParameters.getTargetIdentifiersParameterName()); errorMessages.add(msg); @@ -198,13 +427,13 @@ private IBaseResource resolveResourceByIdentifiers( List resources = bundle.getAllResources(); if (resources.isEmpty()) { String msg = String.format( - "No resources found matching the identifier(s) specified in %s", theOperationParameterName); + "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "not-found"); return null; } if (resources.size() > 1) { String msg = String.format( - "Multiple resources found matching the identifier(s) specified in %s", theOperationParameterName); + "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "multiple-matches"); return null; } @@ -222,14 +451,31 @@ private IBaseResource resolveResourceByReference( Reference r4ref = (Reference) theReference; if (r4ref.hasReferenceElement()) { + + IIdType theResourceId = new IdType(r4ref.getReferenceElement().getValue()); + IBaseResource resource; try { - return myDao.read(r4ref.getReferenceElement(), theRequestDetails); + resource = myDao.read(theResourceId.toVersionless(), theRequestDetails); } catch (ResourceNotFoundException e) { String msg = String.format( - "Resource not found for the reference specified in %s", theOperationParameterName); + "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "not-found"); return null; } + + if (theResourceId.hasVersionIdPart() + && !theResourceId + .getVersionIdPart() + .equals(resource.getIdElement().getVersionIdPart())) { + String msg = String.format( + "The reference in '%s' parameter has a version specified, " + + "but it is not the latest version of the resource", + theOperationParameterName); + addErrorToOperationOutcome(theOutcome, msg, "conflict"); + return null; + } + + return resource; } // reference may have a identifier diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index 8aae98ffb9aa..07acd83e6a12 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -2,19 +2,38 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.CanonicalIdentifier; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.OngoingStubbing; +import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -22,16 +41,17 @@ public class ResourceMergeServiceTest { private static final String MISSING_SOURCE_PARAMS_MSG = - "There are no source resource parameters provided, include either a source-patient, or a source-patient-identifier parameter."; + "There are no source resource parameters provided, include either a 'source-patient', or a 'source-patient-identifier' parameter."; private static final String MISSING_TARGET_PARAMS_MSG = - "There are no target resource parameters provided, include either a target-patient, or a target-patient-identifier parameter."; + "There are no target resource parameters provided, include either a 'target-patient', or a 'target-patient-identifier' parameter."; private static final String BOTH_SOURCE_PARAMS_PROVIDED_MSG = - "Source resource must be provided either by source-patient or by source-patient-identifier, not both."; + "Source resource must be provided either by 'source-patient' or by 'source-patient-identifier', not both."; private static final String BOTH_TARGET_PARAMS_PROVIDED_MSG = - "Target resource must be provided either by target-patient or by target-patient-identifier, not both."; + "Target resource must be provided either by 'target-patient' or by 'target-patient-identifier', not both."; + private static final String SUCCESSFUL_MERGE_MSG = "Merge operation completed successfully"; @Mock - private IFhirResourceDaoPatient myDaoMock; + private IFhirResourceDaoPatient myDaoMock; @Mock RequestDetails myRequestDetailsMock; @@ -45,10 +65,58 @@ void setup() { myResourceMergeService = new ResourceMergeService(myDaoMock); } + @Test + void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheException() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + + ForbiddenOperationException ex = new ForbiddenOperationException("this is the exception message"); + when(myDaoMock.read(any(), eq(myRequestDetailsMock))).thenThrow(ex); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(403); + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("this is the exception message"); + assertThat(issue.getCode().toCode()).isEqualTo("exception"); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_UnhandledExceptionThrown_Uses500StatusCode() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + + RuntimeException ex = new RuntimeException("this is the exception message"); + when(myDaoMock.read(any(), eq(myRequestDetailsMock))).thenThrow(ex); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(500); + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("this is the exception message"); + assertThat(issue.getCode().toCode()).isEqualTo("exception"); + verifyNoMoreInteractions(myDaoMock); + } @Test - void testValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorInOutcomeWith400Status() { + void testMerge_ValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorWith400Status() { // Given MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setTargetResource(new Reference("Patient/123")); @@ -71,7 +139,7 @@ void testValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorInOutco @Test - void testValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorInOutcomeWith400Status() { + void testMerge_ValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorWith400Status() { // Given MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); @@ -94,7 +162,7 @@ void testValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorInOutco } @Test - void testValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ReturnsErrorsInOutcomeWith400Status() { + void testMerge_ValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ReturnsErrorsWith400Status() { // Given MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); @@ -119,7 +187,7 @@ void testValidatesInputParameters_MissingBothSourceAndTargetPatientParams_Return } @Test - void testValidatesInputParameters_BothSourceResourceParamsProvided_ReturnsErrorInOutcomeWith400Status() { + void testMerge_ValidatesInputParameters_BothSourceResourceParamsProvided_ReturnsErrorWith400Status() { // Given MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); @@ -145,7 +213,7 @@ void testValidatesInputParameters_BothSourceResourceParamsProvided_ReturnsErrorI @Test - void testValidatesInputParameters_BothTargetResourceParamsProvided_ReturnsErrorInOutcomeWith400Status() { + void testMerge_ValidatesInputParameters_BothTargetResourceParamsProvided_ReturnsErrorWith400Status() { // Given MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setTargetResource(new Reference("Patient/123")); @@ -168,4 +236,390 @@ void testValidatesInputParameters_BothTargetResourceParamsProvided_ReturnsErrorI verifyNoMoreInteractions(myDaoMock); } + @Test + void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWith422Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + when(myDaoMock.read(new IdType("Patient/123"), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("Resource not found for the reference specified in 'source-patient'"); + assertThat(issue.getCode().toCode()).isEqualTo("not-found"); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWith422Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + Patient sourcePatient = createPatient("Patient/123", Collections.emptyList()); + setupDaoMockForSuccessfulRead(sourcePatient); + when(myDaoMock.read(new IdType("Patient/345"), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("Resource not found for the reference specified in 'target-patient'"); + assertThat(issue.getCode().toCode()).isEqualTo("not-found"); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_ResolvesSourceResourceByIdentifierInReference_NoMatchFound_ReturnsErrorWith422Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference().setIdentifier(new Identifier().setSystem("sys").setValue("val"))); + mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + setupDaoMockSearchForIdentifiers(List.of(Collections.emptyList())); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + verifySearchParametersOnDaoSearchInvocations(List.of(List.of("sys|val"))); + + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("No resources found matching the identifier(s) specified in 'source-patient'"); + assertThat(issue.getCode().toCode()).isEqualTo("not-found"); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_ResolvesTargetResourceByIdentifierInReference_NoMatchFound_ReturnsErrorWith422Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResource(new Reference().setIdentifier(new Identifier().setSystem("sys").setValue("val"))); + setupDaoMockForSuccessfulRead(createPatient("Patient/123", Collections.emptyList())); + setupDaoMockSearchForIdentifiers(List.of(Collections.emptyList())); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + verifySearchParametersOnDaoSearchInvocations(List.of(List.of("sys|val"))); + + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("No resources found matching the identifier(s) specified in " + + "'target-patient'"); + assertThat(issue.getCode().toCode()).isEqualTo("not-found"); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_ResolvesSourceResourceByIdentifiers_NoMatchFound_ReturnsErrorWith422Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResourceIdentifiers(List.of( + new CanonicalIdentifier().setSystem("sys").setValue("val1"), + new CanonicalIdentifier().setSystem("sys").setValue("val2"))); + mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + setupDaoMockSearchForIdentifiers(List.of(Collections.emptyList())); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + verifySearchParametersOnDaoSearchInvocations(List.of(List.of("sys|val1","sys|val2"))); + + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("No resources found matching the identifier(s) specified in 'source-patient-identifier'"); + assertThat(issue.getCode().toCode()).isEqualTo("not-found"); + + verifyNoMoreInteractions(myDaoMock); + } + + + @Test + void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith422Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResourceIdentifiers(List.of( + new CanonicalIdentifier().setSystem("sys").setValue("val1"), + new CanonicalIdentifier().setSystem("sys").setValue("val2"))); + setupDaoMockSearchForIdentifiers(List.of(Collections.emptyList())); + Patient sourcePatient = createPatient("Patient/123", Collections.emptyList()); + setupDaoMockForSuccessfulRead(sourcePatient); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + verifySearchParametersOnDaoSearchInvocations(List.of(List.of("sys|val1", "sys|val2"))); + + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("No resources found matching the identifier(s) specified in " + + "'target-patient-identifier'"); + assertThat(issue.getCode().toCode()).isEqualTo("not-found"); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123/_history/1")); + mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + Patient sourcePatient = createPatient("Patient/123/_history/2", Collections.emptyList()); + setupDaoMockForSuccessfulRead(sourcePatient); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("The reference in 'source-patient' parameter has a version specified, but it is not the latest version of the resource"); + assertThat(issue.getCode().toCode()).isEqualTo("conflict"); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResource(new Reference("Patient/345/_history/1")); + Patient sourcePatient = createPatient("Patient/123", Collections.emptyList()); + Patient targetPatient = createPatient("Patient/345/_history/2", Collections.emptyList()); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("The reference in 'target-patient' parameter has a version " + + "specified, but it is not the latest version of the resource"); + assertThat(issue.getCode().toCode()).isEqualTo("conflict"); + + verifyNoMoreInteractions(myDaoMock); + } + + + + @Test + void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersionAreTheSame_NoErrorsReturned() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123/_history/2")); + mergeOperationParameters.setTargetResource(new Reference("Patient/345/_history/2")); + Patient sourcePatient = createPatient("Patient/123/_history/2", Collections.emptyList()); + Patient targetPatient = createPatient("Patient/345/_history/2", Collections.emptyList()); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(200); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(issue.getDiagnostics()).contains(SUCCESSFUL_MERGE_MSG); + + //TODO: enable this + //verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); + mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val2"))); + Patient sourcePatient = createPatient("Patient/123", Collections.emptyList()); + Patient targetPatient = createPatient("Patient/123", Collections.emptyList()); + setupDaoMockSearchForIdentifiers(List.of(List.of(sourcePatient), List.of(targetPatient))); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + + verifySearchParametersOnDaoSearchInvocations(List.of(List.of("sys|val1"), List.of("sys|val2"))); + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("Source and target resources are the same resource."); + + //TODO: enable this + //verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + Patient sourcePatient = createPatient("Patient/123", Collections.emptyList()); + Patient targetPatient = createPatient("Patient/345", Collections.emptyList()); + targetPatient.setActive(false); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("Target resource is not active, it must be active to be the target of a merge operation"); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsErrorWith422Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + Patient sourcePatient = createPatient("Patient/123", Collections.emptyList()); + Patient targetPatient = createPatient("Patient/345", List.of("Patient/someOtherPatient")); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("Target resource was previously replaced by another resource, it is not a suitable target for merging."); + + verifyNoMoreInteractions(myDaoMock); + } + + private Patient createPatient(String theId, List replacedByLinks) { + Patient patient = new Patient(); + patient.setId(theId); + for (String replacedByLink : replacedByLinks) { + patient.addLink().setType(Patient.LinkType.REPLACEDBY).setOther(new Reference(replacedByLink)); + } + return patient; + } + + private void setupDaoMockForSuccessfulRead(Patient resource) { + assertThat(resource.getIdElement()).isNotNull(); + //dao reads the versionless id + when(myDaoMock.read(resource.getIdElement().toVersionless(), myRequestDetailsMock)).thenReturn(resource); + } + + + /** + * Sets up the dao mock to return the given list of resources for each invocation of the search method + * @param theMatchingResourcesOnInvocations list containing the list of resources the search should return on each + * invocation of the search method, i.e. one list per invocation + */ + private void setupDaoMockSearchForIdentifiers(List> theMatchingResourcesOnInvocations) { + + OngoingStubbing ongoingStubbing = null; + for (List matchingResources : theMatchingResourcesOnInvocations) { + IBundleProvider bundleProviderMock = mock(IBundleProvider.class); + when(bundleProviderMock.getAllResources()).thenReturn(matchingResources); + if (ongoingStubbing == null) { + ongoingStubbing = when(myDaoMock.search(any(), eq(myRequestDetailsMock))).thenReturn(bundleProviderMock); + } + else { + ongoingStubbing.thenReturn(bundleProviderMock); + } + + } + } + + + + private void verifySearchParametersOnDaoSearchInvocations(List> theExpectedIdentifierParams) { + ArgumentCaptor captor = ArgumentCaptor.forClass(SearchParameterMap.class); + verify(myDaoMock, times(theExpectedIdentifierParams.size())).search(captor.capture(), eq(myRequestDetailsMock)); + List maps = captor.getAllValues(); + assertThat(maps).hasSameSizeAs(theExpectedIdentifierParams); + for (int i = 0; i < maps.size(); i++) { + verifySearchParameterOnSingleDaoSearchInvocation(maps.get(i), theExpectedIdentifierParams.get(i)); + } + + } + + private void verifySearchParameterOnSingleDaoSearchInvocation(SearchParameterMap capturedMap, + List theExpectedIdentifierParams) { + List> actualIdentifierParams = capturedMap.get("identifier"); + assertThat(actualIdentifierParams).hasSameSizeAs(theExpectedIdentifierParams); + for (int i = 0; i < theExpectedIdentifierParams.size(); i++) { + assertThat(actualIdentifierParams.get(i)).hasSize(1); + assertThat(actualIdentifierParams.get(i).get(0).getValueAsQueryToken(myFhirContext)).isEqualTo(theExpectedIdentifierParams.get(i)); + } + } } From e34d78849f7c53b6e5dec6b5873baef4e3239871 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Mon, 2 Dec 2024 17:11:02 -0500 Subject: [PATCH 006/148] added source-delete parameter and some validation tests on result-patient --- .../BaseJpaResourceProviderPatient.java | 5 + .../server/provider/ProviderConstants.java | 1 + .../dao/merge/MergeOperationParameters.java | 11 +- .../PatientMergeOperationParameters.java | 19 ++ .../jpa/dao/merge/ResourceMergeService.java | 30 ++- .../dao/merge/ResourceMergeServiceTest.java | 173 ++++++++++++++++-- 6 files changed, 209 insertions(+), 30 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 9cc900a1a9c7..d76a65544a0b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -276,6 +276,8 @@ public IBaseParameters patientMerge( IBaseReference theTargetPatient, @OperationParam(name = ProviderConstants.OPERATION_MERGE_PREVIEW, typeName = "boolean", max = 1) IPrimitiveType thePreview, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_DELETE_SOURCE, typeName = "boolean", max = 1) + IPrimitiveType theDeleteSource, @OperationParam(name = ProviderConstants.OPERATION_MERGE_RESULT_PATIENT, max = 1) IBaseResource theResultPatient) { @@ -287,6 +289,7 @@ public IBaseParameters patientMerge( theSourcePatient, theTargetPatient, thePreview, + theDeleteSource, theResultPatient); IFhirResourceDaoPatient dao = (IFhirResourceDaoPatient) getDao(); @@ -314,6 +317,7 @@ private MergeOperationParameters createMergeOperationParameters( IBaseReference theSourcePatient, IBaseReference theTargetPatient, IPrimitiveType thePreview, + IPrimitiveType theDeleteSource, IBaseResource theResultPatient) { MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); if (theSourcePatientIdentifier != null) { @@ -331,6 +335,7 @@ private MergeOperationParameters createMergeOperationParameters( mergeOperationParameters.setSourceResource(theSourcePatient); mergeOperationParameters.setTargetResource(theTargetPatient); mergeOperationParameters.setPreview(thePreview != null && thePreview.getValue()); + mergeOperationParameters.setDeleteSource(theDeleteSource != null && theDeleteSource.getValue()); mergeOperationParameters.setResultResource(theResultPatient); return mergeOperationParameters; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index 74026d663941..3c816daa8149 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -258,4 +258,5 @@ public class ProviderConstants { public static final String OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER = "target-patient-identifier"; public static final String OPERATION_MERGE_RESULT_PATIENT = "result-patient"; public static final String OPERATION_MERGE_PREVIEW = "preview"; + public static final String OPERATION_MERGE_DELETE_SOURCE = "delete-source"; } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java index c8ea098f0ed7..1346059ba4c0 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java @@ -32,6 +32,7 @@ public abstract class MergeOperationParameters { private IBaseReference mySourceResource; private IBaseReference myTargetResource; private boolean myPreview; + private boolean myDeleteSource; private IBaseResource myResultResource; public abstract String getSourceResourceParameterName(); @@ -68,7 +69,7 @@ public void setTargetResourceIdentifiers(List theTargetIden this.myTargetResourceIdentifiers = theTargetIdentifiers; } - public boolean isPreview() { + public boolean getPreview() { return myPreview; } @@ -76,6 +77,14 @@ public void setPreview(boolean thePreview) { this.myPreview = thePreview; } + public boolean getDeleteSource() { + return myDeleteSource; + } + + public void setDeleteSource(boolean theDeleteSource) { + this.myDeleteSource = theDeleteSource; + } + public IBaseResource getResultResource() { return myResultResource; } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java index 048350cbe414..35a245762e7f 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR Storage api + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.jpa.dao.merge; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_RESULT_PATIENT; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 65c816630928..fadeb2074d28 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -59,6 +59,7 @@ public ResourceMergeService(IFhirResourceDaoPatient thePatientDao) { myDao = thePatientDao; myFhirContext = myDao.getContext(); } + /** * Implementation of the $merge operation for resources * @param theMergeOperationParameters the merge operation parameters @@ -127,7 +128,7 @@ private void doMerge( return; } - if (theMergeOperationParameters.isPreview()) { + if (theMergeOperationParameters.getPreview()) { addInfoToOperationOutcome(operationOutcome, "Preview only merge operation - no issues detected"); // TODO we should also return the resulting target patient in the response return; @@ -143,7 +144,11 @@ private void doMerge( updateTargetResourceAfterRefsUpdated(targetResource, sourceResource, theRequestDetails); } - updateSourceResourceAfterRefsUpdated(sourceResource, targetResource, theRequestDetails); + if (theMergeOperationParameters.getDeleteSource()) { + deleteSourceResourceAfterRefsUpdated(sourceResource, theRequestDetails); + } else { + updateSourceResourceAfterRefsUpdated(sourceResource, targetResource, theRequestDetails); + } addInfoToOperationOutcome(operationOutcome, "Merge operation completed successfully."); } @@ -163,6 +168,8 @@ private boolean validateResultResourceIfExists( return true; } + boolean isValid = true; + Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); // validate the result resource's id as same as the target resource @@ -170,10 +177,10 @@ private boolean validateResultResourceIfExists( String msg = String.format( "'%s' must have the same versionless id as the actual resolved target resource. " + "The actual resolved target resource's id is: '%s'", - theResolvedTargetResource.getIdElement().toVersionless().getValue(), - theMergeOperationParameters.getResultResourceParameterName()); + theMergeOperationParameters.getResultResourceParameterName(), + theResolvedTargetResource.getIdElement().toVersionless().getValue()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); - return false; + isValid = false; } // validate the result resource contains the identifiers provided in the target identifiers param @@ -184,7 +191,7 @@ private boolean validateResultResourceIfExists( theMergeOperationParameters.getResultResourceParameterName(), theMergeOperationParameters.getTargetIdentifiersParameterName()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); - return false; + isValid = false; } if (!validateResultResourceHasReplacesLinkToSourceResource( @@ -192,10 +199,10 @@ private boolean validateResultResourceIfExists( theResolvedSourceResource, theMergeOperationParameters.getResultResourceParameterName(), theOperationOutcome)) { - return false; + isValid = false; } - return true; + return isValid; } private boolean hasAllIdentifiers(Patient theResource, List theIdentifiers) { @@ -222,7 +229,12 @@ protected boolean validateResultResourceHasReplacesLinkToSourceResource( // the result resource must have the replaces link set to the source resource List replacesLinks = getLinksOfType(theResultResource, Patient.LinkType.REPLACES); List replacesLinkToSourceResource = replacesLinks.stream() - .filter(r -> r.getReference() != null && r.getReference().equals(theResolvedSourceResource.getId())) + .filter(r -> r.getReference() != null + && r.getReference() + .equals(theResolvedSourceResource + .getIdElement() + .toVersionless() + .getValue())) .collect(Collectors.toList()); if (replacesLinkToSourceResource.isEmpty()) { diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index 07acd83e6a12..1339e5a9cd16 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -7,8 +7,6 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.CanonicalIdentifier; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -266,7 +264,7 @@ void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWi MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); - Patient sourcePatient = createPatient("Patient/123", Collections.emptyList()); + Patient sourcePatient = createPatient("Patient/123"); setupDaoMockForSuccessfulRead(sourcePatient); when(myDaoMock.read(new IdType("Patient/345"), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); @@ -318,7 +316,7 @@ void testMerge_ResolvesTargetResourceByIdentifierInReference_NoMatchFound_Return MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference().setIdentifier(new Identifier().setSystem("sys").setValue("val"))); - setupDaoMockForSuccessfulRead(createPatient("Patient/123", Collections.emptyList())); + setupDaoMockForSuccessfulRead(createPatient("Patient/123")); setupDaoMockSearchForIdentifiers(List.of(Collections.emptyList())); // When @@ -378,7 +376,7 @@ void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith new CanonicalIdentifier().setSystem("sys").setValue("val1"), new CanonicalIdentifier().setSystem("sys").setValue("val2"))); setupDaoMockSearchForIdentifiers(List.of(Collections.emptyList())); - Patient sourcePatient = createPatient("Patient/123", Collections.emptyList()); + Patient sourcePatient = createPatient("Patient/123"); setupDaoMockForSuccessfulRead(sourcePatient); // When @@ -406,7 +404,7 @@ void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVe MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123/_history/1")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); - Patient sourcePatient = createPatient("Patient/123/_history/2", Collections.emptyList()); + Patient sourcePatient = createPatient("Patient/123/_history/2"); setupDaoMockForSuccessfulRead(sourcePatient); // When @@ -431,8 +429,8 @@ void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVe MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345/_history/1")); - Patient sourcePatient = createPatient("Patient/123", Collections.emptyList()); - Patient targetPatient = createPatient("Patient/345/_history/2", Collections.emptyList()); + Patient sourcePatient = createPatient("Patient/123"); + Patient targetPatient = createPatient("Patient/345/_history/2"); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); @@ -461,8 +459,8 @@ void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersio MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123/_history/2")); mergeOperationParameters.setTargetResource(new Reference("Patient/345/_history/2")); - Patient sourcePatient = createPatient("Patient/123/_history/2", Collections.emptyList()); - Patient targetPatient = createPatient("Patient/345/_history/2", Collections.emptyList()); + Patient sourcePatient = createPatient("Patient/123/_history/2"); + Patient targetPatient = createPatient("Patient/345/_history/2"); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); @@ -488,8 +486,8 @@ void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status() MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val2"))); - Patient sourcePatient = createPatient("Patient/123", Collections.emptyList()); - Patient targetPatient = createPatient("Patient/123", Collections.emptyList()); + Patient sourcePatient = createPatient("Patient/123"); + Patient targetPatient = createPatient("Patient/123"); setupDaoMockSearchForIdentifiers(List.of(List.of(sourcePatient), List.of(targetPatient))); // When @@ -516,8 +514,8 @@ void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status() { MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); - Patient sourcePatient = createPatient("Patient/123", Collections.emptyList()); - Patient targetPatient = createPatient("Patient/345", Collections.emptyList()); + Patient sourcePatient = createPatient("Patient/123"); + Patient targetPatient = createPatient("Patient/345"); targetPatient.setActive(false); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); @@ -543,8 +541,9 @@ void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsError MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); - Patient sourcePatient = createPatient("Patient/123", Collections.emptyList()); - Patient targetPatient = createPatient("Patient/345", List.of("Patient/someOtherPatient")); + Patient sourcePatient = createPatient("Patient/123"); + Patient targetPatient = createPatient("Patient/345"); + addReplacedByLink(targetPatient, "Patient/678"); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); @@ -563,15 +562,149 @@ void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsError verifyNoMoreInteractions(myDaoMock); } - private Patient createPatient(String theId, List replacedByLinks) { + @Test + void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetResource_ReturnsErrorWith400Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + Patient resultPatient = createPatient("Patient/678"); + addReplacesLink(resultPatient, "Patient/123"); + mergeOperationParameters.setResultResource(resultPatient); + + Patient sourcePatient = createPatient("Patient/123/_history/1"); + Patient targetPatient = createPatient("Patient/345/_history/1"); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("'result-patient' must have the same versionless id as the actual resolved target resource. The actual resolved target resource's id is: 'Patient/345'"); + + verifyNoMoreInteractions(myDaoMock); + } + + + @Test + void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersProvidedInTargetIdentifiers_ReturnsErrorWith400Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResourceIdentifiers(List.of( + new CanonicalIdentifier().setSystem("sys").setValue("val1"), + new CanonicalIdentifier().setSystem("sys").setValue("val2") + )); + + // the result patient has only one of the identifiers that were provided in the target identifiers + Patient resultPatient = createPatient("Patient/345"); + resultPatient.addIdentifier().setSystem("sys").setValue("val"); + addReplacesLink(resultPatient, "Patient/123"); + mergeOperationParameters.setResultResource(resultPatient); + Patient sourcePatient = createPatient("Patient/123/_history/1"); + Patient targetPatient = createPatient("Patient/345/_history/1"); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockSearchForIdentifiers(List.of(List.of(targetPatient))); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("'result-patient' must have all the identifiers provided in target-patient-identifier"); + + verifyNoMoreInteractions(myDaoMock); + } + + + @Test + void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLink_ReturnsErrorWith400Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + + Patient resultPatient = createPatient("Patient/345"); + mergeOperationParameters.setResultResource(resultPatient); + Patient sourcePatient = createPatient("Patient/123/_history/1"); + Patient targetPatient = createPatient("Patient/345/_history/1"); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("'result-patient' must have a 'replaces' link to the source resource."); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_ValidatesResultResource_ResultResourceHasRedundantReplacesLinksToSource_ReturnsErrorWith400Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + + Patient resultPatient = createPatient("Patient/345"); + //add the link twice + addReplacesLink(resultPatient, "Patient/123"); + addReplacesLink(resultPatient, "Patient/123"); + + mergeOperationParameters.setResultResource(resultPatient); + Patient sourcePatient = createPatient("Patient/123/_history/1"); + Patient targetPatient = createPatient("Patient/345/_history/1"); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + // When + ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("'result-patient' has multiple 'replaces' links to the source resource. There should be only one."); + + verifyNoMoreInteractions(myDaoMock); + } + + private Patient createPatient(String theId) { Patient patient = new Patient(); patient.setId(theId); - for (String replacedByLink : replacedByLinks) { - patient.addLink().setType(Patient.LinkType.REPLACEDBY).setOther(new Reference(replacedByLink)); - } return patient; } + private void addReplacedByLink(Patient thePatient, String theReplacingResourceId) { + thePatient.addLink().setType(Patient.LinkType.REPLACEDBY).setOther(new Reference(theReplacingResourceId)); + } + + private void addReplacesLink(Patient patient, String theReplacedResourceId) { + patient.addLink().setType(Patient.LinkType.REPLACES).setOther(new Reference(theReplacedResourceId)); + } + private void setupDaoMockForSuccessfulRead(Patient resource) { assertThat(resource.getIdElement()).isNotNull(); //dao reads the versionless id From 80e6439156838b8bce0dbabebd705055a1b56d55 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 2 Dec 2024 18:36:14 -0500 Subject: [PATCH 007/148] update test --- .../java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index d1773477774a..1f922528ebc5 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -146,7 +146,8 @@ public void testMerge() throws Exception { ourLog.info("Found IDs: {}", actual); - assertThat(actual).doesNotContain(sourcePatId); + // FIXME KHS parameterize this based on delete-source=true +// assertThat(actual).doesNotContain(sourcePatId); assertThat(actual).contains(encId1); assertThat(actual).contains(encId2); assertThat(actual).contains(orgId); From f265e64d3ea516eabdd9dfd692bc332c0735cf88 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 3 Dec 2024 11:29:07 -0500 Subject: [PATCH 008/148] update test --- .../jpa/provider/r4/PatientMergeR4Test.java | 21 +++++++++++++------ .../jpa/dao/merge/ResourceMergeService.java | 4 ++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 1f922528ebc5..7459e4b07c1a 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -27,6 +27,8 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.io.IOException; import java.util.ArrayList; @@ -119,11 +121,13 @@ public void before() throws Exception { } - @Test - public void testMerge() throws Exception { - OperationParameters params = new OperationParameters(); + @ParameterizedTest + @ValueSource(booleans = {true, false}) + public void testMerge(boolean withDelete) throws Exception { + PatientMergeInputParameters params = new PatientMergeInputParameters(); params.sourcePatient = new Reference().setReference(sourcePatId); params.targetPatient = new Reference().setReference(targetPatId); + params.deleteSource = withDelete; IGenericClient client = myFhirContext.newRestfulGenericClient(myServerBase); Parameters outParams = client.operation() @@ -146,8 +150,9 @@ public void testMerge() throws Exception { ourLog.info("Found IDs: {}", actual); - // FIXME KHS parameterize this based on delete-source=true -// assertThat(actual).doesNotContain(sourcePatId); + if (withDelete) { + assertThat(actual).doesNotContain(sourcePatId); + } assertThat(actual).contains(encId1); assertThat(actual).contains(encId2); assertThat(actual).contains(orgId); @@ -192,13 +197,14 @@ private Bundle fetchBundle(String theUrl, EncodingEnum theEncoding) throws IOExc - private static class OperationParameters { + private static class PatientMergeInputParameters { Type sourcePatient; Type sourcePatientIdentifier; Type targetPatient; Type targetPatientIdentifier; Patient resultResource; Boolean preview; + Boolean deleteSource; public Parameters asParametersResource() { Parameters inParams = new Parameters(); @@ -220,6 +226,9 @@ public Parameters asParametersResource() { if (preview != null) { inParams.addParameter().setName("preview").setValue(new BooleanType(preview)); } + if (deleteSource != null) { + inParams.addParameter().setName("delete-source").setValue(new BooleanType(deleteSource)); + } return inParams; } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index fadeb2074d28..19eae4629d7a 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -40,6 +40,8 @@ import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; @@ -51,6 +53,7 @@ import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_500_INTERNAL_ERROR; public class ResourceMergeService { + private static final Logger ourLog = LoggerFactory.getLogger(ResourceMergeService.class); IFhirResourceDaoPatient myDao; FhirContext myFhirContext; @@ -77,6 +80,7 @@ public MergeOutcome merge(MergeOperationParameters theMergeOperationParameters, try { doMerge(theMergeOperationParameters, theRequestDetails, mergeOutcome); } catch (Exception e) { + ourLog.error("Resource merge failed", e); if (e instanceof BaseServerResponseException) { mergeOutcome.setHttpStatusCode(((BaseServerResponseException) e).getStatusCode()); } else { From eee9856978a651d808964f63a99155b93f0d56b1 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Tue, 3 Dec 2024 13:02:18 -0500 Subject: [PATCH 009/148] return target patient as output parameter, do not add replacedby link to target if src is to be deleted --- .../BaseJpaResourceProviderPatient.java | 20 +- .../jpa/dao/merge/MergeOperationOutcome.java | 34 ++++ .../jpa/dao/merge/ResourceMergeService.java | 126 ++++++------- .../dao/merge/ResourceMergeServiceTest.java | 173 +++++++++++++----- 4 files changed, 229 insertions(+), 124 deletions(-) create mode 100644 hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index d76a65544a0b..02a94e500bbe 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.dao.PatientEverythingParameters; +import ca.uhn.fhir.jpa.dao.merge.MergeOperationOutcome; import ca.uhn.fhir.jpa.dao.merge.MergeOperationParameters; import ca.uhn.fhir.jpa.dao.merge.PatientMergeOperationParameters; import ca.uhn.fhir.jpa.dao.merge.ResourceMergeService; @@ -297,20 +298,29 @@ public IBaseParameters patientMerge( FhirContext fhirContext = dao.getContext(); - ResourceMergeService.MergeOutcome mergeOutcome = + MergeOperationOutcome mergeOutcome = resourceMergeService.merge(mergeOperationParameters, theRequestDetails); - IBaseParameters retVal = ParametersUtil.newInstance(fhirContext); - ParametersUtil.addParameterToParameters(fhirContext, retVal, "outcome", mergeOutcome.getOperationOutcome()); - theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); - + IBaseParameters retVal = ParametersUtil.newInstance(fhirContext); + addMergeOutcomeToOutputParameters(retVal, fhirContext, mergeOutcome); return retVal; } finally { endRequest(theServletRequest); } } + private void addMergeOutcomeToOutputParameters( + IBaseParameters theParameters, FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome) { + + ParametersUtil.addParameterToParameters( + theFhirContext, theParameters, "outcome", theMergeOutcome.getOperationOutcome()); + if (theMergeOutcome.getUpdatedTargetResource() != null) { + ParametersUtil.addParameterToParameters( + theFhirContext, theParameters, "result", theMergeOutcome.getUpdatedTargetResource()); + } + } + private MergeOperationParameters createMergeOperationParameters( List theSourcePatientIdentifier, List theTargetPatientIdentifier, diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java new file mode 100644 index 000000000000..53e1bcaa92d9 --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java @@ -0,0 +1,34 @@ +package ca.uhn.fhir.jpa.dao.merge; + +import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +import org.hl7.fhir.instance.model.api.IBaseResource; + +public class MergeOperationOutcome { + private IBaseOperationOutcome myOperationOutcome; + private int myHttpStatusCode; + private IBaseResource myUpdatedTargetResource; + + public IBaseOperationOutcome getOperationOutcome() { + return myOperationOutcome; + } + + public void setOperationOutcome(IBaseOperationOutcome theOperationOutcome) { + this.myOperationOutcome = theOperationOutcome; + } + + public int getHttpStatusCode() { + return myHttpStatusCode; + } + + public void setHttpStatusCode(int theHttpStatusCode) { + this.myHttpStatusCode = theHttpStatusCode; + } + + public IBaseResource getUpdatedTargetResource() { + return myUpdatedTargetResource; + } + + public void setUpdatedTargetResource(IBaseResource theUpdatedTargetResource) { + this.myUpdatedTargetResource = theUpdatedTargetResource; + } +} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 19eae4629d7a..1a821e65f574 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -21,6 +21,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -31,7 +32,7 @@ import ca.uhn.fhir.util.CanonicalIdentifier; import ca.uhn.fhir.util.IdentifierUtil; import ca.uhn.fhir.util.OperationOutcomeUtil; -import com.google.common.annotations.VisibleForTesting; +import jakarta.annotation.Nullable; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -55,8 +56,8 @@ public class ResourceMergeService { private static final Logger ourLog = LoggerFactory.getLogger(ResourceMergeService.class); - IFhirResourceDaoPatient myDao; - FhirContext myFhirContext; + private IFhirResourceDaoPatient myDao; + private FhirContext myFhirContext; public ResourceMergeService(IFhirResourceDaoPatient thePatientDao) { myDao = thePatientDao; @@ -69,9 +70,10 @@ public ResourceMergeService(IFhirResourceDaoPatient thePatientDao) { * @param theRequestDetails the request details * @return the merge outcome containing OperationOutcome and HTTP status code */ - public MergeOutcome merge(MergeOperationParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + public MergeOperationOutcome merge( + MergeOperationParameters theMergeOperationParameters, RequestDetails theRequestDetails) { - MergeOutcome mergeOutcome = new MergeOutcome(); + MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); mergeOutcome.setOperationOutcome(operationOutcome); // default to 200 OK, would be changed to another code during processing as required @@ -94,7 +96,7 @@ public MergeOutcome merge(MergeOperationParameters theMergeOperationParameters, private void doMerge( MergeOperationParameters theMergeOperationParameters, RequestDetails theRequestDetails, - MergeOutcome theMergeOutcome) { + MergeOperationOutcome theMergeOutcome) { IBaseOperationOutcome operationOutcome = theMergeOutcome.getOperationOutcome(); @@ -140,25 +142,23 @@ private void doMerge( // TODO Emre: do the actual ref updates - // update resources after the refs update is completed - if (theMergeOperationParameters.getResultResource() != null) { - Patient resultResource = (Patient) theMergeOperationParameters.getResultResource(); - updateTargetResourceBasedOnResultResource(resultResource, theRequestDetails); - } else { - updateTargetResourceAfterRefsUpdated(targetResource, sourceResource, theRequestDetails); - } - if (theMergeOperationParameters.getDeleteSource()) { - deleteSourceResourceAfterRefsUpdated(sourceResource, theRequestDetails); + deleteResource(sourceResource, theRequestDetails); } else { updateSourceResourceAfterRefsUpdated(sourceResource, targetResource, theRequestDetails); } - addInfoToOperationOutcome(operationOutcome, "Merge operation completed successfully."); - } + // update the target patient resource after the references are updated + Patient resultResource = (Patient) theMergeOperationParameters.getResultResource(); + Patient targetPatientAfterUpdate = updateTargetResourceAfterRefsUpdated( + targetResource, + sourceResource, + resultResource, + theRequestDetails, + theMergeOperationParameters.getDeleteSource()); + theMergeOutcome.setUpdatedTargetResource(targetPatientAfterUpdate); - private void updateTargetResourceBasedOnResultResource(Patient resultResource, RequestDetails theRequestDetails) { - myDao.update(resultResource, theRequestDetails); + addInfoToOperationOutcome(operationOutcome, "Merge operation completed successfully."); } private boolean validateResultResourceIfExists( @@ -198,11 +198,14 @@ private boolean validateResultResourceIfExists( isValid = false; } - if (!validateResultResourceHasReplacesLinkToSourceResource( - theResultResource, - theResolvedSourceResource, - theMergeOperationParameters.getResultResourceParameterName(), - theOperationOutcome)) { + // if the source resource is not being deleted, the result resource must have a replaces link to the source + // resource + if (!theMergeOperationParameters.getDeleteSource() + && !validateResultResourceHasReplacesLinkToSourceResource( + theResultResource, + theResolvedSourceResource, + theMergeOperationParameters.getResultResourceParameterName(), + theOperationOutcome)) { isValid = false; } @@ -270,21 +273,6 @@ protected List getLinksOfType(Patient theResource, Patient.LinkType t return links; } - @VisibleForTesting - protected boolean hasReplacedByLink(Patient theResource) { - if (theResource.hasLink()) { - for (Patient.PatientLinkComponent link : theResource.getLink()) { - if (Patient.LinkType.REPLACEDBY.equals(link.getType())) { - if (link.hasOther()) { - String otherReference = link.getOther().getReference(); - return otherReference != null; - } - } - } - } - return false; - } - private boolean validateSourceAndTargetAreMergable( Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { @@ -301,7 +289,8 @@ private boolean validateSourceAndTargetAreMergable( return false; } - if (hasReplacedByLink(theTargetResource)) { + List replacedByLinks = getLinksOfType(theTargetResource, Patient.LinkType.REPLACEDBY); + if (!replacedByLinks.isEmpty()) { String msg = "Target resource was previously replaced by another resource, it is not a suitable target " + "for merging."; addErrorToOperationOutcome(outcome, msg, "invalid"); @@ -312,19 +301,35 @@ private boolean validateSourceAndTargetAreMergable( return true; } - private void updateTargetResourceAfterRefsUpdated( - Patient theTargetResource, Patient theSourceResource, RequestDetails theRequestDetails) { - theTargetResource - .addLink() - .setType(Patient.LinkType.REPLACES) - .setOther(new Reference(theSourceResource.getId())); + private Patient updateTargetResourceAfterRefsUpdated( + Patient theTargetResource, + Patient theSourceResource, + @Nullable Patient theResultResource, + RequestDetails theRequestDetails, + boolean theDeleteSource) { + + // if the client provided a result resource as input then use it to update the target resource + if (theResultResource != null) { + DaoMethodOutcome outcome = myDao.update(theResultResource, theRequestDetails); + return (Patient) outcome.getResource(); + } + + // client did not provide a result resource, update the target resource, + // after adding the replaces link to the target resource, if the source resource is not to be deleted + if (!theDeleteSource) { + // add the replaces link only if the source is not deleted + theTargetResource + .addLink() + .setType(Patient.LinkType.REPLACES) + .setOther(new Reference(theSourceResource.getId())); + } - myDao.update(theTargetResource, theRequestDetails); + DaoMethodOutcome outcome = myDao.update(theTargetResource, theRequestDetails); + return (Patient) outcome.getResource(); } - private void deleteSourceResourceAfterRefsUpdated(Patient theSourceResource, RequestDetails theRequestDetails) { - // TODO: handle errors - myDao.delete(theSourceResource.getIdElement(), theRequestDetails); + private void deleteResource(Patient theResource, RequestDetails theRequestDetails) { + myDao.delete(theResource.getIdElement(), theRequestDetails); } private void updateSourceResourceAfterRefsUpdated( @@ -527,25 +532,4 @@ private void addInfoToOperationOutcome(IBaseOperationOutcome theOutcome, String private void addErrorToOperationOutcome(IBaseOperationOutcome theOutcome, String theMsg, String theCode) { OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "error", theMsg, null, theCode); } - - public static class MergeOutcome { - private IBaseOperationOutcome myOperationOutcome; - private int myHttpStatusCode; - - public IBaseOperationOutcome getOperationOutcome() { - return myOperationOutcome; - } - - public void setOperationOutcome(IBaseOperationOutcome theOperationOutcome) { - this.myOperationOutcome = theOperationOutcome; - } - - public int getHttpStatusCode() { - return myHttpStatusCode; - } - - public void setHttpStatusCode(int theHttpStatusCode) { - this.myHttpStatusCode = theHttpStatusCode; - } - } } diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index 1339e5a9cd16..3c80de91a6d1 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -2,6 +2,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -22,7 +23,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.OngoingStubbing; - import java.util.Collections; import java.util.List; @@ -48,11 +48,16 @@ public class ResourceMergeServiceTest { "Target resource must be provided either by 'target-patient' or by 'target-patient-identifier', not both."; private static final String SUCCESSFUL_MERGE_MSG = "Merge operation completed successfully"; + private static final String SRC_PATIENT_TEST_ID = "Patient/123"; + private static final String TGT_PATIENT_TEST_ID = "Patient/456"; + @Mock private IFhirResourceDaoPatient myDaoMock; @Mock RequestDetails myRequestDetailsMock; + + private ResourceMergeService myResourceMergeService; private final FhirContext myFhirContext = FhirContext.forR4Cached(); @@ -63,6 +68,70 @@ void setup() { myResourceMergeService = new ResourceMergeService(myDaoMock); } + + + // SUCCESS CASES + @Test + void testMerge_WithoutResultResource_Success() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference(SRC_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TGT_PATIENT_TEST_ID)); + Patient sourcePatient = createPatient(SRC_PATIENT_TEST_ID); + Patient targetPatient = createPatient(TGT_PATIENT_TEST_ID); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + setupDaoMockForSuccessfulSourcePatientUpdate(sourcePatient, new Patient()); + Patient patientReturnedFromDaoAfterTargetUpdate = new Patient(); + setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientReturnedFromDaoAfterTargetUpdate); + + // When + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(200); + assertThat(mergeOutcome.getUpdatedTargetResource()).isEqualTo(patientReturnedFromDaoAfterTargetUpdate); + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(issue.getDiagnostics()).contains(SUCCESSFUL_MERGE_MSG); + + verifyNoMoreInteractions(myDaoMock); + } + + + @Test + void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersionAreTheSame_Success() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference("Patient/123/_history/2")); + mergeOperationParameters.setTargetResource(new Reference("Patient/345/_history/2")); + Patient sourcePatient = createPatient("Patient/123/_history/2"); + Patient targetPatient = createPatient("Patient/345/_history/2"); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + when(myDaoMock.update(any(), eq(myRequestDetailsMock))).thenReturn(new DaoMethodOutcome()); + + // When + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(200); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(issue.getDiagnostics()).contains(SUCCESSFUL_MERGE_MSG); + + verifyNoMoreInteractions(myDaoMock); + } + + // ERROR CASES + + @Test void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheException() { // Given @@ -74,7 +143,7 @@ void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheExcepti when(myDaoMock.read(any(), eq(myRequestDetailsMock))).thenThrow(ex); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -99,7 +168,7 @@ void testMerge_UnhandledExceptionThrown_Uses500StatusCode() { when(myDaoMock.read(any(), eq(myRequestDetailsMock))).thenThrow(ex); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -120,7 +189,7 @@ void testMerge_ValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorW mergeOperationParameters.setTargetResource(new Reference("Patient/123")); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -143,7 +212,7 @@ void testMerge_ValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorW mergeOperationParameters.setSourceResource(new Reference("Patient/123")); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -165,7 +234,7 @@ void testMerge_ValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -192,7 +261,7 @@ void testMerge_ValidatesInputParameters_BothSourceResourceParamsProvided_Returns mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -218,7 +287,7 @@ void testMerge_ValidatesInputParameters_BothTargetResourceParamsProvided_Returns mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); mergeOperationParameters.setSourceResource(new Reference("Patient/345")); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -243,7 +312,7 @@ void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWi when(myDaoMock.read(new IdType("Patient/123"), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -269,7 +338,7 @@ void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWi when(myDaoMock.read(new IdType("Patient/345"), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -293,7 +362,7 @@ void testMerge_ResolvesSourceResourceByIdentifierInReference_NoMatchFound_Return setupDaoMockSearchForIdentifiers(List.of(Collections.emptyList())); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then verifySearchParametersOnDaoSearchInvocations(List.of(List.of("sys|val"))); @@ -320,7 +389,7 @@ void testMerge_ResolvesTargetResourceByIdentifierInReference_NoMatchFound_Return setupDaoMockSearchForIdentifiers(List.of(Collections.emptyList())); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then verifySearchParametersOnDaoSearchInvocations(List.of(List.of("sys|val"))); @@ -349,7 +418,7 @@ void testMerge_ResolvesSourceResourceByIdentifiers_NoMatchFound_ReturnsErrorWith setupDaoMockSearchForIdentifiers(List.of(Collections.emptyList())); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then verifySearchParametersOnDaoSearchInvocations(List.of(List.of("sys|val1","sys|val2"))); @@ -380,7 +449,7 @@ void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith setupDaoMockForSuccessfulRead(sourcePatient); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then verifySearchParametersOnDaoSearchInvocations(List.of(List.of("sys|val1", "sys|val2"))); @@ -408,7 +477,7 @@ void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVe setupDaoMockForSuccessfulRead(sourcePatient); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -435,7 +504,7 @@ void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVe setupDaoMockForSuccessfulRead(targetPatient); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -453,32 +522,7 @@ void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVe - @Test - void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersionAreTheSame_NoErrorsReturned() { - // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); - mergeOperationParameters.setSourceResource(new Reference("Patient/123/_history/2")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345/_history/2")); - Patient sourcePatient = createPatient("Patient/123/_history/2"); - Patient targetPatient = createPatient("Patient/345/_history/2"); - setupDaoMockForSuccessfulRead(sourcePatient); - setupDaoMockForSuccessfulRead(targetPatient); - // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); - - // Then - OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); - assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(200); - - assertThat(operationOutcome.getIssue()).hasSize(1); - OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); - assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - assertThat(issue.getDiagnostics()).contains(SUCCESSFUL_MERGE_MSG); - - //TODO: enable this - //verifyNoMoreInteractions(myDaoMock); - } @Test void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status() { @@ -491,7 +535,7 @@ void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status() setupDaoMockSearchForIdentifiers(List.of(List.of(sourcePatient), List.of(targetPatient))); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then @@ -521,7 +565,7 @@ void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status() { setupDaoMockForSuccessfulRead(targetPatient); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -548,7 +592,7 @@ void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsError setupDaoMockForSuccessfulRead(targetPatient); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -578,7 +622,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetRes setupDaoMockForSuccessfulRead(targetPatient); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -614,7 +658,7 @@ void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersPr setupDaoMockSearchForIdentifiers(List.of(List.of(targetPatient))); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -644,7 +688,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLink_ReturnsEr setupDaoMockForSuccessfulRead(targetPatient); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -677,7 +721,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasRedundantReplacesLinksTo setupDaoMockForSuccessfulRead(targetPatient); // When - ResourceMergeService.MergeOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); @@ -734,6 +778,39 @@ private void setupDaoMockSearchForIdentifiers(List> theMatch } + private void setupDaoMockForSuccessfulSourcePatientUpdate(Patient thePatientExpectedAsInput, + Patient thePatientToReturnInDaoOutcome) { + DaoMethodOutcome daoMethodOutcome = new DaoMethodOutcome(); + daoMethodOutcome.setResource(thePatientToReturnInDaoOutcome); + when(myDaoMock.update(thePatientExpectedAsInput, myRequestDetailsMock)) + .thenAnswer(t -> { + Patient capturedSourcePatient = t.getArgument(0); + assertThat(capturedSourcePatient.getLink()).hasSize(1); + assertThat(capturedSourcePatient.getLinkFirstRep().getType()).isEqualTo(Patient.LinkType.REPLACEDBY); + assertThat(capturedSourcePatient.getLinkFirstRep().getOther().getReference()).isEqualTo(TGT_PATIENT_TEST_ID); + + DaoMethodOutcome outcome = new DaoMethodOutcome(); + outcome.setResource(thePatientToReturnInDaoOutcome); + return outcome; + }); + } + + private void setupDaoMockForSuccessfulTargetPatientUpdate(Patient thePatientExpectedAsInput, + Patient thePatientToReturnInDaoOutcome) { + DaoMethodOutcome daoMethodOutcome = new DaoMethodOutcome(); + daoMethodOutcome.setResource(thePatientToReturnInDaoOutcome); + when(myDaoMock.update(thePatientExpectedAsInput, myRequestDetailsMock)) + .thenAnswer(t -> { + Patient capturedTargetPatient = t.getArgument(0); + assertThat(capturedTargetPatient.getLink()).hasSize(1); + assertThat(capturedTargetPatient.getLinkFirstRep().getType()).isEqualTo(Patient.LinkType.REPLACES); + assertThat(capturedTargetPatient.getLinkFirstRep().getOther().getReference()).isEqualTo(SRC_PATIENT_TEST_ID); + + DaoMethodOutcome outcome = new DaoMethodOutcome(); + outcome.setResource(thePatientToReturnInDaoOutcome); + return outcome; + }); + } private void verifySearchParametersOnDaoSearchInvocations(List> theExpectedIdentifierParams) { ArgumentCaptor captor = ArgumentCaptor.forClass(SearchParameterMap.class); From 828931cb7c8bd163533c3b01463ab66d07d4c855 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 3 Dec 2024 13:44:51 -0500 Subject: [PATCH 010/148] output definition --- .../jpa/provider/r4/PatientMergeR4Test.java | 21 +++++++++++++------ .../server/provider/ProviderConstants.java | 3 +++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 7459e4b07c1a..e33e399ce7ce 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -18,6 +18,7 @@ import org.hl7.fhir.r4.model.Encounter.EncounterStatus; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; +import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; @@ -36,6 +37,9 @@ import java.util.TreeSet; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -124,20 +128,25 @@ public void before() throws Exception { @ParameterizedTest @ValueSource(booleans = {true, false}) public void testMerge(boolean withDelete) throws Exception { - PatientMergeInputParameters params = new PatientMergeInputParameters(); - params.sourcePatient = new Reference().setReference(sourcePatId); - params.targetPatient = new Reference().setReference(targetPatId); - params.deleteSource = withDelete; + PatientMergeInputParameters inParams = new PatientMergeInputParameters(); + inParams.sourcePatient = new Reference().setReference(sourcePatId); + inParams.targetPatient = new Reference().setReference(targetPatId); + inParams.deleteSource = withDelete; IGenericClient client = myFhirContext.newRestfulGenericClient(myServerBase); Parameters outParams = client.operation() .onType("Patient") .named(OPERATION_MERGE) - .withParameters(params.asParametersResource()) + .withParameters(inParams.asParametersResource()) .returnResourceType(Parameters.class) .execute(); - // FIXME KHS validate outParams + assertThat(outParams.getParameter()).hasSize(3); + Parameters input = (Parameters) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_INPUT).getResource(); + OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); + Patient mergedPatient = (Patient) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_RESULT).getResource(); + + // FIXME KHS assert on these three Bundle bundle = fetchBundle(myServerBase + "/" + targetPatId + "/$everything?_format=json&_count=100", EncodingEnum.JSON); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index 3c816daa8149..bfb3437d3e96 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -259,4 +259,7 @@ public class ProviderConstants { public static final String OPERATION_MERGE_RESULT_PATIENT = "result-patient"; public static final String OPERATION_MERGE_PREVIEW = "preview"; public static final String OPERATION_MERGE_DELETE_SOURCE = "delete-source"; + public static final String OPERATION_MERGE_OUTPUT_PARAM_INPUT = "input"; + public static final String OPERATION_MERGE_OUTPUT_PARAM_OUTCOME = "outcome"; + public static final String OPERATION_MERGE_OUTPUT_PARAM_RESULT = "result"; } From e34b9893a5c6dc82e01d5582cf87d70036b3b7c4 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Tue, 3 Dec 2024 16:34:06 -0500 Subject: [PATCH 011/148] ignore identifiers in ref, more unit tests, return target in preview mode --- .../jpa/dao/merge/ResourceMergeService.java | 135 +++++----- .../dao/merge/ResourceMergeServiceTest.java | 255 ++++++++++++++---- 2 files changed, 274 insertions(+), 116 deletions(-) diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 1a821e65f574..8b2c12ba7bf7 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -30,7 +30,6 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.CanonicalIdentifier; -import ca.uhn.fhir.util.IdentifierUtil; import ca.uhn.fhir.util.OperationOutcomeUtil; import jakarta.annotation.Nullable; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; @@ -56,8 +55,8 @@ public class ResourceMergeService { private static final Logger ourLog = LoggerFactory.getLogger(ResourceMergeService.class); - private IFhirResourceDaoPatient myDao; - private FhirContext myFhirContext; + private final IFhirResourceDaoPatient myDao; + private final FhirContext myFhirContext; public ResourceMergeService(IFhirResourceDaoPatient thePatientDao) { myDao = thePatientDao; @@ -66,8 +65,9 @@ public ResourceMergeService(IFhirResourceDaoPatient thePatientDao) { /** * Implementation of the $merge operation for resources - * @param theMergeOperationParameters the merge operation parameters - * @param theRequestDetails the request details + * + * @param theMergeOperationParameters the merge operation parameters + * @param theRequestDetails the request details * @return the merge outcome containing OperationOutcome and HTTP status code */ public MergeOperationOutcome merge( @@ -134,9 +134,14 @@ private void doMerge( return; } + Patient resultResource = (Patient) theMergeOperationParameters.getResultResource(); if (theMergeOperationParameters.getPreview()) { + // in preview mode, we should also return how the target would look like + Patient targetPatientAsIfUpdated = prepareTargetPatientForUpdate( + targetResource, sourceResource, resultResource, theMergeOperationParameters.getDeleteSource()); + theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); + addInfoToOperationOutcome(operationOutcome, "Preview only merge operation - no issues detected"); - // TODO we should also return the resulting target patient in the response return; } @@ -145,17 +150,14 @@ private void doMerge( if (theMergeOperationParameters.getDeleteSource()) { deleteResource(sourceResource, theRequestDetails); } else { - updateSourceResourceAfterRefsUpdated(sourceResource, targetResource, theRequestDetails); + prepareSourceResourceForUpdate(sourceResource, targetResource); + updateResource(sourceResource, theRequestDetails); } + Patient patientToUpdate = prepareTargetPatientForUpdate( + targetResource, sourceResource, resultResource, theMergeOperationParameters.getDeleteSource()); // update the target patient resource after the references are updated - Patient resultResource = (Patient) theMergeOperationParameters.getResultResource(); - Patient targetPatientAfterUpdate = updateTargetResourceAfterRefsUpdated( - targetResource, - sourceResource, - resultResource, - theRequestDetails, - theMergeOperationParameters.getDeleteSource()); + Patient targetPatientAfterUpdate = updateResource(patientToUpdate, theRequestDetails); theMergeOutcome.setUpdatedTargetResource(targetPatientAfterUpdate); addInfoToOperationOutcome(operationOutcome, "Merge operation completed successfully."); @@ -301,20 +303,26 @@ private boolean validateSourceAndTargetAreMergable( return true; } - private Patient updateTargetResourceAfterRefsUpdated( + private void prepareSourceResourceForUpdate(Patient theSourceResource, Patient theTargetResource) { + theSourceResource.setActive(false); + theSourceResource + .addLink() + .setType(Patient.LinkType.REPLACEDBY) + .setOther(new Reference(theTargetResource.getId())); + } + + private Patient prepareTargetPatientForUpdate( Patient theTargetResource, Patient theSourceResource, @Nullable Patient theResultResource, - RequestDetails theRequestDetails, boolean theDeleteSource) { // if the client provided a result resource as input then use it to update the target resource if (theResultResource != null) { - DaoMethodOutcome outcome = myDao.update(theResultResource, theRequestDetails); - return (Patient) outcome.getResource(); + return theResultResource; } - // client did not provide a result resource, update the target resource, + // client did not provide a result resource, we should update the target resource, // after adding the replaces link to the target resource, if the source resource is not to be deleted if (!theDeleteSource) { // add the replaces link only if the source is not deleted @@ -324,7 +332,11 @@ private Patient updateTargetResourceAfterRefsUpdated( .setOther(new Reference(theSourceResource.getId())); } - DaoMethodOutcome outcome = myDao.update(theTargetResource, theRequestDetails); + return theTargetResource; + } + + private Patient updateResource(Patient theResource, RequestDetails theRequestDetails) { + DaoMethodOutcome outcome = myDao.update(theResource, theRequestDetails); return (Patient) outcome.getResource(); } @@ -332,20 +344,11 @@ private void deleteResource(Patient theResource, RequestDetails theRequestDetail myDao.delete(theResource.getIdElement(), theRequestDetails); } - private void updateSourceResourceAfterRefsUpdated( - Patient theSourceResource, Patient theTargetResource, RequestDetails theRequestDetails) { - theSourceResource.setActive(false); - theSourceResource - .addLink() - .setType(Patient.LinkType.REPLACEDBY) - .setOther(new Reference(theTargetResource.getId())); - myDao.update(theSourceResource, theRequestDetails); - } - /** * Validates the merge operation parameters and adds validation errors to the outcome + * * @param theMergeOperationParameters the merge operation parameters - * @param theOutcome the outcome to add validation errors to + * @param theOutcome the outcome to add validation errors to * @return true if the parameters are valid, false otherwise */ private boolean validateMergeOperationParameters( @@ -389,6 +392,22 @@ private boolean validateMergeOperationParameters( errorMessages.add(msg); } + Reference sourceRef = (Reference) theMergeOperationParameters.getSourceResource(); + if (sourceRef != null && !sourceRef.hasReference()) { + String msg = String.format( + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getSourceResourceParameterName()); + errorMessages.add(msg); + } + + Reference targetRef = (Reference) theMergeOperationParameters.getTargetResource(); + if (targetRef != null && !targetRef.hasReference()) { + String msg = String.format( + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getTargetResourceParameterName()); + errorMessages.add(msg); + } + if (!errorMessages.isEmpty()) { for (String validationError : errorMessages) { addErrorToOperationOutcome(theOutcome, validationError, "required"); @@ -471,42 +490,30 @@ private IBaseResource resolveResourceByReference( // casting it to r4.Reference for now Reference r4ref = (Reference) theReference; - if (r4ref.hasReferenceElement()) { - - IIdType theResourceId = new IdType(r4ref.getReferenceElement().getValue()); - IBaseResource resource; - try { - resource = myDao.read(theResourceId.toVersionless(), theRequestDetails); - } catch (ResourceNotFoundException e) { - String msg = String.format( - "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); - addErrorToOperationOutcome(theOutcome, msg, "not-found"); - return null; - } - - if (theResourceId.hasVersionIdPart() - && !theResourceId - .getVersionIdPart() - .equals(resource.getIdElement().getVersionIdPart())) { - String msg = String.format( - "The reference in '%s' parameter has a version specified, " - + "but it is not the latest version of the resource", - theOperationParameterName); - addErrorToOperationOutcome(theOutcome, msg, "conflict"); - return null; - } - - return resource; + IIdType theResourceId = new IdType(r4ref.getReferenceElement().getValue()); + IBaseResource resource; + try { + resource = myDao.read(theResourceId.toVersionless(), theRequestDetails); + } catch (ResourceNotFoundException e) { + String msg = String.format( + "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); + addErrorToOperationOutcome(theOutcome, msg, "not-found"); + return null; } - // reference may have a identifier - if (r4ref.hasIdentifier()) { - Identifier identifier = r4ref.getIdentifier(); - CanonicalIdentifier canonicalIdentifier = IdentifierUtil.identifierDtFromIdentifier(identifier); - return resolveResourceByIdentifiers( - List.of(canonicalIdentifier), theRequestDetails, theOutcome, theOperationParameterName); + if (theResourceId.hasVersionIdPart() + && !theResourceId + .getVersionIdPart() + .equals(resource.getIdElement().getVersionIdPart())) { + String msg = String.format( + "The reference in '%s' parameter has a version specified, " + + "but it is not the latest version of the resource", + theOperationParameterName); + addErrorToOperationOutcome(theOutcome, msg, "conflict"); + return null; } - return null; + + return resource; } private IBaseResource resolveResource( diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index 3c80de91a6d1..eac15f6fd82e 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -12,7 +12,6 @@ import ca.uhn.fhir.util.CanonicalIdentifier; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; @@ -47,17 +46,14 @@ public class ResourceMergeServiceTest { private static final String BOTH_TARGET_PARAMS_PROVIDED_MSG = "Target resource must be provided either by 'target-patient' or by 'target-patient-identifier', not both."; private static final String SUCCESSFUL_MERGE_MSG = "Merge operation completed successfully"; - - private static final String SRC_PATIENT_TEST_ID = "Patient/123"; - private static final String TGT_PATIENT_TEST_ID = "Patient/456"; + private static final String SOURCE_PATIENT_TEST_ID = "Patient/123"; + private static final String TARGET_PATIENT_TEST_ID = "Patient/456"; @Mock private IFhirResourceDaoPatient myDaoMock; @Mock RequestDetails myRequestDetailsMock; - - private ResourceMergeService myResourceMergeService; private final FhirContext myFhirContext = FhirContext.forR4Cached(); @@ -68,23 +64,21 @@ void setup() { myResourceMergeService = new ResourceMergeService(myDaoMock); } - - // SUCCESS CASES @Test void testMerge_WithoutResultResource_Success() { // Given MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); - mergeOperationParameters.setSourceResource(new Reference(SRC_PATIENT_TEST_ID)); - mergeOperationParameters.setTargetResource(new Reference(TGT_PATIENT_TEST_ID)); - Patient sourcePatient = createPatient(SRC_PATIENT_TEST_ID); - Patient targetPatient = createPatient(TGT_PATIENT_TEST_ID); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); setupDaoMockForSuccessfulSourcePatientUpdate(sourcePatient, new Patient()); Patient patientReturnedFromDaoAfterTargetUpdate = new Patient(); - setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientReturnedFromDaoAfterTargetUpdate); + setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientReturnedFromDaoAfterTargetUpdate, true); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -101,6 +95,97 @@ void testMerge_WithoutResultResource_Success() { verifyNoMoreInteractions(myDaoMock); } + @Test + void testMerge_WithResultResource_Success() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); + resultPatient.addLink().setType(Patient.LinkType.REPLACES).setOther(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setResultResource(resultPatient); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + setupDaoMockForSuccessfulSourcePatientUpdate(sourcePatient, new Patient()); + Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); + setupDaoMockForSuccessfulTargetPatientUpdate(resultPatient, patientToBeReturnedFromDaoAfterTargetUpdate, true); + + // When + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(200); + assertThat(mergeOutcome.getUpdatedTargetResource()).isEqualTo(patientToBeReturnedFromDaoAfterTargetUpdate); + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(issue.getDiagnostics()).contains(SUCCESSFUL_MERGE_MSG); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_WithDeleteSourceTrue_Success() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setDeleteSource(true); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + when(myDaoMock.delete(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenReturn(new DaoMethodOutcome()); + Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); + setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientToBeReturnedFromDaoAfterTargetUpdate, false); + + // When + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(200); + assertThat(mergeOutcome.getUpdatedTargetResource()).isEqualTo(patientToBeReturnedFromDaoAfterTargetUpdate); + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(issue.getDiagnostics()).contains(SUCCESSFUL_MERGE_MSG); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_WithPreviewTrue_Success() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setPreview(true); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + // When + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(200); + assertThat(mergeOutcome.getUpdatedTargetResource()).isEqualTo(targetPatient); + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(issue.getDiagnostics()).contains("Preview only merge operation - no issues detected"); + + verifyNoMoreInteractions(myDaoMock); + } @Test void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersionAreTheSame_Success() { @@ -131,7 +216,6 @@ void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersio // ERROR CASES - @Test void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheException() { // Given @@ -254,7 +338,7 @@ void testMerge_ValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ } @Test - void testMerge_ValidatesInputParameters_BothSourceResourceParamsProvided_ReturnsErrorWith400Status() { + void testMerge_ValidatesInputParameters_BothSourceResourceAndSourceIdentifierParamsProvided_ReturnsErrorWith400Status() { // Given MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); @@ -280,7 +364,7 @@ void testMerge_ValidatesInputParameters_BothSourceResourceParamsProvided_Returns @Test - void testMerge_ValidatesInputParameters_BothTargetResourceParamsProvided_ReturnsErrorWith400Status() { + void testMerge_ValidatesInputParameters_BothTargetResourceAndTargetIdentifiersParamsProvided_ReturnsErrorWith400Status() { // Given MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setTargetResource(new Reference("Patient/123")); @@ -303,105 +387,102 @@ void testMerge_ValidatesInputParameters_BothTargetResourceParamsProvided_Returns verifyNoMoreInteractions(myDaoMock); } + @Test - void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWith422Status() { + void testMerge_ValidatesInputParameters_SourceResourceParamHasNoReferenceElement_ReturnsErrorWith400Status() { // Given MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345")); - when(myDaoMock.read(new IdType("Patient/123"), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); + mergeOperationParameters.setSourceResource(new Reference()); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); - assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); - assertThat(issue.getDiagnostics()).contains("Resource not found for the reference specified in 'source-patient'"); - assertThat(issue.getCode().toCode()).isEqualTo("not-found"); + assertThat(issue.getDiagnostics()).contains("Reference specified in 'source-patient' parameter does not have a reference element."); + assertThat(issue.getCode().toCode()).isEqualTo("required"); verifyNoMoreInteractions(myDaoMock); } + @Test - void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWith422Status() { + void testMerge_ValidatesInputParameters_TargetResourceParamHasNoReferenceElement_ReturnsErrorWith400Status() { // Given MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345")); - Patient sourcePatient = createPatient("Patient/123"); - setupDaoMockForSuccessfulRead(sourcePatient); - when(myDaoMock.read(new IdType("Patient/345"), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference()); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); - assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); - assertThat(issue.getDiagnostics()).contains("Resource not found for the reference specified in 'target-patient'"); - assertThat(issue.getCode().toCode()).isEqualTo("not-found"); + assertThat(issue.getDiagnostics()).contains("Reference specified in 'target-patient' parameter does not have " + + "a reference element."); + assertThat(issue.getCode().toCode()).isEqualTo("required"); verifyNoMoreInteractions(myDaoMock); } @Test - void testMerge_ResolvesSourceResourceByIdentifierInReference_NoMatchFound_ReturnsErrorWith422Status() { + void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWith422Status() { // Given MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); - mergeOperationParameters.setSourceResource(new Reference().setIdentifier(new Identifier().setSystem("sys").setValue("val"))); + mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); - setupDaoMockSearchForIdentifiers(List.of(Collections.emptyList())); + when(myDaoMock.read(new IdType("Patient/123"), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then - verifySearchParametersOnDaoSearchInvocations(List.of(List.of("sys|val"))); - OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); assertThat(operationOutcome.getIssue()).hasSize(1); OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); - assertThat(issue.getDiagnostics()).contains("No resources found matching the identifier(s) specified in 'source-patient'"); + assertThat(issue.getDiagnostics()).contains("Resource not found for the reference specified in 'source-patient'"); assertThat(issue.getCode().toCode()).isEqualTo("not-found"); verifyNoMoreInteractions(myDaoMock); } @Test - void testMerge_ResolvesTargetResourceByIdentifierInReference_NoMatchFound_ReturnsErrorWith422Status() { + void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWith422Status() { // Given MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); - mergeOperationParameters.setTargetResource(new Reference().setIdentifier(new Identifier().setSystem("sys").setValue("val"))); - setupDaoMockForSuccessfulRead(createPatient("Patient/123")); - setupDaoMockSearchForIdentifiers(List.of(Collections.emptyList())); + mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + Patient sourcePatient = createPatient("Patient/123"); + setupDaoMockForSuccessfulRead(sourcePatient); + when(myDaoMock.read(new IdType("Patient/345"), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then - verifySearchParametersOnDaoSearchInvocations(List.of(List.of("sys|val"))); - OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); assertThat(operationOutcome.getIssue()).hasSize(1); OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); - assertThat(issue.getDiagnostics()).contains("No resources found matching the identifier(s) specified in " + - "'target-patient'"); + assertThat(issue.getDiagnostics()).contains("Resource not found for the reference specified in 'target-patient'"); assertThat(issue.getCode().toCode()).isEqualTo("not-found"); verifyNoMoreInteractions(myDaoMock); @@ -436,6 +517,38 @@ void testMerge_ResolvesSourceResourceByIdentifiers_NoMatchFound_ReturnsErrorWith } + @Test + void testMerge_ResolvesSourceResourceByIdentifiers_MultipleMatchesFound_ReturnsErrorWith422Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + setupDaoMockSearchForIdentifiers(List.of( + List.of( + createPatient("Patient/match-1"), + createPatient("Patient/match-2")) + )); + + // When + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + verifySearchParametersOnDaoSearchInvocations(List.of(List.of("sys|val1"))); + + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("Multiple resources found matching the identifier(s) specified in" + + " 'source-patient-identifier'"); + assertThat(issue.getCode().toCode()).isEqualTo("multiple-matches"); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith422Status() { // Given @@ -467,6 +580,39 @@ void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith verifyNoMoreInteractions(myDaoMock); } + @Test + void testMerge_ResolvesTargetResourceByIdentifiers_MultipleMatchesFound_ReturnsErrorWith422Status() { + // Given + MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); + setupDaoMockSearchForIdentifiers(List.of( + List.of( + createPatient("Patient/match-1"), + createPatient("Patient/match-2")) + )); + + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + setupDaoMockForSuccessfulRead(sourcePatient); + + // When + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + verifySearchParametersOnDaoSearchInvocations(List.of(List.of("sys|val1"))); + + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("Multiple resources found matching the identifier(s) specified in 'target-patient-identifier'"); + assertThat(issue.getCode().toCode()).isEqualTo("multiple-matches"); + + verifyNoMoreInteractions(myDaoMock); + } + @Test void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status() { // Given @@ -787,7 +933,7 @@ private void setupDaoMockForSuccessfulSourcePatientUpdate(Patient thePatientExpe Patient capturedSourcePatient = t.getArgument(0); assertThat(capturedSourcePatient.getLink()).hasSize(1); assertThat(capturedSourcePatient.getLinkFirstRep().getType()).isEqualTo(Patient.LinkType.REPLACEDBY); - assertThat(capturedSourcePatient.getLinkFirstRep().getOther().getReference()).isEqualTo(TGT_PATIENT_TEST_ID); + assertThat(capturedSourcePatient.getLinkFirstRep().getOther().getReference()).isEqualTo(TARGET_PATIENT_TEST_ID); DaoMethodOutcome outcome = new DaoMethodOutcome(); outcome.setResource(thePatientToReturnInDaoOutcome); @@ -796,16 +942,21 @@ private void setupDaoMockForSuccessfulSourcePatientUpdate(Patient thePatientExpe } private void setupDaoMockForSuccessfulTargetPatientUpdate(Patient thePatientExpectedAsInput, - Patient thePatientToReturnInDaoOutcome) { + Patient thePatientToReturnInDaoOutcome, + boolean theExpectLinkToSourcePatient) { DaoMethodOutcome daoMethodOutcome = new DaoMethodOutcome(); daoMethodOutcome.setResource(thePatientToReturnInDaoOutcome); when(myDaoMock.update(thePatientExpectedAsInput, myRequestDetailsMock)) .thenAnswer(t -> { Patient capturedTargetPatient = t.getArgument(0); - assertThat(capturedTargetPatient.getLink()).hasSize(1); - assertThat(capturedTargetPatient.getLinkFirstRep().getType()).isEqualTo(Patient.LinkType.REPLACES); - assertThat(capturedTargetPatient.getLinkFirstRep().getOther().getReference()).isEqualTo(SRC_PATIENT_TEST_ID); - + if (theExpectLinkToSourcePatient) { + assertThat(capturedTargetPatient.getLink()).hasSize(1); + assertThat(capturedTargetPatient.getLinkFirstRep().getType()).isEqualTo(Patient.LinkType.REPLACES); + assertThat(capturedTargetPatient.getLinkFirstRep().getOther().getReference()).isEqualTo(SOURCE_PATIENT_TEST_ID); + } + else { + assertThat(capturedTargetPatient.getLink()).isEmpty(); + } DaoMethodOutcome outcome = new DaoMethodOutcome(); outcome.setResource(thePatientToReturnInDaoOutcome); return outcome; From e0328b0ab96be52f6f8971ee58eac3433d2cf08d Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 3 Dec 2024 17:15:01 -0500 Subject: [PATCH 012/148] output definition --- .../jpa/provider/r4/PatientMergeR4Test.java | 63 ++++++++++++++----- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index e33e399ce7ce..c281cc1e20ad 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -8,6 +8,7 @@ import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import com.google.common.base.Charsets; +import jakarta.persistence.Id; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -16,6 +17,7 @@ import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Encounter.EncounterStatus; +import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; import org.hl7.fhir.r4.model.OperationOutcome; @@ -33,6 +35,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.TreeSet; @@ -44,18 +47,24 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; public class PatientMergeR4Test extends BaseResourceProviderR4Test { - - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PatientMergeR4Test.class); - private String orgId; - private String sourcePatId; - private String taskId; - private String encId1; - private String encId2; - private ArrayList myObsIds; - private String targetPatId; - private String targetEnc1; + static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PatientMergeR4Test.class); + + static final Identifier pat1IdentifierA = new Identifier().setSystem("SYS1A").setValue("VAL1A"); + static final Identifier pat1IdentifierB = new Identifier().setSystem("SYS1B").setValue("VAL1B"); + static final Identifier pat2IdentifierA = new Identifier().setSystem("SYS2A").setValue("VAL2A"); + static final Identifier pat2IdentifierB = new Identifier().setSystem("SYS2B").setValue("VAL2B"); + + String orgId; + String sourcePatId; + String taskId; + String encId1; + String encId2; + ArrayList myObsIds; + String targetPatId; + String targetEnc1; @BeforeEach public void beforeDisableResultReuse() { @@ -83,11 +92,15 @@ public void before() throws Exception { orgId = myClient.create().resource(org).execute().getId().toUnqualifiedVersionless().getValue(); ourLog.info("OrgId: {}", orgId); - Patient patient = new Patient(); - patient.getManagingOrganization().setReference(orgId); - sourcePatId = myClient.create().resource(patient).execute().getId().toUnqualifiedVersionless().getValue(); + Patient patient1 = new Patient(); + patient1.getManagingOrganization().setReference(orgId); + patient1.addIdentifier(pat1IdentifierA); + patient1.addIdentifier(pat1IdentifierB); + sourcePatId = myClient.create().resource(patient1).execute().getId().toUnqualifiedVersionless().getValue(); Patient patient2 = new Patient(); + patient2.addIdentifier(pat2IdentifierA); + patient2.addIdentifier(pat2IdentifierB); patient2.getManagingOrganization().setReference(orgId); targetPatId = myClient.create().resource(patient2).execute().getId().toUnqualifiedVersionless().getValue(); @@ -127,25 +140,41 @@ public void before() throws Exception { @ParameterizedTest @ValueSource(booleans = {true, false}) - public void testMerge(boolean withDelete) throws Exception { + public void testMergeWithoutResult(boolean withDelete) throws Exception { PatientMergeInputParameters inParams = new PatientMergeInputParameters(); inParams.sourcePatient = new Reference().setReference(sourcePatId); inParams.targetPatient = new Reference().setReference(targetPatId); inParams.deleteSource = withDelete; IGenericClient client = myFhirContext.newRestfulGenericClient(myServerBase); + Parameters inParameters = inParams.asParametersResource(); Parameters outParams = client.operation() .onType("Patient") .named(OPERATION_MERGE) - .withParameters(inParams.asParametersResource()) + .withParameters(inParameters) .returnResourceType(Parameters.class) .execute(); - assertThat(outParams.getParameter()).hasSize(3); - Parameters input = (Parameters) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_INPUT).getResource(); OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); + List issues = outcome.getIssue(); + assertThat(issues).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = issues.get(0); + assertEquals("Merge operation completed successfully.", issue.getDiagnostics()); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + Patient mergedPatient = (Patient) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_RESULT).getResource(); + // FIXME KHS change assert order once it stops failing here. + List identifiers = mergedPatient.getIdentifier(); + assertThat(identifiers).hasSize(4); + + // FIXME KHS assert on identifier contents + + assertThat(outParams.getParameter()).hasSize(3); + Parameters input = (Parameters) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_INPUT).getResource(); + + assertTrue(input.equalsDeep(inParameters)); + // FIXME KHS assert on these three Bundle bundle = fetchBundle(myServerBase + "/" + targetPatId + "/$everything?_format=json&_count=100", EncodingEnum.JSON); From 1e97309a3999f69874fc6208a75acca3f480171c Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Wed, 4 Dec 2024 09:24:27 -0500 Subject: [PATCH 013/148] return input parameters in the merge operation output --- .../BaseJpaResourceProviderPatient.java | 35 +++++++---- ...ava => MergeOperationInputParameters.java} | 2 +- ...PatientMergeOperationInputParameters.java} | 2 +- .../jpa/dao/merge/ResourceMergeService.java | 12 ++-- .../dao/merge/ResourceMergeServiceTest.java | 58 +++++++++---------- 5 files changed, 60 insertions(+), 49 deletions(-) rename hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/{MergeOperationParameters.java => MergeOperationInputParameters.java} (98%) rename hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/{PatientMergeOperationParameters.java => PatientMergeOperationInputParameters.java} (95%) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 02a94e500bbe..d63ff0ce04ad 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -22,9 +22,9 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.dao.PatientEverythingParameters; +import ca.uhn.fhir.jpa.dao.merge.MergeOperationInputParameters; import ca.uhn.fhir.jpa.dao.merge.MergeOperationOutcome; -import ca.uhn.fhir.jpa.dao.merge.MergeOperationParameters; -import ca.uhn.fhir.jpa.dao.merge.PatientMergeOperationParameters; +import ca.uhn.fhir.jpa.dao.merge.PatientMergeOperationInputParameters; import ca.uhn.fhir.jpa.dao.merge.ResourceMergeService; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.model.api.annotation.Description; @@ -63,6 +63,7 @@ import java.util.List; import java.util.stream.Collectors; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; import static org.apache.commons.lang3.StringUtils.isNotBlank; public abstract class BaseJpaResourceProviderPatient extends BaseJpaResourceProvider { @@ -284,7 +285,7 @@ public IBaseParameters patientMerge( startRequest(theServletRequest); try { - MergeOperationParameters mergeOperationParameters = createMergeOperationParameters( + MergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( theSourcePatientIdentifier, theTargetPatientIdentifier, theSourcePatient, @@ -302,26 +303,36 @@ public IBaseParameters patientMerge( resourceMergeService.merge(mergeOperationParameters, theRequestDetails); theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); - IBaseParameters retVal = ParametersUtil.newInstance(fhirContext); - addMergeOutcomeToOutputParameters(retVal, fhirContext, mergeOutcome); - return retVal; + return buildMergeOperationOutputParameters(fhirContext, mergeOutcome, theRequestDetails.getResource()); } finally { endRequest(theServletRequest); } } - private void addMergeOutcomeToOutputParameters( - IBaseParameters theParameters, FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome) { + private IBaseParameters buildMergeOperationOutputParameters( + FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome, IBaseResource theInputParameters) { + IBaseParameters retVal = ParametersUtil.newInstance(theFhirContext); ParametersUtil.addParameterToParameters( - theFhirContext, theParameters, "outcome", theMergeOutcome.getOperationOutcome()); + theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT, theInputParameters); + + ParametersUtil.addParameterToParameters( + theFhirContext, + retVal, + ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME, + theMergeOutcome.getOperationOutcome()); + if (theMergeOutcome.getUpdatedTargetResource() != null) { ParametersUtil.addParameterToParameters( - theFhirContext, theParameters, "result", theMergeOutcome.getUpdatedTargetResource()); + theFhirContext, + retVal, + OPERATION_MERGE_OUTPUT_PARAM_RESULT, + theMergeOutcome.getUpdatedTargetResource()); } + return retVal; } - private MergeOperationParameters createMergeOperationParameters( + private MergeOperationInputParameters buildMergeOperationInputParameters( List theSourcePatientIdentifier, List theTargetPatientIdentifier, IBaseReference theSourcePatient, @@ -329,7 +340,7 @@ private MergeOperationParameters createMergeOperationParameters( IPrimitiveType thePreview, IPrimitiveType theDeleteSource, IBaseResource theResultPatient) { - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); if (theSourcePatientIdentifier != null) { List sourceResourceIdentifiers = theSourcePatientIdentifier.stream() .map(IdentifierUtil::identifierDtFromIdentifier) diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationInputParameters.java similarity index 98% rename from hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java rename to hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationInputParameters.java index 1346059ba4c0..342928bb6275 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationParameters.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationInputParameters.java @@ -25,7 +25,7 @@ import java.util.List; -public abstract class MergeOperationParameters { +public abstract class MergeOperationInputParameters { private List mySourceResourceIdentifiers; private List myTargetResourceIdentifiers; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationInputParameters.java similarity index 95% rename from hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java rename to hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationInputParameters.java index 35a245762e7f..557c8041201d 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationParameters.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationInputParameters.java @@ -25,7 +25,7 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER; -public class PatientMergeOperationParameters extends MergeOperationParameters { +public class PatientMergeOperationInputParameters extends MergeOperationInputParameters { @Override public String getSourceResourceParameterName() { return OPERATION_MERGE_SOURCE_PATIENT; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 8b2c12ba7bf7..b544b92d3c05 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -71,7 +71,7 @@ public ResourceMergeService(IFhirResourceDaoPatient thePatientDao) { * @return the merge outcome containing OperationOutcome and HTTP status code */ public MergeOperationOutcome merge( - MergeOperationParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + MergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); @@ -94,7 +94,7 @@ public MergeOperationOutcome merge( } private void doMerge( - MergeOperationParameters theMergeOperationParameters, + MergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails, MergeOperationOutcome theMergeOutcome) { @@ -164,7 +164,7 @@ private void doMerge( } private boolean validateResultResourceIfExists( - MergeOperationParameters theMergeOperationParameters, + MergeOperationInputParameters theMergeOperationParameters, Patient theResolvedTargetResource, Patient theResolvedSourceResource, IBaseOperationOutcome theOperationOutcome) { @@ -352,7 +352,7 @@ private void deleteResource(Patient theResource, RequestDetails theRequestDetail * @return true if the parameters are valid, false otherwise */ private boolean validateMergeOperationParameters( - MergeOperationParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { List errorMessages = new ArrayList<>(); if (!theMergeOperationParameters.hasAtLeastOneSourceIdentifier() && theMergeOperationParameters.getSourceResource() == null) { @@ -421,7 +421,7 @@ private boolean validateMergeOperationParameters( } private IBaseResource resolveSourceResource( - MergeOperationParameters theOperationParameters, + MergeOperationInputParameters theOperationParameters, RequestDetails theRequestDetails, IBaseOperationOutcome theOutcome) { return resolveResource( @@ -434,7 +434,7 @@ private IBaseResource resolveSourceResource( } private IBaseResource resolveTargetResource( - MergeOperationParameters theOperationParameters, + MergeOperationInputParameters theOperationParameters, RequestDetails theRequestDetails, IBaseOperationOutcome theOutcome) { return resolveResource( diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index eac15f6fd82e..df9dcbe5596a 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -68,7 +68,7 @@ void setup() { @Test void testMerge_WithoutResultResource_Success() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); @@ -98,7 +98,7 @@ void testMerge_WithoutResultResource_Success() { @Test void testMerge_WithResultResource_Success() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); @@ -132,7 +132,7 @@ void testMerge_WithResultResource_Success() { @Test void testMerge_WithDeleteSourceTrue_Success() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setDeleteSource(true); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -163,7 +163,7 @@ void testMerge_WithDeleteSourceTrue_Success() { @Test void testMerge_WithPreviewTrue_Success() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setPreview(true); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -190,7 +190,7 @@ void testMerge_WithPreviewTrue_Success() { @Test void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersionAreTheSame_Success() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123/_history/2")); mergeOperationParameters.setTargetResource(new Reference("Patient/345/_history/2")); Patient sourcePatient = createPatient("Patient/123/_history/2"); @@ -219,7 +219,7 @@ void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersio @Test void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheException() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); @@ -244,7 +244,7 @@ void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheExcepti @Test void testMerge_UnhandledExceptionThrown_Uses500StatusCode() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); @@ -269,7 +269,7 @@ void testMerge_UnhandledExceptionThrown_Uses500StatusCode() { @Test void testMerge_ValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setTargetResource(new Reference("Patient/123")); // When @@ -292,7 +292,7 @@ void testMerge_ValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorW @Test void testMerge_ValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); // When @@ -315,7 +315,7 @@ void testMerge_ValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorW @Test void testMerge_ValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ReturnsErrorsWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -340,7 +340,7 @@ void testMerge_ValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ @Test void testMerge_ValidatesInputParameters_BothSourceResourceAndSourceIdentifierParamsProvided_ReturnsErrorWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); @@ -366,7 +366,7 @@ void testMerge_ValidatesInputParameters_BothSourceResourceAndSourceIdentifierPar @Test void testMerge_ValidatesInputParameters_BothTargetResourceAndTargetIdentifiersParamsProvided_ReturnsErrorWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setTargetResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); mergeOperationParameters.setSourceResource(new Reference("Patient/345")); @@ -391,7 +391,7 @@ void testMerge_ValidatesInputParameters_BothTargetResourceAndTargetIdentifiersPa @Test void testMerge_ValidatesInputParameters_SourceResourceParamHasNoReferenceElement_ReturnsErrorWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference()); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -416,7 +416,7 @@ void testMerge_ValidatesInputParameters_SourceResourceParamHasNoReferenceElement @Test void testMerge_ValidatesInputParameters_TargetResourceParamHasNoReferenceElement_ReturnsErrorWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference()); @@ -441,7 +441,7 @@ void testMerge_ValidatesInputParameters_TargetResourceParamHasNoReferenceElement @Test void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWith422Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); when(myDaoMock.read(new IdType("Patient/123"), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); @@ -465,7 +465,7 @@ void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWi @Test void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWith422Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); Patient sourcePatient = createPatient("Patient/123"); @@ -491,7 +491,7 @@ void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWi @Test void testMerge_ResolvesSourceResourceByIdentifiers_NoMatchFound_ReturnsErrorWith422Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResourceIdentifiers(List.of( new CanonicalIdentifier().setSystem("sys").setValue("val1"), new CanonicalIdentifier().setSystem("sys").setValue("val2"))); @@ -520,7 +520,7 @@ void testMerge_ResolvesSourceResourceByIdentifiers_NoMatchFound_ReturnsErrorWith @Test void testMerge_ResolvesSourceResourceByIdentifiers_MultipleMatchesFound_ReturnsErrorWith422Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); setupDaoMockSearchForIdentifiers(List.of( @@ -552,7 +552,7 @@ void testMerge_ResolvesSourceResourceByIdentifiers_MultipleMatchesFound_ReturnsE @Test void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith422Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResourceIdentifiers(List.of( new CanonicalIdentifier().setSystem("sys").setValue("val1"), @@ -583,7 +583,7 @@ void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith @Test void testMerge_ResolvesTargetResourceByIdentifiers_MultipleMatchesFound_ReturnsErrorWith422Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); setupDaoMockSearchForIdentifiers(List.of( @@ -616,7 +616,7 @@ void testMerge_ResolvesTargetResourceByIdentifiers_MultipleMatchesFound_ReturnsE @Test void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123/_history/1")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); Patient sourcePatient = createPatient("Patient/123/_history/2"); @@ -641,7 +641,7 @@ void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVe @Test void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345/_history/1")); Patient sourcePatient = createPatient("Patient/123"); @@ -673,7 +673,7 @@ void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVe @Test void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val2"))); Patient sourcePatient = createPatient("Patient/123"); @@ -701,7 +701,7 @@ void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status() @Test void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); Patient sourcePatient = createPatient("Patient/123"); @@ -728,7 +728,7 @@ void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status() { @Test void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsErrorWith422Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); Patient sourcePatient = createPatient("Patient/123"); @@ -755,7 +755,7 @@ void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsError @Test void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetResource_ReturnsErrorWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); Patient resultPatient = createPatient("Patient/678"); @@ -786,7 +786,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetRes @Test void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersProvidedInTargetIdentifiers_ReturnsErrorWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResourceIdentifiers(List.of( new CanonicalIdentifier().setSystem("sys").setValue("val1"), @@ -822,7 +822,7 @@ void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersPr @Test void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLink_ReturnsErrorWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); @@ -851,7 +851,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLink_ReturnsEr @Test void testMerge_ValidatesResultResource_ResultResourceHasRedundantReplacesLinksToSource_ReturnsErrorWith400Status() { // Given - MergeOperationParameters mergeOperationParameters = new PatientMergeOperationParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); From 09f101d4a0fdcb63b5ee7b12df900fb9ffac194f Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Wed, 4 Dec 2024 10:50:51 -0500 Subject: [PATCH 014/148] copy identifiers from source to target (no duplicate check) --- .../jpa/dao/merge/ResourceMergeService.java | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index b544b92d3c05..bbca7de1218c 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -147,6 +147,12 @@ private void doMerge( // TODO Emre: do the actual ref updates + Patient patientToUpdate = prepareTargetPatientForUpdate( + targetResource, sourceResource, resultResource, theMergeOperationParameters.getDeleteSource()); + // update the target patient resource after the references are updated + Patient targetPatientAfterUpdate = updateResource(patientToUpdate, theRequestDetails); + theMergeOutcome.setUpdatedTargetResource(targetPatientAfterUpdate); + if (theMergeOperationParameters.getDeleteSource()) { deleteResource(sourceResource, theRequestDetails); } else { @@ -154,12 +160,6 @@ private void doMerge( updateResource(sourceResource, theRequestDetails); } - Patient patientToUpdate = prepareTargetPatientForUpdate( - targetResource, sourceResource, resultResource, theMergeOperationParameters.getDeleteSource()); - // update the target patient resource after the references are updated - Patient targetPatientAfterUpdate = updateResource(patientToUpdate, theRequestDetails); - theMergeOutcome.setUpdatedTargetResource(targetPatientAfterUpdate); - addInfoToOperationOutcome(operationOutcome, "Merge operation completed successfully."); } @@ -323,18 +323,25 @@ private Patient prepareTargetPatientForUpdate( } // client did not provide a result resource, we should update the target resource, - // after adding the replaces link to the target resource, if the source resource is not to be deleted + // add the replaces link to the target resource, if the source resource is not to be deleted if (!theDeleteSource) { - // add the replaces link only if the source is not deleted theTargetResource .addLink() .setType(Patient.LinkType.REPLACES) .setOther(new Reference(theSourceResource.getId())); } + // copy all identifiers from the source to the target + copyIdentifiers(theSourceResource, theTargetResource); + return theTargetResource; } + private void copyIdentifiers(Patient theSourceResource, Patient theTargetResource) { + // should we check if the target already has the identifier we are copying to not duplicate it? + theSourceResource.getIdentifier().forEach(theTargetResource::addIdentifier); + } + private Patient updateResource(Patient theResource, RequestDetails theRequestDetails) { DaoMethodOutcome outcome = myDao.update(theResource, theRequestDetails); return (Patient) outcome.getResource(); From 4439fa914d87658b39831afae8292554a2f308ff Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 4 Dec 2024 11:32:32 -0500 Subject: [PATCH 015/148] add shared identifier --- .../java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index c281cc1e20ad..bf5c689ac333 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -56,6 +56,7 @@ public class PatientMergeR4Test extends BaseResourceProviderR4Test { static final Identifier pat1IdentifierB = new Identifier().setSystem("SYS1B").setValue("VAL1B"); static final Identifier pat2IdentifierA = new Identifier().setSystem("SYS2A").setValue("VAL2A"); static final Identifier pat2IdentifierB = new Identifier().setSystem("SYS2B").setValue("VAL2B"); + static final Identifier patBothIdentifierC = new Identifier().setSystem("SYSC").setValue("VALC"); String orgId; String sourcePatId; @@ -96,11 +97,13 @@ public void before() throws Exception { patient1.getManagingOrganization().setReference(orgId); patient1.addIdentifier(pat1IdentifierA); patient1.addIdentifier(pat1IdentifierB); + patient1.addIdentifier(patBothIdentifierC); sourcePatId = myClient.create().resource(patient1).execute().getId().toUnqualifiedVersionless().getValue(); Patient patient2 = new Patient(); patient2.addIdentifier(pat2IdentifierA); patient2.addIdentifier(pat2IdentifierB); + patient1.addIdentifier(patBothIdentifierC); patient2.getManagingOrganization().setReference(orgId); targetPatId = myClient.create().resource(patient2).execute().getId().toUnqualifiedVersionless().getValue(); @@ -166,7 +169,7 @@ public void testMergeWithoutResult(boolean withDelete) throws Exception { // FIXME KHS change assert order once it stops failing here. List identifiers = mergedPatient.getIdentifier(); - assertThat(identifiers).hasSize(4); + assertThat(identifiers).hasSize(5); // FIXME KHS assert on identifier contents From a8b8486f38c1c365f147f7da720bdf5b6c02f4d5 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 4 Dec 2024 11:52:10 -0500 Subject: [PATCH 016/148] add shared identifier --- .../jpa/provider/r4/PatientMergeR4Test.java | 86 +++++++++++-------- 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index bf5c689ac333..8ca4e17c7284 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -12,6 +12,7 @@ import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; @@ -35,6 +36,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; @@ -58,14 +60,14 @@ public class PatientMergeR4Test extends BaseResourceProviderR4Test { static final Identifier pat2IdentifierB = new Identifier().setSystem("SYS2B").setValue("VAL2B"); static final Identifier patBothIdentifierC = new Identifier().setSystem("SYSC").setValue("VALC"); - String orgId; - String sourcePatId; - String taskId; - String encId1; - String encId2; - ArrayList myObsIds; - String targetPatId; - String targetEnc1; + IIdType orgId; + IIdType sourcePatId; + IIdType taskId; + IIdType encId1; + IIdType encId2; + ArrayList myObsIds; + IIdType targetPatId; + IIdType targetEnc1; @BeforeEach public void beforeDisableResultReuse() { @@ -90,52 +92,52 @@ public void before() throws Exception { Organization org = new Organization(); org.setName("an org"); - orgId = myClient.create().resource(org).execute().getId().toUnqualifiedVersionless().getValue(); + orgId = myClient.create().resource(org).execute().getId().toUnqualifiedVersionless(); ourLog.info("OrgId: {}", orgId); Patient patient1 = new Patient(); - patient1.getManagingOrganization().setReference(orgId); + patient1.getManagingOrganization().setReferenceElement(orgId); patient1.addIdentifier(pat1IdentifierA); patient1.addIdentifier(pat1IdentifierB); patient1.addIdentifier(patBothIdentifierC); - sourcePatId = myClient.create().resource(patient1).execute().getId().toUnqualifiedVersionless().getValue(); + sourcePatId = myClient.create().resource(patient1).execute().getId().toUnqualifiedVersionless(); Patient patient2 = new Patient(); patient2.addIdentifier(pat2IdentifierA); patient2.addIdentifier(pat2IdentifierB); patient1.addIdentifier(patBothIdentifierC); - patient2.getManagingOrganization().setReference(orgId); - targetPatId = myClient.create().resource(patient2).execute().getId().toUnqualifiedVersionless().getValue(); + patient2.getManagingOrganization().setReferenceElement(orgId); + targetPatId = myClient.create().resource(patient2).execute().getId().toUnqualifiedVersionless(); Encounter enc1 = new Encounter(); enc1.setStatus(EncounterStatus.CANCELLED); - enc1.getSubject().setReference(sourcePatId); - enc1.getServiceProvider().setReference(orgId); - encId1 = myClient.create().resource(enc1).execute().getId().toUnqualifiedVersionless().getValue(); + enc1.getSubject().setReferenceElement(sourcePatId); + enc1.getServiceProvider().setReferenceElement(orgId); + encId1 = myClient.create().resource(enc1).execute().getId().toUnqualifiedVersionless(); Encounter enc2 = new Encounter(); enc2.setStatus(EncounterStatus.ARRIVED); - enc2.getSubject().setReference(sourcePatId); - enc2.getServiceProvider().setReference(orgId); - encId2 = myClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless().getValue(); + enc2.getSubject().setReferenceElement(sourcePatId); + enc2.getServiceProvider().setReferenceElement(orgId); + encId2 = myClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless(); Task task = new Task(); task.setStatus(Task.TaskStatus.COMPLETED); - task.getOwner().setReference(sourcePatId); - taskId = myClient.create().resource(task).execute().getId().toUnqualifiedVersionless().getValue(); + task.getOwner().setReferenceElement(sourcePatId); + taskId = myClient.create().resource(task).execute().getId().toUnqualifiedVersionless(); Encounter targetEnc1 = new Encounter(); targetEnc1.setStatus(EncounterStatus.ARRIVED); - targetEnc1.getSubject().setReference(targetPatId); - targetEnc1.getServiceProvider().setReference(orgId); - this.targetEnc1 = myClient.create().resource(targetEnc1).execute().getId().toUnqualifiedVersionless().getValue(); + targetEnc1.getSubject().setReferenceElement(targetPatId); + targetEnc1.getServiceProvider().setReferenceElement(orgId); + this.targetEnc1 = myClient.create().resource(targetEnc1).execute().getId().toUnqualifiedVersionless(); myObsIds = new ArrayList<>(); for (int i = 0; i < 20; i++) { Observation obs = new Observation(); - obs.getSubject().setReference(sourcePatId); + obs.getSubject().setReferenceElement(sourcePatId); obs.setStatus(ObservationStatus.FINAL); - String obsId = myClient.create().resource(obs).execute().getId().toUnqualifiedVersionless().getValue(); + IIdType obsId = myClient.create().resource(obs).execute().getId().toUnqualifiedVersionless(); myObsIds.add(obsId); } @@ -145,8 +147,8 @@ public void before() throws Exception { @ValueSource(booleans = {true, false}) public void testMergeWithoutResult(boolean withDelete) throws Exception { PatientMergeInputParameters inParams = new PatientMergeInputParameters(); - inParams.sourcePatient = new Reference().setReference(sourcePatId); - inParams.targetPatient = new Reference().setReference(targetPatId); + inParams.sourcePatient = new Reference().setReferenceElement(sourcePatId); + inParams.targetPatient = new Reference().setReferenceElement(targetPatId); inParams.deleteSource = withDelete; IGenericClient client = myFhirContext.newRestfulGenericClient(myServerBase); @@ -158,6 +160,14 @@ public void testMergeWithoutResult(boolean withDelete) throws Exception { .returnResourceType(Parameters.class) .execute(); + assertThat(outParams.getParameter()).hasSize(3); + + // Assert income + Parameters input = (Parameters) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_INPUT).getResource(); + assertTrue(input.equalsDeep(inParameters)); + + + // Assert outcome OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); List issues = outcome.getIssue(); assertThat(issues).hasSize(1); @@ -165,18 +175,22 @@ public void testMergeWithoutResult(boolean withDelete) throws Exception { assertEquals("Merge operation completed successfully.", issue.getDiagnostics()); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + // Assert Merged Patient Patient mergedPatient = (Patient) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_RESULT).getResource(); - - // FIXME KHS change assert order once it stops failing here. List identifiers = mergedPatient.getIdentifier(); assertThat(identifiers).hasSize(5); // FIXME KHS assert on identifier contents - assertThat(outParams.getParameter()).hasSize(3); - Parameters input = (Parameters) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_INPUT).getResource(); + if (!withDelete) { + // assert source has link to target + Patient source = myPatientDao.read(sourcePatId, mySrd); + List links = source.getLink(); + assertThat(links).hasSize(1); + Patient.PatientLinkComponent link = links.get(0); + assertThat(link.getOther().getReferenceElement()).isEqualTo(targetPatId); + } - assertTrue(input.equalsDeep(inParameters)); // FIXME KHS assert on these three @@ -184,9 +198,9 @@ public void testMergeWithoutResult(boolean withDelete) throws Exception { assertNull(bundle.getLink("next")); - Set actual = new TreeSet<>(); + Set actual = new HashSet<>(); for (BundleEntryComponent nextEntry : bundle.getEntry()) { - actual.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless().getValue()); + actual.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless()); } ourLog.info("Found IDs: {}", actual); @@ -198,7 +212,7 @@ public void testMergeWithoutResult(boolean withDelete) throws Exception { assertThat(actual).contains(encId2); assertThat(actual).contains(orgId); assertThat(actual).contains(taskId); - assertThat(actual).contains(myObsIds.toArray(new String[0])); + assertThat(actual).containsAll(myObsIds); assertThat(actual).contains(targetPatId); assertThat(actual).contains(targetEnc1); } From 89da9102fd426e0162a22e188a6fcadf2a0de5b8 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 4 Dec 2024 11:59:33 -0500 Subject: [PATCH 017/148] moar asserts --- .../jpa/provider/r4/PatientMergeR4Test.java | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 8ca4e17c7284..5c59599cc7b6 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -169,26 +169,33 @@ public void testMergeWithoutResult(boolean withDelete) throws Exception { // Assert outcome OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); - List issues = outcome.getIssue(); - assertThat(issues).hasSize(1); - OperationOutcome.OperationOutcomeIssueComponent issue = issues.get(0); - assertEquals("Merge operation completed successfully.", issue.getDiagnostics()); - assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(outcome.getIssue()) + .hasSize(1) + .element(0) + .satisfies(issue -> { + assertThat(issue.getDiagnostics()).isEqualTo("Merge operation completed successfully."); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + }); // Assert Merged Patient Patient mergedPatient = (Patient) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_RESULT).getResource(); List identifiers = mergedPatient.getIdentifier(); assertThat(identifiers).hasSize(5); - - // FIXME KHS assert on identifier contents + assertThat(identifiers) + .extracting(Identifier::getSystem) + .containsExactlyInAnyOrder("SYS1A", "SYS1B", "SYS2A", "SYS2B", "SYSC"); + assertThat(identifiers) + .extracting(Identifier::getValue) + .containsExactlyInAnyOrder("VAL1A", "VAL1B", "VAL2A", "VAL2B", "VALC"); if (!withDelete) { // assert source has link to target Patient source = myPatientDao.read(sourcePatId, mySrd); - List links = source.getLink(); - assertThat(links).hasSize(1); - Patient.PatientLinkComponent link = links.get(0); - assertThat(link.getOther().getReferenceElement()).isEqualTo(targetPatId); + assertThat(source.getLink()) + .hasSize(1) + .element(0) + .extracting(link -> link.getOther().getReferenceElement()) + .isEqualTo(targetPatId); } From 10bbd5644fa690b4562b2755626bb9b34d40ceab Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 4 Dec 2024 12:52:49 -0500 Subject: [PATCH 018/148] moar asserts --- .../jpa/provider/r4/PatientMergeR4Test.java | 123 ++++++++++-------- 1 file changed, 69 insertions(+), 54 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 5c59599cc7b6..f4afbb90e808 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -8,7 +8,6 @@ import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import com.google.common.base.Charsets; -import jakarta.persistence.Id; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -32,14 +31,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.provider.CsvSource; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.TreeSet; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT; @@ -60,14 +58,15 @@ public class PatientMergeR4Test extends BaseResourceProviderR4Test { static final Identifier pat2IdentifierB = new Identifier().setSystem("SYS2B").setValue("VAL2B"); static final Identifier patBothIdentifierC = new Identifier().setSystem("SYSC").setValue("VALC"); - IIdType orgId; - IIdType sourcePatId; - IIdType taskId; - IIdType encId1; - IIdType encId2; + IIdType myOrgId; + IIdType mySourcePatId; + IIdType myTaskId; + IIdType myEncId1; + IIdType myEncId2; ArrayList myObsIds; - IIdType targetPatId; - IIdType targetEnc1; + IIdType myTargetPatId; + IIdType myTargetEnc1; + Patient myResultPatient; @BeforeEach public void beforeDisableResultReuse() { @@ -92,64 +91,75 @@ public void before() throws Exception { Organization org = new Organization(); org.setName("an org"); - orgId = myClient.create().resource(org).execute().getId().toUnqualifiedVersionless(); - ourLog.info("OrgId: {}", orgId); + myOrgId = myClient.create().resource(org).execute().getId().toUnqualifiedVersionless(); + ourLog.info("OrgId: {}", myOrgId); Patient patient1 = new Patient(); - patient1.getManagingOrganization().setReferenceElement(orgId); + patient1.getManagingOrganization().setReferenceElement(myOrgId); patient1.addIdentifier(pat1IdentifierA); patient1.addIdentifier(pat1IdentifierB); patient1.addIdentifier(patBothIdentifierC); - sourcePatId = myClient.create().resource(patient1).execute().getId().toUnqualifiedVersionless(); + mySourcePatId = myClient.create().resource(patient1).execute().getId().toUnqualifiedVersionless(); Patient patient2 = new Patient(); patient2.addIdentifier(pat2IdentifierA); patient2.addIdentifier(pat2IdentifierB); - patient1.addIdentifier(patBothIdentifierC); - patient2.getManagingOrganization().setReferenceElement(orgId); - targetPatId = myClient.create().resource(patient2).execute().getId().toUnqualifiedVersionless(); + patient2.addIdentifier(patBothIdentifierC); + patient2.getManagingOrganization().setReferenceElement(myOrgId); + myTargetPatId = myClient.create().resource(patient2).execute().getId().toUnqualifiedVersionless(); Encounter enc1 = new Encounter(); enc1.setStatus(EncounterStatus.CANCELLED); - enc1.getSubject().setReferenceElement(sourcePatId); - enc1.getServiceProvider().setReferenceElement(orgId); - encId1 = myClient.create().resource(enc1).execute().getId().toUnqualifiedVersionless(); + enc1.getSubject().setReferenceElement(mySourcePatId); + enc1.getServiceProvider().setReferenceElement(myOrgId); + myEncId1 = myClient.create().resource(enc1).execute().getId().toUnqualifiedVersionless(); Encounter enc2 = new Encounter(); enc2.setStatus(EncounterStatus.ARRIVED); - enc2.getSubject().setReferenceElement(sourcePatId); - enc2.getServiceProvider().setReferenceElement(orgId); - encId2 = myClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless(); + enc2.getSubject().setReferenceElement(mySourcePatId); + enc2.getServiceProvider().setReferenceElement(myOrgId); + myEncId2 = myClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless(); Task task = new Task(); task.setStatus(Task.TaskStatus.COMPLETED); - task.getOwner().setReferenceElement(sourcePatId); - taskId = myClient.create().resource(task).execute().getId().toUnqualifiedVersionless(); + task.getOwner().setReferenceElement(mySourcePatId); + myTaskId = myClient.create().resource(task).execute().getId().toUnqualifiedVersionless(); Encounter targetEnc1 = new Encounter(); targetEnc1.setStatus(EncounterStatus.ARRIVED); - targetEnc1.getSubject().setReferenceElement(targetPatId); - targetEnc1.getServiceProvider().setReferenceElement(orgId); - this.targetEnc1 = myClient.create().resource(targetEnc1).execute().getId().toUnqualifiedVersionless(); + targetEnc1.getSubject().setReferenceElement(myTargetPatId); + targetEnc1.getServiceProvider().setReferenceElement(myOrgId); + this.myTargetEnc1 = myClient.create().resource(targetEnc1).execute().getId().toUnqualifiedVersionless(); myObsIds = new ArrayList<>(); for (int i = 0; i < 20; i++) { Observation obs = new Observation(); - obs.getSubject().setReferenceElement(sourcePatId); + obs.getSubject().setReferenceElement(mySourcePatId); obs.setStatus(ObservationStatus.FINAL); IIdType obsId = myClient.create().resource(obs).execute().getId().toUnqualifiedVersionless(); myObsIds.add(obsId); } + myResultPatient = new Patient(); + myResultPatient.addIdentifier(pat1IdentifierA); } @ParameterizedTest - @ValueSource(booleans = {true, false}) - public void testMergeWithoutResult(boolean withDelete) throws Exception { + @CsvSource({ + // withDelete, withInputResultPatient + "true, true", + "true, false", + "false, true", + "false, false", + }) + public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPatient) throws Exception { PatientMergeInputParameters inParams = new PatientMergeInputParameters(); - inParams.sourcePatient = new Reference().setReferenceElement(sourcePatId); - inParams.targetPatient = new Reference().setReferenceElement(targetPatId); + inParams.sourcePatient = new Reference().setReferenceElement(mySourcePatId); + inParams.targetPatient = new Reference().setReferenceElement(myTargetPatId); inParams.deleteSource = withDelete; + if (withInputResultPatient) { + inParams.resultPatient = myResultPatient; + } IGenericClient client = myFhirContext.newRestfulGenericClient(myServerBase); Parameters inParameters = inParams.asParametersResource(); @@ -180,28 +190,33 @@ public void testMergeWithoutResult(boolean withDelete) throws Exception { // Assert Merged Patient Patient mergedPatient = (Patient) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_RESULT).getResource(); List identifiers = mergedPatient.getIdentifier(); - assertThat(identifiers).hasSize(5); - assertThat(identifiers) - .extracting(Identifier::getSystem) - .containsExactlyInAnyOrder("SYS1A", "SYS1B", "SYS2A", "SYS2B", "SYSC"); - assertThat(identifiers) - .extracting(Identifier::getValue) - .containsExactlyInAnyOrder("VAL1A", "VAL1B", "VAL2A", "VAL2B", "VALC"); - + if (withInputResultPatient) { + assertThat(identifiers).hasSize(1); + assertThat(identifiers.get(0).getSystem()).isEqualTo("SYS1A"); + assertThat(identifiers.get(0).getValue()).isEqualTo("VAL1A"); + } else { + assertThat(identifiers).hasSize(5); + assertThat(identifiers) + .extracting(Identifier::getSystem) + .containsExactlyInAnyOrder("SYS1A", "SYS1B", "SYS2A", "SYS2B", "SYSC"); + assertThat(identifiers) + .extracting(Identifier::getValue) + .containsExactlyInAnyOrder("VAL1A", "VAL1B", "VAL2A", "VAL2B", "VALC"); + } if (!withDelete) { // assert source has link to target - Patient source = myPatientDao.read(sourcePatId, mySrd); + Patient source = myPatientDao.read(mySourcePatId, mySrd); assertThat(source.getLink()) .hasSize(1) .element(0) .extracting(link -> link.getOther().getReferenceElement()) - .isEqualTo(targetPatId); + .isEqualTo(myTargetPatId); } // FIXME KHS assert on these three - Bundle bundle = fetchBundle(myServerBase + "/" + targetPatId + "/$everything?_format=json&_count=100", EncodingEnum.JSON); + Bundle bundle = fetchBundle(myServerBase + "/" + myTargetPatId + "/$everything?_format=json&_count=100", EncodingEnum.JSON); assertNull(bundle.getLink("next")); @@ -213,15 +228,15 @@ public void testMergeWithoutResult(boolean withDelete) throws Exception { ourLog.info("Found IDs: {}", actual); if (withDelete) { - assertThat(actual).doesNotContain(sourcePatId); + assertThat(actual).doesNotContain(mySourcePatId); } - assertThat(actual).contains(encId1); - assertThat(actual).contains(encId2); - assertThat(actual).contains(orgId); - assertThat(actual).contains(taskId); + assertThat(actual).contains(myEncId1); + assertThat(actual).contains(myEncId2); + assertThat(actual).contains(myOrgId); + assertThat(actual).contains(myTaskId); assertThat(actual).containsAll(myObsIds); - assertThat(actual).contains(targetPatId); - assertThat(actual).contains(targetEnc1); + assertThat(actual).contains(myTargetPatId); + assertThat(actual).contains(myTargetEnc1); } @Test @@ -264,7 +279,7 @@ private static class PatientMergeInputParameters { Type sourcePatientIdentifier; Type targetPatient; Type targetPatientIdentifier; - Patient resultResource; + Patient resultPatient; Boolean preview; Boolean deleteSource; @@ -282,8 +297,8 @@ public Parameters asParametersResource() { if (targetPatientIdentifier != null) { inParams.addParameter().setName("target-patient-identifier").setValue(targetPatientIdentifier); } - if (resultResource != null) { - inParams.addParameter().setName("result-patient").setResource(resultResource); + if (resultPatient != null) { + inParams.addParameter().setName("result-patient").setResource(resultPatient); } if (preview != null) { inParams.addParameter().setName("preview").setValue(new BooleanType(preview)); From b2213b4057a1bfbc84347322097893592ca57cf6 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 4 Dec 2024 13:44:51 -0500 Subject: [PATCH 019/148] fixing result patient --- .../jpa/provider/r4/PatientMergeR4Test.java | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index f4afbb90e808..85986eea0343 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.provider.r4; +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.parser.StrictErrorHandler; @@ -17,6 +18,7 @@ import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Encounter.EncounterStatus; +import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; @@ -30,14 +32,19 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT; @@ -52,6 +59,8 @@ public class PatientMergeR4Test extends BaseResourceProviderR4Test { static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PatientMergeR4Test.class); + static final FhirContext ourFhirContext = FhirContext.forR4Cached(); + static final Identifier pat1IdentifierA = new Identifier().setSystem("SYS1A").setValue("VAL1A"); static final Identifier pat1IdentifierB = new Identifier().setSystem("SYS1B").setValue("VAL1B"); static final Identifier pat2IdentifierA = new Identifier().setSystem("SYS2A").setValue("VAL2A"); @@ -68,6 +77,9 @@ public class PatientMergeR4Test extends BaseResourceProviderR4Test { IIdType myTargetEnc1; Patient myResultPatient; + @RegisterExtension + static MyExceptionHandler ourExceptionHandler = new MyExceptionHandler(); + @BeforeEach public void beforeDisableResultReuse() { myStorageSettings.setReuseCachedSearchResultsForMillis(null); @@ -141,7 +153,11 @@ public void before() throws Exception { } myResultPatient = new Patient(); + myResultPatient.setIdElement((IdType) myTargetPatId); myResultPatient.addIdentifier(pat1IdentifierA); + Patient.PatientLinkComponent link = myResultPatient.addLink(); + link.setOther(new Reference(mySourcePatId)); + link.setType(Patient.LinkType.REPLACES); } @ParameterizedTest @@ -273,7 +289,6 @@ private Bundle fetchBundle(String theUrl, EncodingEnum theEncoding) throws IOExc } - private static class PatientMergeInputParameters { Type sourcePatient; Type sourcePatientIdentifier; @@ -288,7 +303,7 @@ public Parameters asParametersResource() { if (sourcePatient != null) { inParams.addParameter().setName("source-patient").setValue(sourcePatient); } - if (sourcePatientIdentifier!= null) { + if (sourcePatientIdentifier != null) { inParams.addParameter().setName("source-patient-identifier").setValue(sourcePatientIdentifier); } if (targetPatient != null) { @@ -311,4 +326,17 @@ public Parameters asParametersResource() { } + static class MyExceptionHandler implements TestExecutionExceptionHandler { + @Override + public void handleTestExecutionException(ExtensionContext theExtensionContext, Throwable theThrowable) throws Throwable { + if (theThrowable instanceof InvalidRequestException ex) { + String body = ex.getResponseBody(); + Parameters outParams = ourFhirContext.newJsonParser().parseResource(Parameters.class, body); + OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); + String message = outcome.getIssue().stream().map(issue -> issue.getDiagnostics()).collect(Collectors.joining(", ")); + throw InvalidRequestException.class.getDeclaredConstructor(String.class, Throwable.class).newInstance(message, ex); + } + throw theThrowable; + } + } } From deb69bfbe69e177d272f87457df8ca1b038de40b Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 4 Dec 2024 14:10:42 -0500 Subject: [PATCH 020/148] fixing result patient --- .../ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 85986eea0343..7f281a8e60f2 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -50,6 +50,7 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_RESULT_PATIENT; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -190,6 +191,12 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa // Assert income Parameters input = (Parameters) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_INPUT).getResource(); + { // if the following assert fails, check that these two patients are identical + Patient p1 = (Patient) inParameters.getParameter(OPERATION_MERGE_RESULT_PATIENT).getResource(); + Patient p2 = (Patient) input.getParameter(OPERATION_MERGE_RESULT_PATIENT).getResource(); + ourLog.info(ourFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(p1)); + ourLog.info(ourFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(p2)); + } assertTrue(input.equalsDeep(inParameters)); From 7cd12417e0d1f0b7b41a1016ebe43aed4dac47b6 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 4 Dec 2024 14:30:05 -0500 Subject: [PATCH 021/148] fixing result patient --- .../java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 7f281a8e60f2..5bcabc305f91 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -191,7 +191,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa // Assert income Parameters input = (Parameters) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_INPUT).getResource(); - { // if the following assert fails, check that these two patients are identical + if (withInputResultPatient) { // if the following assert fails, check that these two patients are identical Patient p1 = (Patient) inParameters.getParameter(OPERATION_MERGE_RESULT_PATIENT).getResource(); Patient p2 = (Patient) input.getParameter(OPERATION_MERGE_RESULT_PATIENT).getResource(); ourLog.info(ourFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(p1)); @@ -237,8 +237,6 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa } - // FIXME KHS assert on these three - Bundle bundle = fetchBundle(myServerBase + "/" + myTargetPatId + "/$everything?_format=json&_count=100", EncodingEnum.JSON); assertNull(bundle.getLink("next")); From eb273175b5410f7e5b44d438c69c8d4e5c2383b0 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Wed, 4 Dec 2024 14:30:32 -0500 Subject: [PATCH 022/148] add duplicate check when copying source identifiers to target --- .../jpa/dao/merge/ResourceMergeService.java | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index bbca7de1218c..6237015a3928 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -337,9 +337,37 @@ private Patient prepareTargetPatientForUpdate( return theTargetResource; } + /** + * Checks if theIdentifiers contains theIdentifier using equalsDeep + * @param theIdentifiers the list of identifiers + * @param theIdentifier the identifier to check + * @return true if theIdentifiers contains theIdentifier, false otherwise + */ + private boolean containsIdentifier(List theIdentifiers, Identifier theIdentifier) { + for (Identifier identifier : theIdentifiers) { + if (identifier.equalsDeep(theIdentifier)) { + return true; + } + } + return false; + } + + /** + * Copies each identifier from theSourceResource to theTargetResource, after checking that theTargetResource does + * not already contain the source identifier + * @param theSourceResource the source resource to copy identifiers from + * @param theTargetResource the target resource to copy identifiers to + */ private void copyIdentifiers(Patient theSourceResource, Patient theTargetResource) { - // should we check if the target already has the identifier we are copying to not duplicate it? - theSourceResource.getIdentifier().forEach(theTargetResource::addIdentifier); + if (theSourceResource.hasIdentifier()) { + List sourceIdentifiers = theSourceResource.getIdentifier(); + List targetIdentifiers = theTargetResource.getIdentifier(); + for (Identifier sourceIdentifier : sourceIdentifiers) { + if (!containsIdentifier(targetIdentifiers, sourceIdentifier)) { + theTargetResource.addIdentifier(sourceIdentifier); + } + } + } } private Patient updateResource(Patient theResource, RequestDetails theRequestDetails) { From baa8d2faac7708b4d9cb6b8966207304675b229b Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Wed, 4 Dec 2024 14:39:03 -0500 Subject: [PATCH 023/148] copy result-patient before passing it to service --- .../fhir/jpa/provider/BaseJpaResourceProviderPatient.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index d63ff0ce04ad..4139fb40bb63 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -357,7 +357,12 @@ private MergeOperationInputParameters buildMergeOperationInputParameters( mergeOperationParameters.setTargetResource(theTargetPatient); mergeOperationParameters.setPreview(thePreview != null && thePreview.getValue()); mergeOperationParameters.setDeleteSource(theDeleteSource != null && theDeleteSource.getValue()); - mergeOperationParameters.setResultResource(theResultPatient); + + if (theResultPatient != null) { + // pass in a copy of the result patient as we don't want it to be modified as it will be + // returned back to the client + mergeOperationParameters.setResultResource(((Patient) theResultPatient).copy()); + } return mergeOperationParameters; } From e08b3e6a3131f7e7b0f7528b0e3c40cff096e1c9 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 4 Dec 2024 15:31:35 -0500 Subject: [PATCH 024/148] add preview to test --- .../jpa/provider/r4/PatientMergeR4Test.java | 96 ++++++++++++------- 1 file changed, 62 insertions(+), 34 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 4ef432eca719..45b1ad143ab8 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -7,6 +7,7 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; @@ -32,7 +33,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; @@ -70,10 +70,10 @@ public class PatientMergeR4Test extends BaseResourceProviderR4Test { IIdType myOrgId; IIdType mySourcePatId; - IIdType myTaskId; - IIdType myEncId1; - IIdType myEncId2; - ArrayList myObsIds; + IIdType mySourceTaskId; + IIdType mySourceEncId1; + IIdType mySourceEncId2; + ArrayList mySourceObsIds; IIdType myTargetPatId; IIdType myTargetEnc1; Patient myResultPatient; @@ -125,18 +125,18 @@ public void before() throws Exception { enc1.setStatus(EncounterStatus.CANCELLED); enc1.getSubject().setReferenceElement(mySourcePatId); enc1.getServiceProvider().setReferenceElement(myOrgId); - myEncId1 = myClient.create().resource(enc1).execute().getId().toUnqualifiedVersionless(); + mySourceEncId1 = myClient.create().resource(enc1).execute().getId().toUnqualifiedVersionless(); Encounter enc2 = new Encounter(); enc2.setStatus(EncounterStatus.ARRIVED); enc2.getSubject().setReferenceElement(mySourcePatId); enc2.getServiceProvider().setReferenceElement(myOrgId); - myEncId2 = myClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless(); + mySourceEncId2 = myClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless(); Task task = new Task(); task.setStatus(Task.TaskStatus.COMPLETED); task.getOwner().setReferenceElement(mySourcePatId); - myTaskId = myClient.create().resource(task).execute().getId().toUnqualifiedVersionless(); + mySourceTaskId = myClient.create().resource(task).execute().getId().toUnqualifiedVersionless(); Encounter targetEnc1 = new Encounter(); targetEnc1.setStatus(EncounterStatus.ARRIVED); @@ -144,13 +144,13 @@ public void before() throws Exception { targetEnc1.getServiceProvider().setReferenceElement(myOrgId); this.myTargetEnc1 = myClient.create().resource(targetEnc1).execute().getId().toUnqualifiedVersionless(); - myObsIds = new ArrayList<>(); + mySourceObsIds = new ArrayList<>(); for (int i = 0; i < 20; i++) { Observation obs = new Observation(); obs.getSubject().setReferenceElement(mySourcePatId); obs.setStatus(ObservationStatus.FINAL); IIdType obsId = myClient.create().resource(obs).execute().getId().toUnqualifiedVersionless(); - myObsIds.add(obsId); + mySourceObsIds.add(obsId); } myResultPatient = new Patient(); @@ -163,13 +163,17 @@ public void before() throws Exception { @ParameterizedTest @CsvSource({ - // withDelete, withInputResultPatient - "true, true", - "true, false", - "false, true", - "false, false", + // withDelete, withInputResultPatient, withPreview + "true, true, true", + "true, false, true", + "false, true, true", + "false, false, true", + "true, true, false", + "true, false, false", + "false, true, false", + "false, false, false", }) - public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPatient) throws Exception { + public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPatient, boolean withPreview) throws Exception { PatientMergeInputParameters inParams = new PatientMergeInputParameters(); inParams.sourcePatient = new Reference().setReferenceElement(mySourcePatId); inParams.targetPatient = new Reference().setReferenceElement(myTargetPatId); @@ -177,6 +181,9 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa if (withInputResultPatient) { inParams.resultPatient = myResultPatient; } + if (withPreview) { + inParams.preview = true; + } IGenericClient client = myFhirContext.newRestfulGenericClient(myServerBase); Parameters inParameters = inParams.asParametersResource(); @@ -191,7 +198,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa // Assert income Parameters input = (Parameters) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_INPUT).getResource(); - if (withInputResultPatient) { // if the following assert fails, check that these two patients are identical + if (withInputResultPatient) { // if the following assert fails, check that these two patients are identical Patient p1 = (Patient) inParameters.getParameter(OPERATION_MERGE_RESULT_PATIENT).getResource(); Patient p2 = (Patient) input.getParameter(OPERATION_MERGE_RESULT_PATIENT).getResource(); ourLog.info(ourFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(p1)); @@ -201,13 +208,23 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa // Assert outcome OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); - assertThat(outcome.getIssue()) - .hasSize(1) - .element(0) - .satisfies(issue -> { - assertThat(issue.getDiagnostics()).isEqualTo("Merge operation completed successfully."); - assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - }); + if (withPreview) { + assertThat(outcome.getIssue()) + .hasSize(1) + .element(0) + .satisfies(issue -> { + assertThat(issue.getDiagnostics()).isEqualTo("Preview only merge operation - no issues detected"); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + }); + } else { + assertThat(outcome.getIssue()) + .hasSize(1) + .element(0) + .satisfies(issue -> { + assertThat(issue.getDiagnostics()).isEqualTo("Merge operation completed successfully."); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + }); + } // Assert Merged Patient Patient mergedPatient = (Patient) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_RESULT).getResource(); @@ -225,7 +242,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa .extracting(Identifier::getValue) .containsExactlyInAnyOrder("VAL1A", "VAL1B", "VAL2A", "VAL2B", "VALC"); } - if (!withDelete) { + if (!withPreview && !withDelete) { // assert source has link to target Patient source = myPatientDao.read(mySourcePatId, mySrd); assertThat(source.getLink()) @@ -246,16 +263,27 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa ourLog.info("Found IDs: {}", actual); - if (withDelete) { + if (withPreview) { assertThat(actual).doesNotContain(mySourcePatId); + assertThat(actual).doesNotContain(mySourceEncId1); + assertThat(actual).doesNotContain(mySourceEncId2); + assertThat(actual).contains(myOrgId); + assertThat(actual).doesNotContain(mySourceTaskId); + assertThat(actual).doesNotContainAnyElementsOf(mySourceObsIds); + assertThat(actual).contains(myTargetPatId); + assertThat(actual).contains(myTargetEnc1); + } else { + if (withDelete) { + assertThat(actual).doesNotContain(mySourcePatId); + } + assertThat(actual).contains(mySourceEncId1); + assertThat(actual).contains(mySourceEncId2); + assertThat(actual).contains(myOrgId); + assertThat(actual).contains(mySourceTaskId); + assertThat(actual).containsAll(mySourceObsIds); + assertThat(actual).contains(myTargetPatId); + assertThat(actual).contains(myTargetEnc1); } - assertThat(actual).contains(myEncId1); - assertThat(actual).contains(myEncId2); - assertThat(actual).contains(myOrgId); - assertThat(actual).contains(myTaskId); - assertThat(actual).containsAll(myObsIds); - assertThat(actual).contains(myTargetPatId); - assertThat(actual).contains(myTargetEnc1); } @Test @@ -332,7 +360,7 @@ public Parameters asParametersResource() { static class MyExceptionHandler implements TestExecutionExceptionHandler { @Override public void handleTestExecutionException(ExtensionContext theExtensionContext, Throwable theThrowable) throws Throwable { - if (theThrowable instanceof InvalidRequestException ex) { + if (theThrowable instanceof BaseServerResponseException ex) { String body = ex.getResponseBody(); Parameters outParams = ourFhirContext.newJsonParser().parseResource(Parameters.class, body); OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); From 0100c722a118a5287a136192a8d6bd1bc96a048c Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 4 Dec 2024 15:40:22 -0500 Subject: [PATCH 025/148] update preview asserts --- .../java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 45b1ad143ab8..fafceb72102b 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -213,16 +213,17 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa .hasSize(1) .element(0) .satisfies(issue -> { - assertThat(issue.getDiagnostics()).isEqualTo("Preview only merge operation - no issues detected"); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(issue.getDetails().getText()).isEqualTo("Preview only merge operation - no issues detected"); + assertThat(issue.getDiagnostics()).isEqualTo("Merge would update 25 resources"); }); } else { assertThat(outcome.getIssue()) .hasSize(1) .element(0) .satisfies(issue -> { - assertThat(issue.getDiagnostics()).isEqualTo("Merge operation completed successfully."); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(issue.getDiagnostics()).isEqualTo("Merge operation completed successfully."); }); } From e96895b647c196538a899e18ce8b8ff0d8303a6a Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Wed, 4 Dec 2024 15:36:56 -0500 Subject: [PATCH 026/148] move IReplaceReferencesSvc to storage-package, and call replaceReference in patient merge --- .../jpa/provider/BaseJpaResourceProviderPatient.java | 10 +++++++--- .../uhn/fhir/jpa/dao/merge/ResourceMergeService.java | 8 ++++++-- .../uhn/fhir/jpa/provider/IReplaceReferencesSvc.java | 0 .../fhir/jpa/dao/merge/ResourceMergeServiceTest.java | 7 ++++++- 4 files changed, 19 insertions(+), 6 deletions(-) rename {hapi-fhir-jpaserver-base => hapi-fhir-storage}/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java (100%) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 4139fb40bb63..2a6625076ada 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -58,6 +58,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Patient; +import org.springframework.beans.factory.annotation.Autowired; import java.util.Arrays; import java.util.List; @@ -68,6 +69,9 @@ public abstract class BaseJpaResourceProviderPatient extends BaseJpaResourceProvider { + @Autowired + private IReplaceReferencesSvc myReplaceReferencesSvc; + /** * Patient/123/$everything */ @@ -295,7 +299,7 @@ public IBaseParameters patientMerge( theResultPatient); IFhirResourceDaoPatient dao = (IFhirResourceDaoPatient) getDao(); - ResourceMergeService resourceMergeService = new ResourceMergeService(dao); + ResourceMergeService resourceMergeService = new ResourceMergeService(dao, myReplaceReferencesSvc); FhirContext fhirContext = dao.getContext(); @@ -359,8 +363,8 @@ private MergeOperationInputParameters buildMergeOperationInputParameters( mergeOperationParameters.setDeleteSource(theDeleteSource != null && theDeleteSource.getValue()); if (theResultPatient != null) { - // pass in a copy of the result patient as we don't want it to be modified as it will be - // returned back to the client + // pass in a copy of the result patient as we don't want it to be modified. It will be + // returned back to the client as part of the response. mergeOperationParameters.setResultResource(((Patient) theResultPatient).copy()); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 6237015a3928..7d5b92f425ba 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; +import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -56,10 +57,13 @@ public class ResourceMergeService { private static final Logger ourLog = LoggerFactory.getLogger(ResourceMergeService.class); private final IFhirResourceDaoPatient myDao; + private final IReplaceReferencesSvc myReplaceReferencesSvc; private final FhirContext myFhirContext; - public ResourceMergeService(IFhirResourceDaoPatient thePatientDao) { + public ResourceMergeService( + IFhirResourceDaoPatient thePatientDao, IReplaceReferencesSvc theReplaceReferencesSvc) { myDao = thePatientDao; + myReplaceReferencesSvc = theReplaceReferencesSvc; myFhirContext = myDao.getContext(); } @@ -145,7 +149,7 @@ private void doMerge( return; } - // TODO Emre: do the actual ref updates + myReplaceReferencesSvc.replaceReferences(sourceResource.getId(), targetResource.getId(), theRequestDetails); Patient patientToUpdate = prepareTargetPatientForUpdate( targetResource, sourceResource, resultResource, theMergeOperationParameters.getDeleteSource()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java similarity index 100% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java rename to hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index df9dcbe5596a..6c6f5c02787b 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -3,6 +3,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; +import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -51,6 +52,10 @@ public class ResourceMergeServiceTest { @Mock private IFhirResourceDaoPatient myDaoMock; + + @Mock + IReplaceReferencesSvc myReplaceReferencesSvcMock; + @Mock RequestDetails myRequestDetailsMock; @@ -61,7 +66,7 @@ public class ResourceMergeServiceTest { @BeforeEach void setup() { when(myDaoMock.getContext()).thenReturn(myFhirContext); - myResourceMergeService = new ResourceMergeService(myDaoMock); + myResourceMergeService = new ResourceMergeService(myDaoMock, myReplaceReferencesSvcMock); } // SUCCESS CASES From d7e7a6d3f3caf4949f9e1c93d8f1941c53440e0d Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 4 Dec 2024 16:35:17 -0500 Subject: [PATCH 027/148] moar tests --- .../jpa/provider/r4/PatientMergeR4Test.java | 128 ++++++++++++++---- 1 file changed, 102 insertions(+), 26 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index fafceb72102b..93cfd53f17ec 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -9,6 +9,7 @@ import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; @@ -30,6 +31,7 @@ import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.Type; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -52,6 +54,7 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_RESULT_PATIENT; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -81,10 +84,7 @@ public class PatientMergeR4Test extends BaseResourceProviderR4Test { @RegisterExtension static MyExceptionHandler ourExceptionHandler = new MyExceptionHandler(); - @BeforeEach - public void beforeDisableResultReuse() { - myStorageSettings.setReuseCachedSearchResultsForMillis(null); - } + IGenericClient myFhirClient; @Override @AfterEach @@ -98,13 +98,15 @@ public void after() throws Exception { @BeforeEach public void before() throws Exception { super.before(); + myStorageSettings.setReuseCachedSearchResultsForMillis(null); + myStorageSettings.setAllowMultipleDelete(true); myFhirContext.setParserErrorHandler(new StrictErrorHandler()); - myStorageSettings.setAllowMultipleDelete(true); + myFhirClient = myFhirContext.newRestfulGenericClient(myServerBase); Organization org = new Organization(); org.setName("an org"); - myOrgId = myClient.create().resource(org).execute().getId().toUnqualifiedVersionless(); + myOrgId = myFhirClient.create().resource(org).execute().getId().toUnqualifiedVersionless(); ourLog.info("OrgId: {}", myOrgId); Patient patient1 = new Patient(); @@ -112,44 +114,44 @@ public void before() throws Exception { patient1.addIdentifier(pat1IdentifierA); patient1.addIdentifier(pat1IdentifierB); patient1.addIdentifier(patBothIdentifierC); - mySourcePatId = myClient.create().resource(patient1).execute().getId().toUnqualifiedVersionless(); + mySourcePatId = myFhirClient.create().resource(patient1).execute().getId().toUnqualifiedVersionless(); Patient patient2 = new Patient(); patient2.addIdentifier(pat2IdentifierA); patient2.addIdentifier(pat2IdentifierB); patient2.addIdentifier(patBothIdentifierC); patient2.getManagingOrganization().setReferenceElement(myOrgId); - myTargetPatId = myClient.create().resource(patient2).execute().getId().toUnqualifiedVersionless(); + myTargetPatId = myFhirClient.create().resource(patient2).execute().getId().toUnqualifiedVersionless(); Encounter enc1 = new Encounter(); enc1.setStatus(EncounterStatus.CANCELLED); enc1.getSubject().setReferenceElement(mySourcePatId); enc1.getServiceProvider().setReferenceElement(myOrgId); - mySourceEncId1 = myClient.create().resource(enc1).execute().getId().toUnqualifiedVersionless(); + mySourceEncId1 = myFhirClient.create().resource(enc1).execute().getId().toUnqualifiedVersionless(); Encounter enc2 = new Encounter(); enc2.setStatus(EncounterStatus.ARRIVED); enc2.getSubject().setReferenceElement(mySourcePatId); enc2.getServiceProvider().setReferenceElement(myOrgId); - mySourceEncId2 = myClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless(); + mySourceEncId2 = myFhirClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless(); Task task = new Task(); task.setStatus(Task.TaskStatus.COMPLETED); task.getOwner().setReferenceElement(mySourcePatId); - mySourceTaskId = myClient.create().resource(task).execute().getId().toUnqualifiedVersionless(); + mySourceTaskId = myFhirClient.create().resource(task).execute().getId().toUnqualifiedVersionless(); Encounter targetEnc1 = new Encounter(); targetEnc1.setStatus(EncounterStatus.ARRIVED); targetEnc1.getSubject().setReferenceElement(myTargetPatId); targetEnc1.getServiceProvider().setReferenceElement(myOrgId); - this.myTargetEnc1 = myClient.create().resource(targetEnc1).execute().getId().toUnqualifiedVersionless(); + this.myTargetEnc1 = myFhirClient.create().resource(targetEnc1).execute().getId().toUnqualifiedVersionless(); mySourceObsIds = new ArrayList<>(); for (int i = 0; i < 20; i++) { Observation obs = new Observation(); obs.getSubject().setReferenceElement(mySourcePatId); obs.setStatus(ObservationStatus.FINAL); - IIdType obsId = myClient.create().resource(obs).execute().getId().toUnqualifiedVersionless(); + IIdType obsId = myFhirClient.create().resource(obs).execute().getId().toUnqualifiedVersionless(); mySourceObsIds.add(obsId); } @@ -185,9 +187,8 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa inParams.preview = true; } - IGenericClient client = myFhirContext.newRestfulGenericClient(myServerBase); Parameters inParameters = inParams.asParametersResource(); - Parameters outParams = client.operation() + Parameters outParams = myFhirClient.operation() .onType("Patient") .named(OPERATION_MERGE) .withParameters(inParameters) @@ -287,21 +288,91 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa } } + @ParameterizedTest + @CsvSource({ + // withDelete, withInputResultPatient, withPreview + "true, true, true", + "true, false, true", + "false, true, true", + "false, false, true", + "true, true, false", + "true, false, false", + "false, true, false", + "false, false, false", + }) + public void testMultipleTargetMatchesFails(boolean withDelete, boolean withInputResultPatient, boolean withPreview) throws Exception { + PatientMergeInputParameters inParams = new PatientMergeInputParameters(); + inParams.sourcePatient = new Reference().setReferenceElement(mySourcePatId); + inParams.targetPatientIdentifier = patBothIdentifierC; + inParams.deleteSource = withDelete; + if (withInputResultPatient) { + inParams.resultPatient = myResultPatient; + } + if (withPreview) { + inParams.preview = true; + } + + + Parameters inParameters = inParams.asParametersResource(); + + assertUnprocessibleEntityWithMessage(myFhirClient, inParameters, "Multiple resources found matching the identifier(s) specified in 'target-patient-identifier'"); + } + + + @ParameterizedTest + @CsvSource({ + // withDelete, withInputResultPatient, withPreview + "true, true, true", + "true, false, true", + "false, true, true", + "false, false, true", + "true, true, false", + "true, false, false", + "false, true, false", + "false, false, false", + }) + public void testMultipleSourceMatchesFails(boolean withDelete, boolean withInputResultPatient, boolean withPreview) throws Exception { + PatientMergeInputParameters inParams = new PatientMergeInputParameters(); + inParams.sourcePatientIdentifier = patBothIdentifierC; + inParams.targetPatient = new Reference().setReferenceElement(mySourcePatId); + inParams.deleteSource = withDelete; + if (withInputResultPatient) { + inParams.resultPatient = myResultPatient; + } + if (withPreview) { + inParams.preview = true; + } + + Parameters inParameters = inParams.asParametersResource(); + + assertUnprocessibleEntityWithMessage(myFhirClient, inParameters, "Multiple resources found matching the identifier(s) specified in 'source-patient-identifier'"); + } + + private static void assertUnprocessibleEntityWithMessage(IGenericClient client, Parameters inParameters, String theExpectedMessage) { + assertThatThrownBy(() -> + client.operation() + .onType("Patient") + .named(OPERATION_MERGE) + .withParameters(inParameters) + .execute()) + .isInstanceOf(UnprocessableEntityException.class) + .extracting(e -> extractFailureMessage((UnprocessableEntityException) e)) + .isEqualTo(theExpectedMessage); + } + @Test void test_MissingRequiredParameters_Returns400BadRequest() { Parameters inParams = new Parameters(); - IGenericClient client = myFhirContext.newRestfulGenericClient(myServerBase); - - InvalidRequestException thrown = assertThrows(InvalidRequestException.class, () -> client.operation() + assertThatThrownBy(() -> myFhirClient.operation() .onType("Patient") .named(OPERATION_MERGE) .withParameters(inParams) .returnResourceType(Parameters.class) .execute() - ); - - assertThat(thrown.getStatusCode()).isEqualTo(400); + ).isInstanceOf(InvalidRequestException.class) + .extracting(e -> ((InvalidRequestException) e).getStatusCode()) + .isEqualTo(400); } // FIXME KHS look at PatientEverythingR4Test for ideas for other tests @@ -362,13 +433,18 @@ static class MyExceptionHandler implements TestExecutionExceptionHandler { @Override public void handleTestExecutionException(ExtensionContext theExtensionContext, Throwable theThrowable) throws Throwable { if (theThrowable instanceof BaseServerResponseException ex) { - String body = ex.getResponseBody(); - Parameters outParams = ourFhirContext.newJsonParser().parseResource(Parameters.class, body); - OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); - String message = outcome.getIssue().stream().map(issue -> issue.getDiagnostics()).collect(Collectors.joining(", ")); - throw InvalidRequestException.class.getDeclaredConstructor(String.class, Throwable.class).newInstance(message, ex); + String message = extractFailureMessage(ex); + throw ex.getClass().getDeclaredConstructor(String.class, Throwable.class).newInstance(message, ex); } throw theThrowable; } } + + private static @NotNull String extractFailureMessage(BaseServerResponseException ex) { + String body = ex.getResponseBody(); + Parameters outParams = ourFhirContext.newJsonParser().parseResource(Parameters.class, body); + OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); + String message = outcome.getIssue().stream().map(issue -> issue.getDiagnostics()).collect(Collectors.joining(", ")); + return message; + } } From 8d05f1082cdcadac7a93e8ee88eb1d59bc425e93 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 4 Dec 2024 17:30:04 -0500 Subject: [PATCH 028/148] cleanup --- .../jpa/provider/r4/PatientMergeR4Test.java | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 93cfd53f17ec..1b00b16009f4 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -14,6 +14,7 @@ import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.assertj.core.api.Assertions; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Bundle; @@ -188,12 +189,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa } Parameters inParameters = inParams.asParametersResource(); - Parameters outParams = myFhirClient.operation() - .onType("Patient") - .named(OPERATION_MERGE) - .withParameters(inParameters) - .returnResourceType(Parameters.class) - .execute(); + Parameters outParams = callMergeOperation(inParameters); assertThat(outParams.getParameter()).hasSize(3); @@ -315,7 +311,7 @@ public void testMultipleTargetMatchesFails(boolean withDelete, boolean withInput Parameters inParameters = inParams.asParametersResource(); - assertUnprocessibleEntityWithMessage(myFhirClient, inParameters, "Multiple resources found matching the identifier(s) specified in 'target-patient-identifier'"); + assertUnprocessibleEntityWithMessage(inParameters, "Multiple resources found matching the identifier(s) specified in 'target-patient-identifier'"); } @@ -345,31 +341,29 @@ public void testMultipleSourceMatchesFails(boolean withDelete, boolean withInput Parameters inParameters = inParams.asParametersResource(); - assertUnprocessibleEntityWithMessage(myFhirClient, inParameters, "Multiple resources found matching the identifier(s) specified in 'source-patient-identifier'"); + assertUnprocessibleEntityWithMessage(inParameters, "Multiple resources found matching the identifier(s) specified in 'source-patient-identifier'"); } - private static void assertUnprocessibleEntityWithMessage(IGenericClient client, Parameters inParameters, String theExpectedMessage) { + private void assertUnprocessibleEntityWithMessage(Parameters inParameters, String theExpectedMessage) { assertThatThrownBy(() -> - client.operation() - .onType("Patient") - .named(OPERATION_MERGE) - .withParameters(inParameters) - .execute()) + callMergeOperation(inParameters)) .isInstanceOf(UnprocessableEntityException.class) .extracting(e -> extractFailureMessage((UnprocessableEntityException) e)) .isEqualTo(theExpectedMessage); } - @Test - void test_MissingRequiredParameters_Returns400BadRequest() { - Parameters inParams = new Parameters(); - - assertThatThrownBy(() -> myFhirClient.operation() + private Parameters callMergeOperation(Parameters inParameters) { + return myClient.operation() .onType("Patient") .named(OPERATION_MERGE) - .withParameters(inParams) + .withParameters(inParameters) .returnResourceType(Parameters.class) - .execute() + .execute(); + } + + @Test + void test_MissingRequiredParameters_Returns400BadRequest() { + assertThatThrownBy(() -> callMergeOperation(new Parameters()) ).isInstanceOf(InvalidRequestException.class) .extracting(e -> ((InvalidRequestException) e).getStatusCode()) .isEqualTo(400); From 747ee8f707552ad4976bab3e1e13754a4dc5e7d0 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 5 Dec 2024 11:42:38 -0500 Subject: [PATCH 029/148] add task test --- .../jpa/provider/r4/PatientMergeR4Test.java | 94 ++++++++++++++++--- .../server/provider/ProviderConstants.java | 1 + 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 1b00b16009f4..595a4b5d0a81 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -7,6 +7,8 @@ import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInput; +import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInputAndPartialOutput; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; @@ -19,6 +21,7 @@ import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Encounter.EncounterStatus; import org.hl7.fhir.r4.model.IdType; @@ -30,6 +33,7 @@ import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.Type; import org.jetbrains.annotations.NotNull; @@ -49,13 +53,17 @@ import java.util.Set; import java.util.stream.Collectors; +import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER; +import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER_RESPOND_ASYNC; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_RESULT_PATIENT; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -166,17 +174,28 @@ public void before() throws Exception { @ParameterizedTest @CsvSource({ - // withDelete, withInputResultPatient, withPreview - "true, true, true", - "true, false, true", - "false, true, true", - "false, false, true", - "true, true, false", - "true, false, false", - "false, true, false", - "false, false, false", + // withDelete, withInputResultPatient, withPreview, isAsync + "true, true, true, false", + "true, false, true, false", + "false, true, true, false", + "false, false, true, false", + "true, true, false, false", + "true, false, false, false", + "false, true, false, false", + "false, false, false, false", + + "true, true, true, true", + "true, false, true, true", + "false, true, true, true", + "false, false, true, true", + "true, true, false, true", + "true, false, false, true", + "false, true, false, true", + "false, false, false, true", }) - public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPatient, boolean withPreview) throws Exception { + public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPatient, boolean withPreview, boolean isAsync) throws Exception { + // setup + PatientMergeInputParameters inParams = new PatientMergeInputParameters(); inParams.sourcePatient = new Reference().setReferenceElement(mySourcePatId); inParams.targetPatient = new Reference().setReferenceElement(myTargetPatId); @@ -189,11 +208,14 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa } Parameters inParameters = inParams.asParametersResource(); - Parameters outParams = callMergeOperation(inParameters); + // exec + Parameters outParams = callMergeOperation(inParameters, isAsync); + + // validate assertThat(outParams.getParameter()).hasSize(3); - // Assert income + // Assert input Parameters input = (Parameters) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_INPUT).getResource(); if (withInputResultPatient) { // if the following assert fails, check that these two patients are identical Patient p1 = (Patient) inParameters.getParameter(OPERATION_MERGE_RESULT_PATIENT).getResource(); @@ -204,7 +226,37 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa assertTrue(input.equalsDeep(inParameters)); // Assert outcome - OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); + OperationOutcome outcome; + + // Assert Task + if (isAsync) { + Task task = (Task) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource(); + await().until(() -> task.getStatus() == Task.TaskStatus.COMPLETED); + + Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); + + Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); + + // Assert on the output type + Coding taskType = taskOutput.getType().getCodingFirstRep(); + assertEquals("http://hl7.org/fhir/ValueSet/resource-types", taskType.getSystem()); + assertEquals("OperationOutcome", taskType.getCode()); + + List containedResources = taskWithOutput.getContained(); + assertThat(containedResources) + .hasSize(1) + .element(0) + .isInstanceOf(OperationOutcome.class); + + OperationOutcome containedOutcome = (OperationOutcome) containedResources.get(0); + + Reference outputRef = (Reference) taskOutput.getValue(); + outcome = (OperationOutcome) outputRef.getResource(); + assertTrue(containedOutcome.equalsDeep(outcome)); + } else { + outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); + } + if (withPreview) { assertThat(outcome.getIssue()) .hasSize(1) @@ -250,6 +302,8 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa .isEqualTo(myTargetPatId); } + // Check that the linked resources were updated + Bundle bundle = fetchBundle(myServerBase + "/" + myTargetPatId + "/$everything?_format=json&_count=100", EncodingEnum.JSON); assertNull(bundle.getLink("next")); @@ -353,10 +407,20 @@ private void assertUnprocessibleEntityWithMessage(Parameters inParameters, Strin } private Parameters callMergeOperation(Parameters inParameters) { - return myClient.operation() + return this.callMergeOperation(inParameters, false); + } + + private Parameters callMergeOperation(Parameters inParameters, boolean isAsync) { + IOperationUntypedWithInput request = myClient.operation() .onType("Patient") .named(OPERATION_MERGE) - .withParameters(inParameters) + .withParameters(inParameters); + + if (isAsync) { + request.withAdditionalHeader(HEADER_PREFER, HEADER_PREFER_RESPOND_ASYNC); + } + + return request .returnResourceType(Parameters.class) .execute(); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index 0507396bfa36..377bd53359ef 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -278,4 +278,5 @@ public class ProviderConstants { public static final String OPERATION_MERGE_OUTPUT_PARAM_INPUT = "input"; public static final String OPERATION_MERGE_OUTPUT_PARAM_OUTCOME = "outcome"; public static final String OPERATION_MERGE_OUTPUT_PARAM_RESULT = "result"; + public static final String OPERATION_MERGE_OUTPUT_PARAM_TASK = "task"; } From bcc987d2f7e3798b70f557fe259e5c950861f28c Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 5 Dec 2024 14:08:14 -0500 Subject: [PATCH 030/148] add replace references test --- .../jpa/provider/r4/PatientMergeR4Test.java | 74 ++++++++++++++++--- 1 file changed, 63 insertions(+), 11 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 595a4b5d0a81..8c2b8ffced0b 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -8,19 +8,19 @@ import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInput; -import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInputAndPartialOutput; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.rest.server.provider.ProviderConstants; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; -import org.assertj.core.api.Assertions; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.CarePlan; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Encounter.EncounterStatus; @@ -34,6 +34,7 @@ import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.Type; import org.jetbrains.annotations.NotNull; @@ -51,6 +52,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER; @@ -61,6 +63,7 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_RESULT_PATIENT; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; @@ -82,7 +85,7 @@ public class PatientMergeR4Test extends BaseResourceProviderR4Test { IIdType myOrgId; IIdType mySourcePatId; - IIdType mySourceTaskId; + IIdType mySourceCarePlanId; IIdType mySourceEncId1; IIdType mySourceEncId2; ArrayList mySourceObsIds; @@ -144,10 +147,10 @@ public void before() throws Exception { enc2.getServiceProvider().setReferenceElement(myOrgId); mySourceEncId2 = myFhirClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless(); - Task task = new Task(); - task.setStatus(Task.TaskStatus.COMPLETED); - task.getOwner().setReferenceElement(mySourcePatId); - mySourceTaskId = myFhirClient.create().resource(task).execute().getId().toUnqualifiedVersionless(); + CarePlan carePlan = new CarePlan(); + carePlan.setStatus(CarePlan.CarePlanStatus.ACTIVE); + carePlan.getSubject().setReferenceElement(mySourcePatId); + mySourceCarePlanId = myFhirClient.create().resource(carePlan).execute().getId().toUnqualifiedVersionless(); Encounter targetEnc1 = new Encounter(); targetEnc1.setStatus(EncounterStatus.ARRIVED); @@ -320,7 +323,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa assertThat(actual).doesNotContain(mySourceEncId1); assertThat(actual).doesNotContain(mySourceEncId2); assertThat(actual).contains(myOrgId); - assertThat(actual).doesNotContain(mySourceTaskId); + assertThat(actual).doesNotContain(mySourceCarePlanId); assertThat(actual).doesNotContainAnyElementsOf(mySourceObsIds); assertThat(actual).contains(myTargetPatId); assertThat(actual).contains(myTargetEnc1); @@ -331,7 +334,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa assertThat(actual).contains(mySourceEncId1); assertThat(actual).contains(mySourceEncId2); assertThat(actual).contains(myOrgId); - assertThat(actual).contains(mySourceTaskId); + assertThat(actual).contains(mySourceCarePlanId); assertThat(actual).containsAll(mySourceObsIds); assertThat(actual).contains(myTargetPatId); assertThat(actual).contains(myTargetEnc1); @@ -402,7 +405,8 @@ private void assertUnprocessibleEntityWithMessage(Parameters inParameters, Strin assertThatThrownBy(() -> callMergeOperation(inParameters)) .isInstanceOf(UnprocessableEntityException.class) - .extracting(e -> extractFailureMessage((UnprocessableEntityException) e)) + .extracting(UnprocessableEntityException.class::cast) + .extracting(PatientMergeR4Test::extractFailureMessage) .isEqualTo(theExpectedMessage); } @@ -433,7 +437,55 @@ void test_MissingRequiredParameters_Returns400BadRequest() { .isEqualTo(400); } - // FIXME KHS look at PatientEverythingR4Test for ideas for other tests + @Test + void testReplaceReferences() throws IOException { + // exec + Parameters outParams = myClient.operation() + .onServer() + .named(OPERATION_REPLACE_REFERENCES) + .withParameter(Parameters.class, ProviderConstants.PARAM_SOURCE_REFERENCE_ID, new StringType(mySourcePatId.getValue())) + .andParameter(ProviderConstants.PARAM_TARGET_REFERENCE_ID, new StringType(myTargetPatId.getValue())) + .returnResourceType(Parameters.class) + .execute(); + + // validate + Pattern expectedPatchIssuePattern = Pattern.compile("Successfully patched resource \"(Observation|Encounter|CarePlan)/\\d+/_history/\\d+\"\\. Took \\d+ms\\."); + assertThat(outParams.getParameter()) + .hasSize(23) + .allSatisfy(component -> + assertThat(component.getResource()) + .isInstanceOf(OperationOutcome.class) + .extracting(OperationOutcome.class::cast) + .extracting(OperationOutcome::getIssue) + .satisfies(issues -> + assertThat(issues).hasSize(1) + .element(0) + .extracting(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics) + .satisfies(diagnostics -> assertThat(diagnostics).matches(expectedPatchIssuePattern)))); + + // Check that the linked resources were updated + + Bundle bundle = fetchBundle(myServerBase + "/" + myTargetPatId + "/$everything?_format=json&_count=100", EncodingEnum.JSON); + + assertNull(bundle.getLink("next")); + + Set actual = new HashSet<>(); + for (BundleEntryComponent nextEntry : bundle.getEntry()) { + actual.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless()); + } + + ourLog.info("Found IDs: {}", actual); + + assertThat(actual).contains(mySourceEncId1); + assertThat(actual).contains(mySourceEncId2); + assertThat(actual).contains(myOrgId); + assertThat(actual).contains(mySourceCarePlanId); + assertThat(actual).containsAll(mySourceObsIds); + assertThat(actual).contains(myTargetPatId); + assertThat(actual).contains(myTargetEnc1); + } + +// FIXME KHS look at PatientEverythingR4Test for ideas for other tests private Bundle fetchBundle(String theUrl, EncodingEnum theEncoding) throws IOException { Bundle bundle; From 0bfb3c18ee447194b3f7fd11e0b5e65a35614707 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 5 Dec 2024 14:12:20 -0500 Subject: [PATCH 031/148] kebab-case replace references --- .../ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java | 8 ++++---- .../uhn/fhir/rest/server/provider/ProviderConstants.java | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 8c2b8ffced0b..a099b4cbf249 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -485,7 +485,7 @@ void testReplaceReferences() throws IOException { assertThat(actual).contains(myTargetEnc1); } -// FIXME KHS look at PatientEverythingR4Test for ideas for other tests + // FIXME KHS look at PatientEverythingR4Test for ideas for other tests private Bundle fetchBundle(String theUrl, EncodingEnum theEncoding) throws IOException { Bundle bundle; @@ -501,7 +501,6 @@ private Bundle fetchBundle(String theUrl, EncodingEnum theEncoding) throws IOExc return bundle; } - private static class PatientMergeInputParameters { Type sourcePatient; Type sourcePatientIdentifier; @@ -554,7 +553,8 @@ public void handleTestExecutionException(ExtensionContext theExtensionContext, T String body = ex.getResponseBody(); Parameters outParams = ourFhirContext.newJsonParser().parseResource(Parameters.class, body); OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); - String message = outcome.getIssue().stream().map(issue -> issue.getDiagnostics()).collect(Collectors.joining(", ")); - return message; + return outcome.getIssue().stream() + .map(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics) + .collect(Collectors.joining(", ")); } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index 377bd53359ef..cabe6a312439 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -253,12 +253,12 @@ public class ProviderConstants { /** * Parameter for source reference of the "$replace-references" operation */ - public static final String PARAM_SOURCE_REFERENCE_ID = "sourceReferenceId"; + public static final String PARAM_SOURCE_REFERENCE_ID = "source-reference-id"; /** * Parameter for target reference of the "$replace-references" operation */ - public static final String PARAM_TARGET_REFERENCE_ID = "targetReferenceId"; + public static final String PARAM_TARGET_REFERENCE_ID = "target-reference-id"; /** * Operation name for the Resource "$merge" operation * Hapi-fhir use is based on https://www.hl7.org/fhir/patient-operation-merge.html From 497c8b0e8f366c8283b5bb6555bc62eb4e549536 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 6 Dec 2024 00:22:29 -0500 Subject: [PATCH 032/148] add sync -> async switch --- .../ca/uhn/fhir/jpa/config/JpaConfig.java | 5 +- .../fhir/jpa/dao/data/IResourceLinkDao.java | 5 + .../BaseJpaResourceProviderPatient.java | 384 +++++++++--------- .../fhir/jpa/provider/JpaSystemProvider.java | 71 ++-- .../provider/ReplaceReferencesSvcImpl.java | 204 +++++----- .../jpa/provider/StopLimitAccumulator.java | 44 ++ .../jpa/provider/r4/PatientMergeR4Test.java | 52 ++- .../fhir/rest/api/server/RequestDetails.java | 8 + .../fhir/rest/server/RestfulServerUtils.java | 16 +- .../server/provider/ProviderConstants.java | 24 +- .../merge/MergeOperationInputParameters.java | 9 + .../PatientMergeOperationInputParameters.java | 4 + .../jpa/dao/merge/ResourceMergeService.java | 4 +- .../jpa/provider/IReplaceReferencesSvc.java | 2 +- .../jpa/provider/ReplaceReferenceRequest.java | 56 +++ .../dao/merge/ResourceMergeServiceTest.java | 62 +-- 16 files changed, 575 insertions(+), 375 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java create mode 100644 hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index bb0e4964e9d5..74945247c9fe 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -55,6 +55,7 @@ import ca.uhn.fhir.jpa.dao.ResourceHistoryCalculator; import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; import ca.uhn.fhir.jpa.dao.TransactionProcessor; +import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; import ca.uhn.fhir.jpa.dao.data.IResourceModifiedDao; import ca.uhn.fhir.jpa.dao.data.IResourceSearchUrlDao; import ca.uhn.fhir.jpa.dao.data.ITagDefinitionDao; @@ -930,7 +931,7 @@ public CacheTagDefinitionDao tagDefinitionDao( } @Bean - public IReplaceReferencesSvc replaceReferencesSvc(FhirContext theFhirContext, DaoRegistry theDaoRegistry) { - return new ReplaceReferencesSvcImpl(theFhirContext, theDaoRegistry); + public IReplaceReferencesSvc replaceReferencesSvc(FhirContext theFhirContext, DaoRegistry theDaoRegistry, HapiTransactionService theHapiTransactionService, IdHelperService theIdHelperService, IResourceLinkDao theResourceLinkDao) { + return new ReplaceReferencesSvcImpl(theFhirContext, theDaoRegistry, theHapiTransactionService, theIdHelperService, theResourceLinkDao); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java index f848f23eb1a9..125e13a14e05 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java @@ -26,6 +26,7 @@ import org.springframework.data.repository.query.Param; import java.util.List; +import java.util.stream.Stream; public interface IResourceLinkDao extends JpaRepository, IHapiFhirJpaRepository { @@ -45,4 +46,8 @@ public interface IResourceLinkDao extends JpaRepository, IHa */ @Query("SELECT t FROM ResourceLink t LEFT JOIN FETCH t.myTargetResource tr WHERE t.myId in :pids") List findByPidAndFetchTargetDetails(@Param("pids") List thePids); + + @Query("SELECT DISTINCT t.mySourceResourcePid FROM ResourceLink t WHERE t.myTargetResourcePid = :resId") + Stream streamSourcePidsForTargetPid(@Param("resId") Long theTargetPid); + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 2a6625076ada..23c6f53b01e8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -49,6 +49,7 @@ import ca.uhn.fhir.util.CanonicalIdentifier; import ca.uhn.fhir.util.IdentifierUtil; import ca.uhn.fhir.util.ParametersUtil; +import jakarta.annotation.Nonnull; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.hl7.fhir.instance.model.api.IBaseParameters; @@ -65,6 +66,7 @@ import java.util.stream.Collectors; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; import static org.apache.commons.lang3.StringUtils.isNotBlank; public abstract class BaseJpaResourceProviderPatient extends BaseJpaResourceProvider { @@ -76,71 +78,71 @@ public abstract class BaseJpaResourceProviderPatient ex * Patient/123/$everything */ @Operation( - name = JpaConstants.OPERATION_EVERYTHING, - canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-everything", - idempotent = true, - bundleType = BundleTypeEnum.SEARCHSET) + name = JpaConstants.OPERATION_EVERYTHING, + canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-everything", + idempotent = true, + bundleType = BundleTypeEnum.SEARCHSET) public IBundleProvider patientInstanceEverything( - jakarta.servlet.http.HttpServletRequest theServletRequest, - @IdParam IIdType theId, - @Description( - shortDefinition = - "Results from this method are returned across multiple pages. This parameter controls the size of those pages.") - @OperationParam(name = Constants.PARAM_COUNT, typeName = "unsignedInt") - IPrimitiveType theCount, - @Description( - shortDefinition = - "Results from this method are returned across multiple pages. This parameter controls the offset when fetching a page.") - @OperationParam(name = Constants.PARAM_OFFSET, typeName = "unsignedInt") - IPrimitiveType theOffset, - @Description( - shortDefinition = - "Only return resources which were last updated as specified by the given range") - @OperationParam(name = Constants.PARAM_LASTUPDATED, min = 0, max = 1) - DateRangeParam theLastUpdated, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _content filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_CONTENT, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theContent, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _text filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_TEXT, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theNarrative, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _filter filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_FILTER, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theFilter, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_TYPE, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theTypes, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam(name = Constants.PARAM_MDM, min = 0, max = 1, typeName = "boolean") - IPrimitiveType theMdmExpand, - @Sort SortSpec theSortSpec, - RequestDetails theRequestDetails) { + jakarta.servlet.http.HttpServletRequest theServletRequest, + @IdParam IIdType theId, + @Description( + shortDefinition = + "Results from this method are returned across multiple pages. This parameter controls the size of those pages.") + @OperationParam(name = Constants.PARAM_COUNT, typeName = "unsignedInt") + IPrimitiveType theCount, + @Description( + shortDefinition = + "Results from this method are returned across multiple pages. This parameter controls the offset when fetching a page.") + @OperationParam(name = Constants.PARAM_OFFSET, typeName = "unsignedInt") + IPrimitiveType theOffset, + @Description( + shortDefinition = + "Only return resources which were last updated as specified by the given range") + @OperationParam(name = Constants.PARAM_LASTUPDATED, min = 0, max = 1) + DateRangeParam theLastUpdated, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _content filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_CONTENT, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theContent, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _text filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_TEXT, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theNarrative, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _filter filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_FILTER, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theFilter, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_TYPE, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theTypes, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam(name = Constants.PARAM_MDM, min = 0, max = 1, typeName = "boolean") + IPrimitiveType theMdmExpand, + @Sort SortSpec theSortSpec, + RequestDetails theRequestDetails) { startRequest(theServletRequest); try { @@ -156,7 +158,7 @@ public IBundleProvider patientInstanceEverything( everythingParams.setMdmExpand(resolveNullValue(theMdmExpand)); return ((IFhirResourceDaoPatient) getDao()) - .patientInstanceEverything(theServletRequest, theRequestDetails, everythingParams, theId); + .patientInstanceEverything(theServletRequest, theRequestDetails, everythingParams, theId); } finally { endRequest(theServletRequest); } @@ -166,77 +168,77 @@ public IBundleProvider patientInstanceEverything( * /Patient/$everything */ @Operation( - name = JpaConstants.OPERATION_EVERYTHING, - canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-everything", - idempotent = true, - bundleType = BundleTypeEnum.SEARCHSET) + name = JpaConstants.OPERATION_EVERYTHING, + canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-everything", + idempotent = true, + bundleType = BundleTypeEnum.SEARCHSET) public IBundleProvider patientTypeEverything( - jakarta.servlet.http.HttpServletRequest theServletRequest, - @Description( - shortDefinition = - "Results from this method are returned across multiple pages. This parameter controls the size of those pages.") - @OperationParam(name = Constants.PARAM_COUNT, typeName = "unsignedInt") - IPrimitiveType theCount, - @Description( - shortDefinition = - "Results from this method are returned across multiple pages. This parameter controls the offset when fetching a page.") - @OperationParam(name = Constants.PARAM_OFFSET, typeName = "unsignedInt") - IPrimitiveType theOffset, - @Description( - shortDefinition = - "Only return resources which were last updated as specified by the given range") - @OperationParam(name = Constants.PARAM_LASTUPDATED, min = 0, max = 1) - DateRangeParam theLastUpdated, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _content filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_CONTENT, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theContent, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _text filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_TEXT, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theNarrative, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _filter filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_FILTER, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theFilter, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_TYPE, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theTypes, - @Description(shortDefinition = "Filter the resources to return based on the patient ids provided.") - @OperationParam( - name = Constants.PARAM_ID, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "id") - List theId, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam(name = Constants.PARAM_MDM, min = 0, max = 1, typeName = "boolean") - IPrimitiveType theMdmExpand, - @Sort SortSpec theSortSpec, - RequestDetails theRequestDetails) { + jakarta.servlet.http.HttpServletRequest theServletRequest, + @Description( + shortDefinition = + "Results from this method are returned across multiple pages. This parameter controls the size of those pages.") + @OperationParam(name = Constants.PARAM_COUNT, typeName = "unsignedInt") + IPrimitiveType theCount, + @Description( + shortDefinition = + "Results from this method are returned across multiple pages. This parameter controls the offset when fetching a page.") + @OperationParam(name = Constants.PARAM_OFFSET, typeName = "unsignedInt") + IPrimitiveType theOffset, + @Description( + shortDefinition = + "Only return resources which were last updated as specified by the given range") + @OperationParam(name = Constants.PARAM_LASTUPDATED, min = 0, max = 1) + DateRangeParam theLastUpdated, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _content filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_CONTENT, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theContent, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _text filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_TEXT, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theNarrative, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _filter filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_FILTER, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theFilter, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_TYPE, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theTypes, + @Description(shortDefinition = "Filter the resources to return based on the patient ids provided.") + @OperationParam( + name = Constants.PARAM_ID, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "id") + List theId, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam(name = Constants.PARAM_MDM, min = 0, max = 1, typeName = "boolean") + IPrimitiveType theMdmExpand, + @Sort SortSpec theSortSpec, + RequestDetails theRequestDetails) { startRequest(theServletRequest); try { @@ -252,11 +254,11 @@ public IBundleProvider patientTypeEverything( everythingParams.setMdmExpand(resolveNullValue(theMdmExpand)); return ((IFhirResourceDaoPatient) getDao()) - .patientTypeEverything( - theServletRequest, - theRequestDetails, - everythingParams, - toFlattenedPatientIdTokenParamList(theId)); + .patientTypeEverything( + theServletRequest, + theRequestDetails, + everythingParams, + toFlattenedPatientIdTokenParamList(theId)); } finally { endRequest(theServletRequest); } @@ -266,37 +268,40 @@ public IBundleProvider patientTypeEverything( * /Patient/$merge */ @Operation( - name = ProviderConstants.OPERATION_MERGE, - canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge") + name = ProviderConstants.OPERATION_MERGE, + canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge") public IBaseParameters patientMerge( - HttpServletRequest theServletRequest, - HttpServletResponse theServletResponse, - ServletRequestDetails theRequestDetails, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER) - List theSourcePatientIdentifier, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER) - List theTargetPatientIdentifier, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT, max = 1) - IBaseReference theSourcePatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT, max = 1) - IBaseReference theTargetPatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PREVIEW, typeName = "boolean", max = 1) - IPrimitiveType thePreview, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_DELETE_SOURCE, typeName = "boolean", max = 1) - IPrimitiveType theDeleteSource, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_RESULT_PATIENT, max = 1) - IBaseResource theResultPatient) { + HttpServletRequest theServletRequest, + HttpServletResponse theServletResponse, + ServletRequestDetails theRequestDetails, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER) + List theSourcePatientIdentifier, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER) + List theTargetPatientIdentifier, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT, max = 1) + IBaseReference theSourcePatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT, max = 1) + IBaseReference theTargetPatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PREVIEW, typeName = "boolean", max = 1) + IPrimitiveType thePreview, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_DELETE_SOURCE, typeName = "boolean", max = 1) + IPrimitiveType theDeleteSource, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_RESULT_PATIENT, max = 1) + IBaseResource theResultPatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PAGE_SIZE, typeName = "unsignedInt") IPrimitiveType thePageSize) { startRequest(theServletRequest); + @Nonnull Integer pageSize = defaultIfNull(IPrimitiveType.toValueOrNull(thePageSize), myStorageSettings.getInternalSynchronousSearchSize()); try { MergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( - theSourcePatientIdentifier, - theTargetPatientIdentifier, - theSourcePatient, - theTargetPatient, - thePreview, - theDeleteSource, - theResultPatient); + theSourcePatientIdentifier, + theTargetPatientIdentifier, + theSourcePatient, + theTargetPatient, + thePreview, + theDeleteSource, + theResultPatient, + pageSize); IFhirResourceDaoPatient dao = (IFhirResourceDaoPatient) getDao(); ResourceMergeService resourceMergeService = new ResourceMergeService(dao, myReplaceReferencesSvc); @@ -304,7 +309,7 @@ public IBaseParameters patientMerge( FhirContext fhirContext = dao.getContext(); MergeOperationOutcome mergeOutcome = - resourceMergeService.merge(mergeOperationParameters, theRequestDetails); + resourceMergeService.merge(mergeOperationParameters, theRequestDetails); theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); return buildMergeOperationOutputParameters(fhirContext, mergeOutcome, theRequestDetails.getResource()); @@ -314,47 +319,48 @@ public IBaseParameters patientMerge( } private IBaseParameters buildMergeOperationOutputParameters( - FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome, IBaseResource theInputParameters) { + FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome, IBaseResource theInputParameters) { IBaseParameters retVal = ParametersUtil.newInstance(theFhirContext); ParametersUtil.addParameterToParameters( - theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT, theInputParameters); + theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT, theInputParameters); ParametersUtil.addParameterToParameters( - theFhirContext, - retVal, - ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME, - theMergeOutcome.getOperationOutcome()); + theFhirContext, + retVal, + ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME, + theMergeOutcome.getOperationOutcome()); if (theMergeOutcome.getUpdatedTargetResource() != null) { ParametersUtil.addParameterToParameters( - theFhirContext, - retVal, - OPERATION_MERGE_OUTPUT_PARAM_RESULT, - theMergeOutcome.getUpdatedTargetResource()); + theFhirContext, + retVal, + OPERATION_MERGE_OUTPUT_PARAM_RESULT, + theMergeOutcome.getUpdatedTargetResource()); } return retVal; } private MergeOperationInputParameters buildMergeOperationInputParameters( - List theSourcePatientIdentifier, - List theTargetPatientIdentifier, - IBaseReference theSourcePatient, - IBaseReference theTargetPatient, - IPrimitiveType thePreview, - IPrimitiveType theDeleteSource, - IBaseResource theResultPatient) { - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + List theSourcePatientIdentifier, + List theTargetPatientIdentifier, + IBaseReference theSourcePatient, + IBaseReference theTargetPatient, + IPrimitiveType thePreview, + IPrimitiveType theDeleteSource, + IBaseResource theResultPatient, + int thePageSize) { + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(thePageSize); if (theSourcePatientIdentifier != null) { List sourceResourceIdentifiers = theSourcePatientIdentifier.stream() - .map(IdentifierUtil::identifierDtFromIdentifier) - .collect(Collectors.toList()); + .map(IdentifierUtil::identifierDtFromIdentifier) + .collect(Collectors.toList()); mergeOperationParameters.setSourceResourceIdentifiers(sourceResourceIdentifiers); } if (theTargetPatientIdentifier != null) { List targetResourceIdentifiers = theTargetPatientIdentifier.stream() - .map(IdentifierUtil::identifierDtFromIdentifier) - .collect(Collectors.toList()); + .map(IdentifierUtil::identifierDtFromIdentifier) + .collect(Collectors.toList()); mergeOperationParameters.setTargetResourceIdentifiers(targetResourceIdentifiers); } mergeOperationParameters.setSourceResource(theSourcePatient); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index 9954a7e7c7d2..f0b7b03cd261 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -19,9 +19,11 @@ */ package ca.uhn.fhir.jpa.provider; +import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.Transaction; @@ -35,28 +37,32 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import java.security.InvalidParameterException; import java.util.Collections; import java.util.Map; import java.util.TreeMap; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static software.amazon.awssdk.utils.StringUtils.isBlank; public final class JpaSystemProvider extends BaseJpaSystemProvider { @Description( - "Marks all currently existing resources of a given type, or all resources of all types, for reindexing.") + "Marks all currently existing resources of a given type, or all resources of all types, for reindexing.") @Operation( - name = MARK_ALL_RESOURCES_FOR_REINDEXING, - idempotent = false, - returnParameters = {@OperationParam(name = "status")}) + name = MARK_ALL_RESOURCES_FOR_REINDEXING, + idempotent = false, + returnParameters = {@OperationParam(name = "status")}) /** * @deprecated * @see ReindexProvider#Reindex(List, IPrimitiveType, RequestDetails) */ @Deprecated public IBaseResource markAllResourcesForReindexing( - @OperationParam(name = "type", min = 0, max = 1, typeName = "code") IPrimitiveType theType) { + @OperationParam(name = "type", min = 0, max = 1, typeName = "code") IPrimitiveType theType) { if (theType != null && isNotBlank(theType.getValueAsString())) { getResourceReindexingSvc().markAllResourcesForReindexing(theType.getValueAsString()); @@ -74,9 +80,9 @@ public IBaseResource markAllResourcesForReindexing( @Description("Forces a single pass of the resource reindexing processor") @Operation( - name = PERFORM_REINDEXING_PASS, - idempotent = false, - returnParameters = {@OperationParam(name = "status")}) + name = PERFORM_REINDEXING_PASS, + idempotent = false, + returnParameters = {@OperationParam(name = "status")}) /** * @deprecated * @see ReindexProvider#Reindex(List, IPrimitiveType, RequestDetails) @@ -100,8 +106,8 @@ public IBaseResource performReindexingPass() { @Operation(name = JpaConstants.OPERATION_GET_RESOURCE_COUNTS, idempotent = true) @Description( - shortDefinition = - "Provides the number of resources currently stored on the server, broken down by resource type") + shortDefinition = + "Provides the number of resources currently stored on the server, broken down by resource type") public IBaseParameters getResourceCounts() { IBaseParameters retVal = ParametersUtil.newInstance(getContext()); @@ -110,23 +116,23 @@ public IBaseParameters getResourceCounts() { counts = new TreeMap<>(counts); for (Map.Entry nextEntry : counts.entrySet()) { ParametersUtil.addParameterToParametersInteger( - getContext(), - retVal, - nextEntry.getKey(), - nextEntry.getValue().intValue()); + getContext(), + retVal, + nextEntry.getKey(), + nextEntry.getValue().intValue()); } return retVal; } @Operation( - name = ProviderConstants.OPERATION_META, - idempotent = true, - returnParameters = {@OperationParam(name = "return", typeName = "Meta")}) + name = ProviderConstants.OPERATION_META, + idempotent = true, + returnParameters = {@OperationParam(name = "return", typeName = "Meta")}) public IBaseParameters meta(RequestDetails theRequestDetails) { IBaseParameters retVal = ParametersUtil.newInstance(getContext()); ParametersUtil.addParameterToParameters( - getContext(), retVal, "return", getDao().metaGetOperation(theRequestDetails)); + getContext(), retVal, "return", getDao().metaGetOperation(theRequestDetails)); return retVal; } @@ -144,14 +150,29 @@ public IBaseBundle transaction(RequestDetails theRequestDetails, @TransactionPar @Operation(name = ProviderConstants.OPERATION_REPLACE_REFERENCES, global = true) @Description( - value = - "This operation searches for all references matching the provided id and updates them to references to the provided newReferenceTargetId.", - shortDefinition = "Repoints referencing resources to another resources instance") + value = + "This operation searches for all references matching the provided id and updates them to references to the provided newReferenceTargetId.", + shortDefinition = "Repoints referencing resources to another resources instance") public IBaseParameters replaceReferences( - @OperationParam(name = ProviderConstants.PARAM_SOURCE_REFERENCE_ID) String theSourceId, - @OperationParam(name = ProviderConstants.PARAM_TARGET_REFERENCE_ID) String theTargetId, - RequestDetails theRequest) { + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID) String theSourceId, + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID) String theTargetId, + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PAGE_SIZE, typeName = "unsignedInt") IPrimitiveType thePageSize, + RequestDetails theRequestDetails) { + validateReplaceReferencesParams(theSourceId, theTargetId); + Integer pageSize = defaultIfNull(IPrimitiveType.toValueOrNull(thePageSize), myStorageSettings.getInternalSynchronousSearchSize()); + ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest(new IdDt(theSourceId), new IdDt(theTargetId), pageSize); + return getReplaceReferencesSvc().replaceReferences(replaceReferenceRequest, theRequestDetails); + } + + private static void validateReplaceReferencesParams(String theSourceId, String theTargetId) { + if (isBlank(theSourceId)) { + throw new InvalidParameterException( + Msg.code(2583) + "Parameter '" + OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID + "' is blank"); + } - return getReplaceReferencesSvc().replaceReferences(theSourceId, theTargetId, theRequest); + if (isBlank(theTargetId)) { + throw new InvalidParameterException( + Msg.code(2584) + "Parameter '" + OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID + "' is blank"); + } } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index a0f2fddfae7d..0ee0746c630e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -21,15 +21,17 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; +import ca.uhn.fhir.jpa.dao.index.IdHelperService; +import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; +import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.PatchTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.ResourceReferenceInfo; import jakarta.annotation.Nonnull; @@ -42,146 +44,194 @@ import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Type; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.security.InvalidParameterException; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static ca.uhn.fhir.jpa.patch.FhirPatch.OPERATION_REPLACE; import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_OPERATION; import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_PATH; import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_TYPE; import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_VALUE; -import static ca.uhn.fhir.rest.api.Constants.PARAM_ID; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.PARAM_SOURCE_REFERENCE_ID; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.PARAM_TARGET_REFERENCE_ID; -import static software.amazon.awssdk.utils.StringUtils.isBlank; public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { - + private static final Logger ourLog = LoggerFactory.getLogger(ReplaceReferencesSvcImpl.class); private final FhirContext myFhirContext; private final DaoRegistry myDaoRegistry; + private final HapiTransactionService myHapiTransactionService; + private final IdHelperService myIdHelperService; + private final IResourceLinkDao myResourceLinkDao; - public ReplaceReferencesSvcImpl(FhirContext theFhirContext, DaoRegistry theDaoRegistry) { + public ReplaceReferencesSvcImpl(FhirContext theFhirContext, DaoRegistry theDaoRegistry, HapiTransactionService theHapiTransactionService, IdHelperService theIdHelperService, IResourceLinkDao theResourceLinkDao) { myFhirContext = theFhirContext; myDaoRegistry = theDaoRegistry; + myHapiTransactionService = theHapiTransactionService; + myIdHelperService = theIdHelperService; + myResourceLinkDao = theResourceLinkDao; } @Override - public IBaseParameters replaceReferences(String theSourceRefId, String theTargetRefId, RequestDetails theRequest) { + public IBaseParameters replaceReferences(ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + theReplaceReferenceRequest.validateOrThrowInvalidParameterException(); + + if (theRequestDetails.isPreferAsync()) { + return replaceReferencesPreferAsync(theReplaceReferenceRequest, theRequestDetails); + } else { + return replaceReferencesPreferSync(theReplaceReferenceRequest, theRequestDetails); + } + } - validateParameters(theSourceRefId, theTargetRefId); - IIdType sourceRefId = new IdDt(theSourceRefId); - IIdType targetRefId = new IdDt(theTargetRefId); + private IBaseParameters replaceReferencesPreferAsync(ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + // FIXME KHS + return null; + } + + /** + * Try to perform the operation synchronously. However if there is more than a page of results, fall back to asynchronous operation + */ + private @NotNull IBaseParameters replaceReferencesPreferSync(ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // todo jm: this could be problematic depending on referenceing object set size, however we are adding // batch job option to handle that case as part of this feature - List referencingResources = findReferencingResourceIds(sourceRefId, theRequest); + IFhirResourceDao dao = getDao(theReplaceReferenceRequest.sourceId.getResourceType()); + if (dao == null) { + throw new InternalErrorException( + Msg.code(2582) + "Couldn't obtain DAO for resource type" + theReplaceReferenceRequest.sourceId.getResourceType()); + } + + return myHapiTransactionService.withRequest(theRequestDetails).execute( + () -> performReplaceInTransaction(theReplaceReferenceRequest, theRequestDetails, dao)); + } + + private @Nullable IBaseParameters performReplaceInTransaction(ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails, IFhirResourceDao dao) { + // FIXME KHS get partition from request + JpaPid sourcePid = myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); + + Stream pidStream = myResourceLinkDao.streamSourcePidsForTargetPid(sourcePid.getId()).map(JpaPid::fromId); + StopLimitAccumulator accumulator = StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.pageSize); + + if (accumulator.isTruncated()) { + ourLog.info("Too many results. Switching to asynchronous reference replacement."); + return replaceReferencesPreferAsync(theReplaceReferenceRequest, theRequestDetails); + } + + List referencingResources = accumulator.getItemList().stream().map(myIdHelperService::translatePidIdToForcedIdWithCache) + .filter(Optional::isPresent) + .map(Optional::get) + .map(IdDt::new) + .map(id -> getDao(id.getResourceType()).read(id, theRequestDetails)) + .collect(Collectors.toUnmodifiableList()); - return replaceReferencesInTransaction(referencingResources, sourceRefId, targetRefId, theRequest); + return replaceReferencesInTransaction(referencingResources, theReplaceReferenceRequest, theRequestDetails); } private IBaseParameters replaceReferencesInTransaction( - List theReferencingResources, - IIdType theCurrentTargetId, - IIdType theNewTargetId, - RequestDetails theRequest) { + List theReferencingResources, + ReplaceReferenceRequest theReplaceReferenceRequest, + RequestDetails theRequestDetails) { Parameters resultParams = new Parameters(); // map resourceType -> map resourceId -> patch Parameters Map> parametersMap = - buildPatchParameterMap(theReferencingResources, theCurrentTargetId, theNewTargetId); + buildPatchParameterMap(theReferencingResources, theReplaceReferenceRequest.sourceId, theReplaceReferenceRequest.targetId); for (Map.Entry> mapEntry : parametersMap.entrySet()) { String resourceType = mapEntry.getKey(); IFhirResourceDao resDao = myDaoRegistry.getResourceDao(resourceType); if (resDao == null) { throw new InternalErrorException( - Msg.code(2588) + "No DAO registered for resource type: " + resourceType); + Msg.code(2588) + "No DAO registered for resource type: " + resourceType); } // patch each resource of resourceType - patchResourceTypeResources(mapEntry, resDao, resultParams, theRequest); + patchResourceTypeResources(mapEntry, resDao, resultParams, theRequestDetails); } return resultParams; } private void patchResourceTypeResources( - Map.Entry> mapEntry, - IFhirResourceDao resDao, - Parameters resultParams, - RequestDetails theRequest) { + Map.Entry> mapEntry, + IFhirResourceDao resDao, + Parameters resultParams, + RequestDetails theRequest) { for (Map.Entry idParamMapEntry : - mapEntry.getValue().entrySet()) { + mapEntry.getValue().entrySet()) { IIdType resourceId = idParamMapEntry.getKey(); Parameters parameters = idParamMapEntry.getValue(); MethodOutcome result = - resDao.patch(resourceId, null, PatchTypeEnum.FHIR_PATCH_JSON, null, parameters, theRequest); + resDao.patch(resourceId, null, PatchTypeEnum.FHIR_PATCH_JSON, null, parameters, theRequest); resultParams.addParameter().setResource((Resource) result.getOperationOutcome()); } } private Map> buildPatchParameterMap( - List theReferencingResources, - IIdType theCurrentReferencedResourceId, - IIdType theNewReferencedResourceId) { + List theReferencingResources, + IIdType theCurrentReferencedResourceId, + IIdType theNewReferencedResourceId) { Map> paramsMap = new HashMap<>(); for (IBaseResource referencingResource : theReferencingResources) { // resource can have more than one reference to the same target resource for (ResourceReferenceInfo refInfo : - myFhirContext.newTerser().getAllResourceReferences(referencingResource)) { + myFhirContext.newTerser().getAllResourceReferences(referencingResource)) { addReferenceToMapIfForSource( - theCurrentReferencedResourceId, - theNewReferencedResourceId, - referencingResource, - refInfo, - paramsMap); + theCurrentReferencedResourceId, + theNewReferencedResourceId, + referencingResource, + refInfo, + paramsMap); } } return paramsMap; } private void addReferenceToMapIfForSource( - IIdType theCurrentReferencedResourceId, - IIdType theNewReferencedResourceId, - IBaseResource referencingResource, - ResourceReferenceInfo refInfo, - Map> paramsMap) { + IIdType theCurrentReferencedResourceId, + IIdType theNewReferencedResourceId, + IBaseResource referencingResource, + ResourceReferenceInfo refInfo, + Map> paramsMap) { if (!refInfo.getResourceReference() - .getReferenceElement() + .getReferenceElement() + .toUnqualifiedVersionless() + .getValueAsString() + .equals(theCurrentReferencedResourceId .toUnqualifiedVersionless() - .getValueAsString() - .equals(theCurrentReferencedResourceId - .toUnqualifiedVersionless() - .getValueAsString())) { + .getValueAsString())) { // not a reference to the resource being replaced return; } Parameters.ParametersParameterComponent paramComponent = createReplaceReferencePatchOperation( - referencingResource.fhirType() + "." + refInfo.getName(), - new Reference( - theNewReferencedResourceId.toUnqualifiedVersionless().getValueAsString())); + referencingResource.fhirType() + "." + refInfo.getName(), + new Reference( + theNewReferencedResourceId.toUnqualifiedVersionless().getValueAsString())); paramsMap - // preserve order, in case it could matter - .computeIfAbsent(referencingResource.fhirType(), k -> new LinkedHashMap<>()) - .computeIfAbsent(referencingResource.getIdElement(), k -> new Parameters()) - .addParameter(paramComponent); + // preserve order, in case it could matter + .computeIfAbsent(referencingResource.fhirType(), k -> new LinkedHashMap<>()) + .computeIfAbsent(referencingResource.getIdElement(), k -> new Parameters()) + .addParameter(paramComponent); } @Nonnull private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( - String thePath, Type theValue) { + String thePath, Type theValue) { Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); operation.setName(PARAMETER_OPERATION); @@ -191,50 +241,8 @@ private Parameters.ParametersParameterComponent createReplaceReferencePatchOpera return operation; } - private List findReferencingResourceIds( - IIdType theSourceRefIdParam, RequestDetails theRequest) { - IFhirResourceDao dao = getDao(theSourceRefIdParam.getResourceType()); - if (dao == null) { - throw new InternalErrorException( - Msg.code(2582) + "Couldn't obtain DAO for resource type" + theSourceRefIdParam.getResourceType()); - } - - SearchParameterMap parameterMap = new SearchParameterMap(); - parameterMap.add(PARAM_ID, new StringParam(theSourceRefIdParam.getValue())); - parameterMap.addRevInclude(new Include("*")); - return dao.search(parameterMap, theRequest).getAllResources(); - } - private IFhirResourceDao getDao(String theResourceName) { return myDaoRegistry.getResourceDao(theResourceName); } - private void validateParameters(String theSourceRefIdParam, String theTargetRefIdParam) { - if (isBlank(theSourceRefIdParam)) { - throw new InvalidParameterException( - Msg.code(2583) + "Parameter '" + PARAM_SOURCE_REFERENCE_ID + "' is blank"); - } - - if (isBlank(theTargetRefIdParam)) { - throw new InvalidParameterException( - Msg.code(2584) + "Parameter '" + PARAM_TARGET_REFERENCE_ID + "' is blank"); - } - - IIdType sourceId = new IdDt(theSourceRefIdParam); - if (isBlank(sourceId.getResourceType())) { - throw new InvalidParameterException( - Msg.code(2585) + "'" + PARAM_SOURCE_REFERENCE_ID + "' must be a resource type qualified id"); - } - - IIdType targetId = new IdDt(theTargetRefIdParam); - if (isBlank(targetId.getResourceType())) { - throw new InvalidParameterException( - Msg.code(2586) + "'" + PARAM_TARGET_REFERENCE_ID + "' must be a resource type qualified id"); - } - - if (!targetId.getResourceType().equals(sourceId.getResourceType())) { - throw new InvalidParameterException( - Msg.code(2587) + "Source and target id parameters must be for the same resource type"); - } - } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java new file mode 100644 index 000000000000..b8108e6a0a8d --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java @@ -0,0 +1,44 @@ +package ca.uhn.fhir.jpa.provider; + +import jakarta.annotation.Nonnull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; + +// FIXME KHS test +class StopLimitAccumulator { + private final boolean isTruncated; + private final List myList; + + StopLimitAccumulator(List theList, boolean theIsTruncated) { + myList = Collections.unmodifiableList(theList); + isTruncated = theIsTruncated; + } + + static StopLimitAccumulator fromStreamAndLimit(@Nonnull Stream thePidStream, int theLimit) { + AtomicBoolean isBeyondLimit = new AtomicBoolean(false); + List accumulator = new ArrayList<>(); + + thePidStream + .limit(theLimit + 1) // Fetch one extra item to see if there are more items past our limit + .forEach(item -> { + if (accumulator.size() < theLimit) { + accumulator.add(item); + } else { + isBeyondLimit.set(true); + } + }); + return new StopLimitAccumulator<>(accumulator, isBeyondLimit.get()); + } + + public boolean isTruncated() { + return isTruncated; + } + + public List getItemList() { + return myList; + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index a099b4cbf249..953202078988 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -43,9 +43,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; +import org.junit.jupiter.params.provider.ValueSource; import java.io.IOException; import java.util.ArrayList; @@ -69,7 +70,6 @@ import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; public class PatientMergeR4Test extends BaseResourceProviderR4Test { @@ -234,7 +234,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa // Assert Task if (isAsync) { Task task = (Task) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource(); - await().until(() -> task.getStatus() == Task.TaskStatus.COMPLETED); + await().until(() -> taskCompleted(task.getIdElement())); Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); @@ -307,7 +307,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa // Check that the linked resources were updated - Bundle bundle = fetchBundle(myServerBase + "/" + myTargetPatId + "/$everything?_format=json&_count=100", EncodingEnum.JSON); + Bundle bundle = fetchBundle(myServerBase + "/" + myTargetPatId + "/$everything?_format=json&_count=100"); assertNull(bundle.getLink("next")); @@ -341,6 +341,11 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa } } + private Boolean taskCompleted(IdType theTask) { + Task updatedTask = myTaskDao.read(theTask, mySrd); + return updatedTask.getStatus() == Task.TaskStatus.COMPLETED; + } + @ParameterizedTest @CsvSource({ // withDelete, withInputResultPatient, withPreview @@ -353,7 +358,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa "false, true, false", "false, false, false", }) - public void testMultipleTargetMatchesFails(boolean withDelete, boolean withInputResultPatient, boolean withPreview) throws Exception { + public void testMultipleTargetMatchesFails(boolean withDelete, boolean withInputResultPatient, boolean withPreview) { PatientMergeInputParameters inParams = new PatientMergeInputParameters(); inParams.sourcePatient = new Reference().setReferenceElement(mySourcePatId); inParams.targetPatientIdentifier = patBothIdentifierC; @@ -384,7 +389,7 @@ public void testMultipleTargetMatchesFails(boolean withDelete, boolean withInput "false, true, false", "false, false, false", }) - public void testMultipleSourceMatchesFails(boolean withDelete, boolean withInputResultPatient, boolean withPreview) throws Exception { + public void testMultipleSourceMatchesFails(boolean withDelete, boolean withInputResultPatient, boolean withPreview) { PatientMergeInputParameters inParams = new PatientMergeInputParameters(); inParams.sourcePatientIdentifier = patBothIdentifierC; inParams.targetPatient = new Reference().setReferenceElement(mySourcePatId); @@ -410,8 +415,8 @@ private void assertUnprocessibleEntityWithMessage(Parameters inParameters, Strin .isEqualTo(theExpectedMessage); } - private Parameters callMergeOperation(Parameters inParameters) { - return this.callMergeOperation(inParameters, false); + private void callMergeOperation(Parameters inParameters) { + this.callMergeOperation(inParameters, false); } private Parameters callMergeOperation(Parameters inParameters, boolean isAsync) { @@ -433,18 +438,26 @@ private Parameters callMergeOperation(Parameters inParameters, boolean isAsync) void test_MissingRequiredParameters_Returns400BadRequest() { assertThatThrownBy(() -> callMergeOperation(new Parameters()) ).isInstanceOf(InvalidRequestException.class) - .extracting(e -> ((InvalidRequestException) e).getStatusCode()) + .extracting(InvalidRequestException.class::cast) + .extracting(BaseServerResponseException::getStatusCode) .isEqualTo(400); } - @Test - void testReplaceReferences() throws IOException { + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void testReplaceReferences(boolean isAsync) throws IOException { // exec - Parameters outParams = myClient.operation() + IOperationUntypedWithInput request = myClient.operation() .onServer() .named(OPERATION_REPLACE_REFERENCES) - .withParameter(Parameters.class, ProviderConstants.PARAM_SOURCE_REFERENCE_ID, new StringType(mySourcePatId.getValue())) - .andParameter(ProviderConstants.PARAM_TARGET_REFERENCE_ID, new StringType(myTargetPatId.getValue())) + .withParameter(Parameters.class, ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID, new StringType(mySourcePatId.getValue())) + .andParameter(ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID, new StringType(myTargetPatId.getValue())); + + if (isAsync) { + request.withAdditionalHeader(HEADER_PREFER, HEADER_PREFER_RESPOND_ASYNC); + } + + Parameters outParams = request .returnResourceType(Parameters.class) .execute(); @@ -465,7 +478,7 @@ void testReplaceReferences() throws IOException { // Check that the linked resources were updated - Bundle bundle = fetchBundle(myServerBase + "/" + myTargetPatId + "/$everything?_format=json&_count=100", EncodingEnum.JSON); + Bundle bundle = fetchBundle(myServerBase + "/" + myTargetPatId + "/$everything?_format=json&_count=100"); assertNull(bundle.getLink("next")); @@ -487,13 +500,13 @@ void testReplaceReferences() throws IOException { // FIXME KHS look at PatientEverythingR4Test for ideas for other tests - private Bundle fetchBundle(String theUrl, EncodingEnum theEncoding) throws IOException { + private Bundle fetchBundle(String theUrl) throws IOException { Bundle bundle; HttpGet get = new HttpGet(theUrl); CloseableHttpResponse resp = ourHttpClient.execute(get); try { - assertEquals(theEncoding.getResourceContentTypeNonLegacy(), resp.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue().replaceAll(";.*", "")); - bundle = theEncoding.newParser(myFhirContext).parseResource(Bundle.class, IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8)); + assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy(), resp.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue().replaceAll(";.*", "")); + bundle = EncodingEnum.JSON.newParser(myFhirContext).parseResource(Bundle.class, IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8)); } finally { IOUtils.closeQuietly(resp); } @@ -541,7 +554,8 @@ public Parameters asParametersResource() { static class MyExceptionHandler implements TestExecutionExceptionHandler { @Override public void handleTestExecutionException(ExtensionContext theExtensionContext, Throwable theThrowable) throws Throwable { - if (theThrowable instanceof BaseServerResponseException ex) { + if (theThrowable instanceof BaseServerResponseException) { + BaseServerResponseException ex = (BaseServerResponseException) theThrowable; String message = extractFailureMessage(ex); throw ex.getClass().getDeclaredConstructor(String.class, Throwable.class).newInstance(message, ex); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java index e3f6d852f709..88a14e5e2fff 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java @@ -23,9 +23,11 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.PreferHeader; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.server.IRestfulServerDefaults; +import ca.uhn.fhir.rest.server.RestfulServerUtils; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.UrlUtil; @@ -609,4 +611,10 @@ public boolean isRetry() { public void setRetry(boolean theRetry) { myRetry = theRetry; } + + public boolean isPreferAsync() { + String prefer = getHeader(Constants.HEADER_PREFER); + PreferHeader preferHeader = RestfulServerUtils.parsePreferHeader(prefer); + return preferHeader.getRespondAsync(); + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java index 8477293b90be..7d707c29152c 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java @@ -791,6 +791,17 @@ public static boolean respectPreferHeader(RestOperationTypeEnum theRestOperation @Nonnull public static PreferHeader parsePreferHeader(IRestfulServer theServer, String theValue) { + PreferHeader retVal = parsePreferHeader(theValue); + + if (retVal.getReturn() == null && theServer != null && theServer.getDefaultPreferReturn() != null) { + retVal.setReturn(theServer.getDefaultPreferReturn()); + } + + return retVal; + } + + @Nonnull + public static PreferHeader parsePreferHeader(String theValue) { PreferHeader retVal = new PreferHeader(); if (isNotBlank(theValue)) { @@ -825,11 +836,6 @@ public static PreferHeader parsePreferHeader(IRestfulServer theServer, String } } } - - if (retVal.getReturn() == null && theServer != null && theServer.getDefaultPreferReturn() != null) { - retVal.setReturn(theServer.getDefaultPreferReturn()); - } - return retVal; } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index cabe6a312439..26d83713aa7e 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -132,7 +132,7 @@ public class ProviderConstants { public static final String OPERATION_META = "$meta"; /** - * Operation name for the $expunge operation + * Operation name for the $expunge operation */ public static final String OPERATION_EXPUNGE = "$expunge"; @@ -253,12 +253,25 @@ public class ProviderConstants { /** * Parameter for source reference of the "$replace-references" operation */ - public static final String PARAM_SOURCE_REFERENCE_ID = "source-reference-id"; + public static final String OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID = "source-reference-id"; /** * Parameter for target reference of the "$replace-references" operation */ - public static final String PARAM_TARGET_REFERENCE_ID = "target-reference-id"; + public static final String OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID = "target-reference-id"; + + /** + * The number of resources that will be modified at a time. If the number of resources that need to change + * exceeds this amount, the operation will switch to async mode. + */ + public static final String OPERATION_REPLACE_REFERENCES_PAGE_SIZE = "page-size"; + + /** + * $replace-references output Parameters names + */ + public static final String OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK = "task"; + public static final String OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME = "outcome"; + /** * Operation name for the Resource "$merge" operation * Hapi-fhir use is based on https://www.hl7.org/fhir/patient-operation-merge.html @@ -273,10 +286,11 @@ public class ProviderConstants { public static final String OPERATION_MERGE_TARGET_PATIENT = "target-patient"; public static final String OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER = "target-patient-identifier"; public static final String OPERATION_MERGE_RESULT_PATIENT = "result-patient"; + public static final String OPERATION_MERGE_PAGE_SIZE = "page-size"; public static final String OPERATION_MERGE_PREVIEW = "preview"; public static final String OPERATION_MERGE_DELETE_SOURCE = "delete-source"; public static final String OPERATION_MERGE_OUTPUT_PARAM_INPUT = "input"; - public static final String OPERATION_MERGE_OUTPUT_PARAM_OUTCOME = "outcome"; + public static final String OPERATION_MERGE_OUTPUT_PARAM_OUTCOME = OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; public static final String OPERATION_MERGE_OUTPUT_PARAM_RESULT = "result"; - public static final String OPERATION_MERGE_OUTPUT_PARAM_TASK = "task"; + public static final String OPERATION_MERGE_OUTPUT_PARAM_TASK = OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationInputParameters.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationInputParameters.java index 342928bb6275..0df95d13daac 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationInputParameters.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationInputParameters.java @@ -34,6 +34,11 @@ public abstract class MergeOperationInputParameters { private boolean myPreview; private boolean myDeleteSource; private IBaseResource myResultResource; + private final int myPageSize; + + protected MergeOperationInputParameters(int thePageSize) { + myPageSize = thePageSize; + } public abstract String getSourceResourceParameterName(); @@ -108,4 +113,8 @@ public IBaseReference getTargetResource() { public void setTargetResource(IBaseReference theTargetResource) { this.myTargetResource = theTargetResource; } + + public int getPageSize() { + return myPageSize; + } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationInputParameters.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationInputParameters.java index 557c8041201d..ed8a65b49519 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationInputParameters.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationInputParameters.java @@ -26,6 +26,10 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER; public class PatientMergeOperationInputParameters extends MergeOperationInputParameters { + public PatientMergeOperationInputParameters(int thePageSize) { + super(thePageSize); + } + @Override public String getSourceResourceParameterName() { return OPERATION_MERGE_SOURCE_PATIENT; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 7d5b92f425ba..b7753824a66f 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -23,6 +23,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; +import ca.uhn.fhir.jpa.provider.ReplaceReferenceRequest; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -149,7 +150,8 @@ private void doMerge( return; } - myReplaceReferencesSvc.replaceReferences(sourceResource.getId(), targetResource.getId(), theRequestDetails); + ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest(sourceResource.getIdElement(), targetResource.getIdElement(), theMergeOperationParameters.getPageSize()); + myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); Patient patientToUpdate = prepareTargetPatientForUpdate( targetResource, sourceResource, resultResource, theMergeOperationParameters.getDeleteSource()); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java index fb1112c63541..88d0a368655d 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java @@ -27,5 +27,5 @@ */ public interface IReplaceReferencesSvc { - IBaseParameters replaceReferences(String theSourceRefId, String theTargetRefId, RequestDetails theRequest); + IBaseParameters replaceReferences(ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java new file mode 100644 index 000000000000..4847e8af9a9b --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java @@ -0,0 +1,56 @@ +package ca.uhn.fhir.jpa.provider; + +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.api.Include; +import ca.uhn.fhir.rest.param.StringParam; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.instance.model.api.IIdType; + +import java.security.InvalidParameterException; + +import static ca.uhn.fhir.rest.api.Constants.PARAM_ID; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID; +import static org.apache.commons.lang3.StringUtils.isBlank; + +public class ReplaceReferenceRequest { + @Nonnull + public final IIdType sourceId; + @Nonnull + public final IIdType targetId; + @Nonnull + public final int pageSize; + + public ReplaceReferenceRequest(@Nonnull IIdType theSourceId, @Nonnull IIdType theTargetId, int thePageSize) { + sourceId = theSourceId; + targetId = theTargetId; + pageSize = thePageSize; + } + + public void validateOrThrowInvalidParameterException() { + if (isBlank(sourceId.getResourceType())) { + throw new InvalidParameterException( + Msg.code(2585) + "'" + OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID + "' must be a resource type qualified id"); + } + + if (isBlank(targetId.getResourceType())) { + throw new InvalidParameterException( + Msg.code(2586) + "'" + OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID + "' must be a resource type qualified id"); + } + + if (!targetId.getResourceType().equals(sourceId.getResourceType())) { + throw new InvalidParameterException( + Msg.code(2587) + "Source and target id parameters must be for the same resource type"); + } + } + +// FIXME KHS remove + public SearchParameterMap getSearchParameterMap() { + SearchParameterMap retval = SearchParameterMap.newSynchronous(); + retval.add(PARAM_ID, new StringParam(sourceId.getValue())); + retval.addRevInclude(new Include("*")); + // Note we do not set the count since we will be streaming + return retval; + } +} diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index 6c6f5c02787b..2216313cf173 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -23,6 +23,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.OngoingStubbing; + import java.util.Collections; import java.util.List; @@ -37,7 +38,8 @@ @ExtendWith(MockitoExtension.class) public class ResourceMergeServiceTest { - + private static final Integer PAGE_SIZE = 1024; + private static final String MISSING_SOURCE_PARAMS_MSG = "There are no source resource parameters provided, include either a 'source-patient', or a 'source-patient-identifier' parameter."; private static final String MISSING_TARGET_PARAMS_MSG = @@ -73,7 +75,7 @@ void setup() { @Test void testMerge_WithoutResultResource_Success() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); @@ -103,7 +105,7 @@ void testMerge_WithoutResultResource_Success() { @Test void testMerge_WithResultResource_Success() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); @@ -137,7 +139,7 @@ void testMerge_WithResultResource_Success() { @Test void testMerge_WithDeleteSourceTrue_Success() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setDeleteSource(true); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -168,7 +170,7 @@ void testMerge_WithDeleteSourceTrue_Success() { @Test void testMerge_WithPreviewTrue_Success() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(true); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -195,7 +197,7 @@ void testMerge_WithPreviewTrue_Success() { @Test void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersionAreTheSame_Success() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123/_history/2")); mergeOperationParameters.setTargetResource(new Reference("Patient/345/_history/2")); Patient sourcePatient = createPatient("Patient/123/_history/2"); @@ -224,7 +226,7 @@ void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersio @Test void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheException() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); @@ -249,7 +251,7 @@ void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheExcepti @Test void testMerge_UnhandledExceptionThrown_Uses500StatusCode() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); @@ -274,7 +276,7 @@ void testMerge_UnhandledExceptionThrown_Uses500StatusCode() { @Test void testMerge_ValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorWith400Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setTargetResource(new Reference("Patient/123")); // When @@ -297,7 +299,7 @@ void testMerge_ValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorW @Test void testMerge_ValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorWith400Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); // When @@ -320,7 +322,7 @@ void testMerge_ValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorW @Test void testMerge_ValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ReturnsErrorsWith400Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -345,7 +347,7 @@ void testMerge_ValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ @Test void testMerge_ValidatesInputParameters_BothSourceResourceAndSourceIdentifierParamsProvided_ReturnsErrorWith400Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); @@ -371,7 +373,7 @@ void testMerge_ValidatesInputParameters_BothSourceResourceAndSourceIdentifierPar @Test void testMerge_ValidatesInputParameters_BothTargetResourceAndTargetIdentifiersParamsProvided_ReturnsErrorWith400Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setTargetResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); mergeOperationParameters.setSourceResource(new Reference("Patient/345")); @@ -396,7 +398,7 @@ void testMerge_ValidatesInputParameters_BothTargetResourceAndTargetIdentifiersPa @Test void testMerge_ValidatesInputParameters_SourceResourceParamHasNoReferenceElement_ReturnsErrorWith400Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference()); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -421,7 +423,7 @@ void testMerge_ValidatesInputParameters_SourceResourceParamHasNoReferenceElement @Test void testMerge_ValidatesInputParameters_TargetResourceParamHasNoReferenceElement_ReturnsErrorWith400Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference()); @@ -446,7 +448,7 @@ void testMerge_ValidatesInputParameters_TargetResourceParamHasNoReferenceElement @Test void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWith422Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); when(myDaoMock.read(new IdType("Patient/123"), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); @@ -470,7 +472,7 @@ void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWi @Test void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWith422Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); Patient sourcePatient = createPatient("Patient/123"); @@ -496,7 +498,7 @@ void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWi @Test void testMerge_ResolvesSourceResourceByIdentifiers_NoMatchFound_ReturnsErrorWith422Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResourceIdentifiers(List.of( new CanonicalIdentifier().setSystem("sys").setValue("val1"), new CanonicalIdentifier().setSystem("sys").setValue("val2"))); @@ -525,7 +527,7 @@ void testMerge_ResolvesSourceResourceByIdentifiers_NoMatchFound_ReturnsErrorWith @Test void testMerge_ResolvesSourceResourceByIdentifiers_MultipleMatchesFound_ReturnsErrorWith422Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); setupDaoMockSearchForIdentifiers(List.of( @@ -557,7 +559,7 @@ void testMerge_ResolvesSourceResourceByIdentifiers_MultipleMatchesFound_ReturnsE @Test void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith422Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResourceIdentifiers(List.of( new CanonicalIdentifier().setSystem("sys").setValue("val1"), @@ -588,7 +590,7 @@ void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith @Test void testMerge_ResolvesTargetResourceByIdentifiers_MultipleMatchesFound_ReturnsErrorWith422Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); setupDaoMockSearchForIdentifiers(List.of( @@ -621,7 +623,7 @@ void testMerge_ResolvesTargetResourceByIdentifiers_MultipleMatchesFound_ReturnsE @Test void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123/_history/1")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); Patient sourcePatient = createPatient("Patient/123/_history/2"); @@ -646,7 +648,7 @@ void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVe @Test void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345/_history/1")); Patient sourcePatient = createPatient("Patient/123"); @@ -678,7 +680,7 @@ void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVe @Test void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val2"))); Patient sourcePatient = createPatient("Patient/123"); @@ -706,7 +708,7 @@ void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status() @Test void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); Patient sourcePatient = createPatient("Patient/123"); @@ -733,7 +735,7 @@ void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status() { @Test void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsErrorWith422Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); Patient sourcePatient = createPatient("Patient/123"); @@ -760,7 +762,7 @@ void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsError @Test void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetResource_ReturnsErrorWith400Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); Patient resultPatient = createPatient("Patient/678"); @@ -791,7 +793,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetRes @Test void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersProvidedInTargetIdentifiers_ReturnsErrorWith400Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResourceIdentifiers(List.of( new CanonicalIdentifier().setSystem("sys").setValue("val1"), @@ -827,7 +829,7 @@ void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersPr @Test void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLink_ReturnsErrorWith400Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); @@ -856,7 +858,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLink_ReturnsEr @Test void testMerge_ValidatesResultResource_ResultResourceHasRedundantReplacesLinksToSource_ReturnsErrorWith400Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference("Patient/123")); mergeOperationParameters.setTargetResource(new Reference("Patient/345")); From 5da594331c6658d442608e00d855eb6bfc2d1877 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 6 Dec 2024 00:31:43 -0500 Subject: [PATCH 033/148] test --- .../jpa/provider/StopLimitAccumulator.java | 2 +- .../provider/StopLimitAccumulatorTest.java | 66 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulatorTest.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java index b8108e6a0a8d..601ed5d03b11 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java @@ -8,7 +8,6 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; -// FIXME KHS test class StopLimitAccumulator { private final boolean isTruncated; private final List myList; @@ -19,6 +18,7 @@ class StopLimitAccumulator { } static StopLimitAccumulator fromStreamAndLimit(@Nonnull Stream thePidStream, int theLimit) { + assert theLimit > 0; AtomicBoolean isBeyondLimit = new AtomicBoolean(false); List accumulator = new ArrayList<>(); diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulatorTest.java b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulatorTest.java new file mode 100644 index 000000000000..e9270c0cb65e --- /dev/null +++ b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulatorTest.java @@ -0,0 +1,66 @@ +package ca.uhn.fhir.jpa.provider; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class StopLimitAccumulatorTest { + + @Test + void testFromStreamAndLimit_withNoTruncation() { + // setup + Stream stream = Stream.of(1, 2, 3, 4, 5); + int limit = 5; + + // execute + StopLimitAccumulator accumulator = StopLimitAccumulator.fromStreamAndLimit(stream, limit); + + // verify + assertFalse(accumulator.isTruncated(), "The result should not be truncated"); + assertEquals(List.of(1, 2, 3, 4, 5), accumulator.getItemList(), "The list should contain all items within the limit"); + } + + @Test + void testFromStreamAndLimit_withTruncation() { + // setup + Stream stream = Stream.of(1, 2, 3, 4, 5, 6, 7); + int limit = 5; + + // execute + StopLimitAccumulator accumulator = StopLimitAccumulator.fromStreamAndLimit(stream, limit); + + // verify + assertTrue(accumulator.isTruncated(), "The result should be truncated"); + assertEquals(List.of(1, 2, 3, 4, 5), accumulator.getItemList(), "The list should contain only the items within the limit"); + } + + @Test + void testFromStreamAndLimit_withEmptyStream() { + // setup + Stream stream = Stream.empty(); + int limit = 5; + + // execute + StopLimitAccumulator accumulator = StopLimitAccumulator.fromStreamAndLimit(stream, limit); + + // verify + assertFalse(accumulator.isTruncated(), "The result should not be truncated for an empty stream"); + assertTrue(accumulator.getItemList().isEmpty(), "The list should be empty"); + } + + @Test + void testImmutabilityOfItemList() { + // setup + Stream stream = Stream.of(1, 2, 3); + int limit = 3; + + StopLimitAccumulator accumulator = StopLimitAccumulator.fromStreamAndLimit(stream, limit); + + // execute and Assert + List itemList = accumulator.getItemList(); + assertThrows(UnsupportedOperationException.class, () -> itemList.add(4), "The list should be immutable"); + } +} From 009331391b17083c418b6187947aac18dc611672 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 6 Dec 2024 00:33:45 -0500 Subject: [PATCH 034/148] test --- .../java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java index 601ed5d03b11..0e8ad4f24577 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java @@ -12,7 +12,7 @@ class StopLimitAccumulator { private final boolean isTruncated; private final List myList; - StopLimitAccumulator(List theList, boolean theIsTruncated) { + private StopLimitAccumulator(List theList, boolean theIsTruncated) { myList = Collections.unmodifiableList(theList); isTruncated = theIsTruncated; } From 0562e24910de7443d6d0c418e6d6b828f4c4cc2e Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 6 Dec 2024 11:48:16 -0500 Subject: [PATCH 035/148] start migrating to stream --- .../uhn/fhir/util}/StopLimitAccumulator.java | 6 +- .../fhir/util}/StopLimitAccumulatorTest.java | 2 +- .../provider/ReplaceReferencesSvcImpl.java | 91 +++++++------------ 3 files changed, 38 insertions(+), 61 deletions(-) rename {hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider => hapi-fhir-base/src/main/java/ca/uhn/fhir/util}/StopLimitAccumulator.java (84%) rename {hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/provider => hapi-fhir-base/src/test/java/ca/uhn/fhir/util}/StopLimitAccumulatorTest.java (98%) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java similarity index 84% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java index 0e8ad4f24577..e6d2ff0bc0c0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.provider; +package ca.uhn.fhir.util; import jakarta.annotation.Nonnull; @@ -8,7 +8,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; -class StopLimitAccumulator { +public class StopLimitAccumulator { private final boolean isTruncated; private final List myList; @@ -17,7 +17,7 @@ private StopLimitAccumulator(List theList, boolean theIsTruncated) { isTruncated = theIsTruncated; } - static StopLimitAccumulator fromStreamAndLimit(@Nonnull Stream thePidStream, int theLimit) { + public static StopLimitAccumulator fromStreamAndLimit(@Nonnull Stream thePidStream, long theLimit) { assert theLimit > 0; AtomicBoolean isBeyondLimit = new AtomicBoolean(false); List accumulator = new ArrayList<>(); diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulatorTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/StopLimitAccumulatorTest.java similarity index 98% rename from hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulatorTest.java rename to hapi-fhir-base/src/test/java/ca/uhn/fhir/util/StopLimitAccumulatorTest.java index e9270c0cb65e..825571336969 100644 --- a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/provider/StopLimitAccumulatorTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/StopLimitAccumulatorTest.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.provider; +package ca.uhn.fhir.util; import org.junit.jupiter.api.Test; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 0ee0746c630e..f3d9476b0b15 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -34,6 +34,7 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.ResourceReferenceInfo; +import ca.uhn.fhir.util.StopLimitAccumulator; import jakarta.annotation.Nonnull; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -54,7 +55,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; import java.util.stream.Stream; import static ca.uhn.fhir.jpa.patch.FhirPatch.OPERATION_REPLACE; @@ -124,79 +124,57 @@ private IBaseParameters replaceReferencesPreferAsync(ReplaceReferenceRequest the return replaceReferencesPreferAsync(theReplaceReferenceRequest, theRequestDetails); } - List referencingResources = accumulator.getItemList().stream().map(myIdHelperService::translatePidIdToForcedIdWithCache) + Stream referencingResourceStream = accumulator.getItemList().stream().map(myIdHelperService::translatePidIdToForcedIdWithCache) .filter(Optional::isPresent) .map(Optional::get) .map(IdDt::new) - .map(id -> getDao(id.getResourceType()).read(id, theRequestDetails)) - .collect(Collectors.toUnmodifiableList()); + .map(id -> getDao(id.getResourceType()).read(id, theRequestDetails)); - return replaceReferencesInTransaction(referencingResources, theReplaceReferenceRequest, theRequestDetails); + return replaceReferencesInTransaction(referencingResourceStream, theReplaceReferenceRequest, theRequestDetails); } private IBaseParameters replaceReferencesInTransaction( - List theReferencingResources, + Stream theReferencingResourceStream, ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { Parameters resultParams = new Parameters(); - // map resourceType -> map resourceId -> patch Parameters - Map> parametersMap = - buildPatchParameterMap(theReferencingResources, theReplaceReferenceRequest.sourceId, theReplaceReferenceRequest.targetId); - for (Map.Entry> mapEntry : parametersMap.entrySet()) { - String resourceType = mapEntry.getKey(); - IFhirResourceDao resDao = myDaoRegistry.getResourceDao(resourceType); - if (resDao == null) { - throw new InternalErrorException( - Msg.code(2588) + "No DAO registered for resource type: " + resourceType); - } - - // patch each resource of resourceType - patchResourceTypeResources(mapEntry, resDao, resultParams, theRequestDetails); - } - - return resultParams; - } - - private void patchResourceTypeResources( - Map.Entry> mapEntry, - IFhirResourceDao resDao, - Parameters resultParams, - RequestDetails theRequest) { - - for (Map.Entry idParamMapEntry : - mapEntry.getValue().entrySet()) { - IIdType resourceId = idParamMapEntry.getKey(); - Parameters parameters = idParamMapEntry.getValue(); - - MethodOutcome result = - resDao.patch(resourceId, null, PatchTypeEnum.FHIR_PATCH_JSON, null, parameters, theRequest); - - resultParams.addParameter().setResource((Resource) result.getOperationOutcome()); - } - } + // Map resourceType -> map resourceId -> patch Parameters + Map> parametersMap = new HashMap<>(); - private Map> buildPatchParameterMap( - List theReferencingResources, - IIdType theCurrentReferencedResourceId, - IIdType theNewReferencedResourceId) { - Map> paramsMap = new HashMap<>(); - - for (IBaseResource referencingResource : theReferencingResources) { - // resource can have more than one reference to the same target resource + // Process each resource in the stream + theReferencingResourceStream.forEach(referencingResource -> { for (ResourceReferenceInfo refInfo : myFhirContext.newTerser().getAllResourceReferences(referencingResource)) { addReferenceToMapIfForSource( - theCurrentReferencedResourceId, - theNewReferencedResourceId, + theReplaceReferenceRequest.sourceId, + theReplaceReferenceRequest.targetId, referencingResource, refInfo, - paramsMap); + parametersMap); } - } - return paramsMap; + }); + + // Apply patches for each resourceType + parametersMap.forEach((resourceType, resourceIdMap) -> { + IFhirResourceDao resDao = myDaoRegistry.getResourceDao(resourceType); + if (resDao == null) { + throw new InternalErrorException( + Msg.code(2588) + "No DAO registered for resource type: " + resourceType); + } + + // Patch each resource of the resourceType + resourceIdMap.forEach((resourceId, parameters) -> { + MethodOutcome result = + resDao.patch(resourceId, null, PatchTypeEnum.FHIR_PATCH_JSON, null, parameters, theRequestDetails); + + resultParams.addParameter().setResource((Resource) result.getOperationOutcome()); + }); + }); + + return resultParams; } private void addReferenceToMapIfForSource( @@ -205,6 +183,7 @@ private void addReferenceToMapIfForSource( IBaseResource referencingResource, ResourceReferenceInfo refInfo, Map> paramsMap) { + if (!refInfo.getResourceReference() .getReferenceElement() .toUnqualifiedVersionless() @@ -212,8 +191,7 @@ private void addReferenceToMapIfForSource( .equals(theCurrentReferencedResourceId .toUnqualifiedVersionless() .getValueAsString())) { - - // not a reference to the resource being replaced + // Not a reference to the resource being replaced return; } @@ -223,7 +201,6 @@ private void addReferenceToMapIfForSource( theNewReferencedResourceId.toUnqualifiedVersionless().getValueAsString())); paramsMap - // preserve order, in case it could matter .computeIfAbsent(referencingResource.fhirType(), k -> new LinkedHashMap<>()) .computeIfAbsent(referencingResource.getIdElement(), k -> new Parameters()) .addParameter(paramComponent); From bce4868c4166b8c89a59fcf10aa9a7f31219ff1e Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Thu, 5 Dec 2024 09:52:15 -0500 Subject: [PATCH 036/148] validate result-patient does not have link to source if delete-source is true --- .../jpa/dao/merge/ResourceMergeService.java | 88 ++++++++++++++----- .../dao/merge/ResourceMergeServiceTest.java | 32 +++++++ 2 files changed, 97 insertions(+), 23 deletions(-) diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index b7753824a66f..2b657b3bfd6f 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -206,13 +206,13 @@ private boolean validateResultResourceIfExists( isValid = false; } - // if the source resource is not being deleted, the result resource must have a replaces link to the source - // resource - if (!theMergeOperationParameters.getDeleteSource() - && !validateResultResourceHasReplacesLinkToSourceResource( + // if the source resource is not being deleted, the result resource must have a replaces link to the source resource + // if the source resource is being deleted, the result resource must not have a replaces link to the source resource + if (!validateResultResourceReplacesLinkToSourceResource( theResultResource, theResolvedSourceResource, theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getDeleteSource(), theOperationOutcome)) { isValid = false; } @@ -236,35 +236,77 @@ private boolean hasAllIdentifiers(Patient theResource, List return true; } - protected boolean validateResultResourceHasReplacesLinkToSourceResource( - Patient theResultResource, - Patient theResolvedSourceResource, - String theResultResourceParameterName, - IBaseOperationOutcome theOperationOutcome) { - // the result resource must have the replaces link set to the source resource - List replacesLinks = getLinksOfType(theResultResource, Patient.LinkType.REPLACES); - List replacesLinkToSourceResource = replacesLinks.stream() + + private List getLinksToResource(Patient theResource, + Patient.LinkType theLinkType, + IIdType theResourceId) { + List links = getLinksOfType(theResource, theLinkType); + return links.stream() .filter(r -> r.getReference() != null - && r.getReference() - .equals(theResolvedSourceResource - .getIdElement() - .toVersionless() - .getValue())) + && r.getReference() + .equals(theResourceId + .toVersionless() + .getValue())) .collect(Collectors.toList()); - if (replacesLinkToSourceResource.isEmpty()) { + } + + private boolean validateResultResourceDoesNotHaveReplacesLinkToSourceResource( + Patient theResultResource, + Patient theResolvedSourceResource, + String theResultResourceParameterName, + IBaseOperationOutcome theOperationOutcome) { + + List replacesLinkToSourceResource = getLinksToResource( + theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); + + if (!replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); + "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } - if (replacesLinkToSourceResource.size() > 1) { - String msg = String.format( + return true; + + } + + private boolean validateResultResourceReplacesLinkToSourceResource( + Patient theResultResource, + Patient theResolvedSourceResource, + String theResultResourceParameterName, + boolean theDeleteSource, + IBaseOperationOutcome theOperationOutcome) { + // the result resource must have the replaces link set to the source resource + List replacesLinkToSourceResource = getLinksToResource( + theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); + + if (theDeleteSource) { + if (!replacesLinkToSourceResource.isEmpty()) { + String msg = String.format( + "'%s' must not have a 'replaces' link to the source resource " + + "when the source resource will be deleted, as the link may prevent deleting the source " + + "resource.", + theResultResourceParameterName); + addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); + return false; + } + } + else { + if (replacesLinkToSourceResource.isEmpty()) { + String msg = String.format( + "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); + addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); + return false; + } + + if (replacesLinkToSourceResource.size() > 1) { + String msg = String.format( "'%s' has multiple 'replaces' links to the source resource. There should be only one.", theResultResourceParameterName); - addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); - return false; + addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); + return false; + } } return true; } diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index 2216313cf173..bfcc350d401a 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -855,6 +855,38 @@ void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLink_ReturnsEr verifyNoMoreInteractions(myDaoMock); } + + @Test + void testMerge_ValidatesResultResource_ResultResourceHasReplacesLinkAndDeleteSourceIsTrue_ReturnsErrorWith400Status() { + // Given + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + mergeOperationParameters.setDeleteSource(true); + + Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); + addReplacesLink(resultPatient, SOURCE_PATIENT_TEST_ID); + mergeOperationParameters.setResultResource(resultPatient); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + // When + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("'result-patient' must not have a 'replaces' link to the source resource when the source resource will be deleted, as the link may prevent deleting the source resource."); + + verifyNoMoreInteractions(myDaoMock); + } + @Test void testMerge_ValidatesResultResource_ResultResourceHasRedundantReplacesLinksToSource_ReturnsErrorWith400Status() { // Given From 957b7dacb9a699931080ac3d0a5a766a84d3a543 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Thu, 5 Dec 2024 16:06:42 -0500 Subject: [PATCH 037/148] add update count to msg in preview mode --- .../uhn/fhir/util/OperationOutcomeUtil.java | 54 ++++++++---- .../fhir/jpa/dao/data/IResourceLinkDao.java | 3 + .../provider/ReplaceReferencesSvcImpl.java | 10 +++ .../jpa/provider/r4/PatientMergeR4Test.java | 2 +- .../jpa/dao/merge/ResourceMergeService.java | 83 ++++++++++--------- .../jpa/provider/IReplaceReferencesSvc.java | 7 ++ .../dao/merge/ResourceMergeServiceTest.java | 18 ++-- .../fhir/util/OperationOutcomeUtilTest.java | 32 +++++++ 8 files changed, 146 insertions(+), 63 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/OperationOutcomeUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/OperationOutcomeUtil.java index 3c11e702124e..7feea8a15d0d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/OperationOutcomeUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/OperationOutcomeUtil.java @@ -265,30 +265,48 @@ public static IBase addIssueWithMessageId( } public static void addDetailsToIssue(FhirContext theFhirContext, IBase theIssue, String theSystem, String theCode) { + addDetailsToIssue(theFhirContext, theIssue, theSystem, theCode, null); + } + + public static void addDetailsToIssue( + FhirContext theFhirContext, IBase theIssue, String theSystem, String theCode, String theText) { BaseRuntimeElementCompositeDefinition issueElement = (BaseRuntimeElementCompositeDefinition) theFhirContext.getElementDefinition(theIssue.getClass()); BaseRuntimeChildDefinition detailsChildDef = issueElement.getChildByName("details"); - - BaseRuntimeElementCompositeDefinition codingDef = - (BaseRuntimeElementCompositeDefinition) theFhirContext.getElementDefinition("Coding"); - ICompositeType coding = (ICompositeType) codingDef.newInstance(); - - // System - IPrimitiveType system = - (IPrimitiveType) theFhirContext.getElementDefinition("uri").newInstance(); - system.setValueAsString(theSystem); - codingDef.getChildByName("system").getMutator().addValue(coding, system); - - // Code - IPrimitiveType code = - (IPrimitiveType) theFhirContext.getElementDefinition("code").newInstance(); - code.setValueAsString(theCode); - codingDef.getChildByName("code").getMutator().addValue(coding, code); BaseRuntimeElementCompositeDefinition ccDef = (BaseRuntimeElementCompositeDefinition) theFhirContext.getElementDefinition("CodeableConcept"); - ICompositeType codeableConcept = (ICompositeType) ccDef.newInstance(); - ccDef.getChildByName("coding").getMutator().addValue(codeableConcept, coding); + + if (isNotBlank(theSystem) || isNotBlank(theCode)) { + BaseRuntimeElementCompositeDefinition codingDef = + (BaseRuntimeElementCompositeDefinition) theFhirContext.getElementDefinition("Coding"); + ICompositeType coding = (ICompositeType) codingDef.newInstance(); + + // System + if (isNotBlank(theSystem)) { + IPrimitiveType system = (IPrimitiveType) + theFhirContext.getElementDefinition("uri").newInstance(); + system.setValueAsString(theSystem); + codingDef.getChildByName("system").getMutator().addValue(coding, system); + } + + // Code + if (isNotBlank(theCode)) { + IPrimitiveType code = (IPrimitiveType) + theFhirContext.getElementDefinition("code").newInstance(); + code.setValueAsString(theCode); + codingDef.getChildByName("code").getMutator().addValue(coding, code); + } + + ccDef.getChildByName("coding").getMutator().addValue(codeableConcept, coding); + } + + if (isNotBlank(theText)) { + IPrimitiveType textElem = (IPrimitiveType) + ccDef.getChildByName("text").getChildByName("text").newInstance(theText); + ccDef.getChildByName("text").getMutator().addValue(codeableConcept, textElem); + } + detailsChildDef.getMutator().addValue(theIssue, codeableConcept); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java index 125e13a14e05..a38f1b3e7e52 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java @@ -50,4 +50,7 @@ public interface IResourceLinkDao extends JpaRepository, IHa @Query("SELECT DISTINCT t.mySourceResourcePid FROM ResourceLink t WHERE t.myTargetResourcePid = :resId") Stream streamSourcePidsForTargetPid(@Param("resId") Long theTargetPid); + @Query("SELECT COUNT(DISTINCT t.mySourceResourcePid) FROM ResourceLink t WHERE t.myTargetResourcePid = :resId") + Integer countResourcesTargetingPid(@Param("resId") Long theTargetPid); + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index f3d9476b0b15..6e0874e2e2dd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -90,6 +90,16 @@ public IBaseParameters replaceReferences(ReplaceReferenceRequest theReplaceRefer } } + @Override + public Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails) { + return myHapiTransactionService.withRequest(theRequestDetails).execute( + () -> { + // FIXME KHS get partition from request + JpaPid sourcePid = myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); + return myResourceLinkDao.countResourcesTargetingPid(sourcePid.getId()); + }); + } + private IBaseParameters replaceReferencesPreferAsync(ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // FIXME KHS return null; diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 953202078988..0839862a10dc 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -275,7 +275,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa .element(0) .satisfies(issue -> { assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - assertThat(issue.getDiagnostics()).isEqualTo("Merge operation completed successfully."); + assertThat(issue.getDetails().getText()).isEqualTo("Merge operation completed successfully."); }); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 2b657b3bfd6f..93a77cc026d1 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -34,6 +34,7 @@ import ca.uhn.fhir.util.CanonicalIdentifier; import ca.uhn.fhir.util.OperationOutcomeUtil; import jakarta.annotation.Nullable; +import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -141,12 +142,20 @@ private void doMerge( Patient resultResource = (Patient) theMergeOperationParameters.getResultResource(); if (theMergeOperationParameters.getPreview()) { + Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( + sourceResource.getIdElement(), theRequestDetails); + // in preview mode, we should also return how the target would look like Patient targetPatientAsIfUpdated = prepareTargetPatientForUpdate( targetResource, sourceResource, resultResource, theMergeOperationParameters.getDeleteSource()); theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); - addInfoToOperationOutcome(operationOutcome, "Preview only merge operation - no issues detected"); + // adding +2 because the source and the target resources themselved would be updated as well + // TODO: but what if target resource is already referencing source resource as a link? how do we handle + // that case? + String diagnosticsMsg = String.format("Merge would update %d resources", referencingResourceCount + 2); + String detailsText = "Preview only merge operation - no issues detected"; + addInfoToOperationOutcome(operationOutcome, diagnosticsMsg, detailsText); return; } @@ -166,7 +175,8 @@ private void doMerge( updateResource(sourceResource, theRequestDetails); } - addInfoToOperationOutcome(operationOutcome, "Merge operation completed successfully."); + String detailsText = "Merge operation completed successfully."; + addInfoToOperationOutcome(operationOutcome, null, detailsText); } private boolean validateResultResourceIfExists( @@ -206,14 +216,16 @@ private boolean validateResultResourceIfExists( isValid = false; } - // if the source resource is not being deleted, the result resource must have a replaces link to the source resource - // if the source resource is being deleted, the result resource must not have a replaces link to the source resource + // if the source resource is not being deleted, the result resource must have a replaces link to the source + // resource + // if the source resource is being deleted, the result resource must not have a replaces link to the source + // resource if (!validateResultResourceReplacesLinkToSourceResource( - theResultResource, - theResolvedSourceResource, - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getDeleteSource(), - theOperationOutcome)) { + theResultResource, + theResolvedSourceResource, + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getDeleteSource(), + theOperationOutcome)) { isValid = false; } @@ -236,39 +248,32 @@ private boolean hasAllIdentifiers(Patient theResource, List return true; } - - private List getLinksToResource(Patient theResource, - Patient.LinkType theLinkType, - IIdType theResourceId) { + private List getLinksToResource( + Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { List links = getLinksOfType(theResource, theLinkType); return links.stream() .filter(r -> r.getReference() != null - && r.getReference() - .equals(theResourceId - .toVersionless() - .getValue())) + && r.getReference().equals(theResourceId.toVersionless().getValue())) .collect(Collectors.toList()); - } private boolean validateResultResourceDoesNotHaveReplacesLinkToSourceResource( - Patient theResultResource, - Patient theResolvedSourceResource, - String theResultResourceParameterName, - IBaseOperationOutcome theOperationOutcome) { + Patient theResultResource, + Patient theResolvedSourceResource, + String theResultResourceParameterName, + IBaseOperationOutcome theOperationOutcome) { List replacesLinkToSourceResource = getLinksToResource( - theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); + theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); if (!replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); + "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } return true; - } private boolean validateResultResourceReplacesLinkToSourceResource( @@ -284,26 +289,25 @@ private boolean validateResultResourceReplacesLinkToSourceResource( if (theDeleteSource) { if (!replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must not have a 'replaces' link to the source resource " + - "when the source resource will be deleted, as the link may prevent deleting the source " + - "resource.", - theResultResourceParameterName); + "'%s' must not have a 'replaces' link to the source resource " + + "when the source resource will be deleted, as the link may prevent deleting the source " + + "resource.", + theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } - } - else { + } else { if (replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); + "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } if (replacesLinkToSourceResource.size() > 1) { String msg = String.format( - "'%s' has multiple 'replaces' links to the source resource. There should be only one.", - theResultResourceParameterName); + "'%s' has multiple 'replaces' links to the source resource. There should be only one.", + theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } @@ -615,11 +619,14 @@ private IBaseResource resolveResource( theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); } - private void addInfoToOperationOutcome(IBaseOperationOutcome theOutcome, String theMsg) { - OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theMsg, null, null); + private void addInfoToOperationOutcome( + IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { + IBase issue = + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); + OperationOutcomeUtil.addDetailsToIssue(myFhirContext, issue, null, null, theDetailsText); } - private void addErrorToOperationOutcome(IBaseOperationOutcome theOutcome, String theMsg, String theCode) { - OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "error", theMsg, null, theCode); + private void addErrorToOperationOutcome(IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theCode) { + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "error", theDiagnosticMsg, null, theCode); } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java index 88d0a368655d..c8b8c40e05eb 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java @@ -21,6 +21,10 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; + +import java.util.List; /** * Contract for service which replaces references @@ -28,4 +32,7 @@ public interface IReplaceReferencesSvc { IBaseParameters replaceReferences(ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails); + + Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails); + } diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index bfcc350d401a..ea3a5fa1ad3b 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -11,8 +11,10 @@ import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.CanonicalIdentifier; +import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; @@ -97,7 +99,7 @@ void testMerge_WithoutResultResource_Success() { assertThat(operationOutcome.getIssue()).hasSize(1); OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - assertThat(issue.getDiagnostics()).contains(SUCCESSFUL_MERGE_MSG); + assertThat(issue.getDetails().getText()).contains(SUCCESSFUL_MERGE_MSG); verifyNoMoreInteractions(myDaoMock); } @@ -131,7 +133,7 @@ void testMerge_WithResultResource_Success() { assertThat(operationOutcome.getIssue()).hasSize(1); OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - assertThat(issue.getDiagnostics()).contains(SUCCESSFUL_MERGE_MSG); + assertThat(issue.getDetails().getText()).contains(SUCCESSFUL_MERGE_MSG); verifyNoMoreInteractions(myDaoMock); } @@ -162,7 +164,7 @@ void testMerge_WithDeleteSourceTrue_Success() { assertThat(operationOutcome.getIssue()).hasSize(1); OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - assertThat(issue.getDiagnostics()).contains(SUCCESSFUL_MERGE_MSG); + assertThat(issue.getDetails().getText()).contains(SUCCESSFUL_MERGE_MSG); verifyNoMoreInteractions(myDaoMock); } @@ -179,6 +181,9 @@ void testMerge_WithPreviewTrue_Success() { setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); + when(myReplaceReferencesSvcMock.countResourcesReferencingResource(new IdType(SOURCE_PATIENT_TEST_ID), + myRequestDetailsMock)).thenReturn(10); + // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -189,7 +194,8 @@ void testMerge_WithPreviewTrue_Success() { assertThat(operationOutcome.getIssue()).hasSize(1); OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - assertThat(issue.getDiagnostics()).contains("Preview only merge operation - no issues detected"); + assertThat(issue.getDetails().getText()).contains("Preview only merge operation - no issues detected"); + assertThat(issue.getDiagnostics()).contains("Merge would update 12 resources"); verifyNoMoreInteractions(myDaoMock); } @@ -216,7 +222,7 @@ void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersio assertThat(operationOutcome.getIssue()).hasSize(1); OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - assertThat(issue.getDiagnostics()).contains(SUCCESSFUL_MERGE_MSG); + assertThat(issue.getDetails().getText()).contains(SUCCESSFUL_MERGE_MSG); verifyNoMoreInteractions(myDaoMock); } @@ -859,7 +865,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLink_ReturnsEr @Test void testMerge_ValidatesResultResource_ResultResourceHasReplacesLinkAndDeleteSourceIsTrue_ReturnsErrorWith400Status() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(); + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); mergeOperationParameters.setDeleteSource(true); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/OperationOutcomeUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/OperationOutcomeUtilTest.java index b1cba4a3be4a..88529113162e 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/OperationOutcomeUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/OperationOutcomeUtilTest.java @@ -4,6 +4,8 @@ import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.r4.model.OperationOutcome; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -44,6 +46,36 @@ public void testAddIssueWithMessageId() { assertThat(oo.getIssueFirstRep().getDetails()).as("OO.issue.details is empty").isNotNull(); } + @ParameterizedTest + @CsvSource(value = { + "system, code, text", + "system, code, null", + "system, null, text", + "null, code, text", + "system, null, null", + "null, code, null ", + "null, null, text", + "null, null, null ", + }, nullValues={"null"}) + public void testAddDetailsToIssue(String theSystem, String theCode, String theText) { + + OperationOutcome oo = (OperationOutcome) OperationOutcomeUtil.newInstance(myCtx); + OperationOutcomeUtil.addIssue(myCtx, oo, "error", "Help i'm a bug",null, null); + + OperationOutcomeUtil.addDetailsToIssue(myCtx, oo.getIssueFirstRep(), theSystem, theCode, theText); + + assertThat(oo.getIssueFirstRep().getDetails().getText()).isEqualTo(theText); + if (theCode != null || theSystem != null) { + assertThat(oo.getIssueFirstRep().getDetails().getCoding()).hasSize(1); + assertThat(oo.getIssueFirstRep().getDetails().getCodingFirstRep().getSystem()).isEqualTo(theSystem); + assertThat(oo.getIssueFirstRep().getDetails().getCodingFirstRep().getCode()).isEqualTo(theCode); + } + else { + //both code and system are null, no coding should be present + assertThat(oo.getIssueFirstRep().getDetails().getCoding()).isEmpty(); + } + } + @Test public void hasIssuesOfSeverity_noMatchingIssues() { OperationOutcome oo = new OperationOutcome(); From 4f423e0606c632331b0dd40b5b01ea80a448c319 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Fri, 6 Dec 2024 12:10:00 -0500 Subject: [PATCH 038/148] spotless --- .../uhn/fhir/util/StopLimitAccumulator.java | 16 +- .../ca/uhn/fhir/jpa/config/JpaConfig.java | 10 +- .../fhir/jpa/dao/data/IResourceLinkDao.java | 1 - .../BaseJpaResourceProviderPatient.java | 387 +++++++++--------- .../fhir/jpa/provider/JpaSystemProvider.java | 67 +-- .../provider/ReplaceReferencesSvcImpl.java | 123 +++--- .../fhir/rest/server/RestfulServerUtils.java | 2 +- .../server/provider/ProviderConstants.java | 1 + .../jpa/dao/merge/ResourceMergeService.java | 5 +- .../jpa/provider/IReplaceReferencesSvc.java | 7 +- .../jpa/provider/ReplaceReferenceRequest.java | 12 +- 11 files changed, 332 insertions(+), 299 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java index e6d2ff0bc0c0..ca280e5d3884 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java @@ -23,14 +23,14 @@ public static StopLimitAccumulator fromStreamAndLimit(@Nonnull Stream List accumulator = new ArrayList<>(); thePidStream - .limit(theLimit + 1) // Fetch one extra item to see if there are more items past our limit - .forEach(item -> { - if (accumulator.size() < theLimit) { - accumulator.add(item); - } else { - isBeyondLimit.set(true); - } - }); + .limit(theLimit + 1) // Fetch one extra item to see if there are more items past our limit + .forEach(item -> { + if (accumulator.size() < theLimit) { + accumulator.add(item); + } else { + isBeyondLimit.set(true); + } + }); return new StopLimitAccumulator<>(accumulator, isBeyondLimit.get()); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index 74945247c9fe..7b49b430ce6d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -931,7 +931,13 @@ public CacheTagDefinitionDao tagDefinitionDao( } @Bean - public IReplaceReferencesSvc replaceReferencesSvc(FhirContext theFhirContext, DaoRegistry theDaoRegistry, HapiTransactionService theHapiTransactionService, IdHelperService theIdHelperService, IResourceLinkDao theResourceLinkDao) { - return new ReplaceReferencesSvcImpl(theFhirContext, theDaoRegistry, theHapiTransactionService, theIdHelperService, theResourceLinkDao); + public IReplaceReferencesSvc replaceReferencesSvc( + FhirContext theFhirContext, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IdHelperService theIdHelperService, + IResourceLinkDao theResourceLinkDao) { + return new ReplaceReferencesSvcImpl( + theFhirContext, theDaoRegistry, theHapiTransactionService, theIdHelperService, theResourceLinkDao); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java index a38f1b3e7e52..a696409aef06 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java @@ -52,5 +52,4 @@ public interface IResourceLinkDao extends JpaRepository, IHa @Query("SELECT COUNT(DISTINCT t.mySourceResourcePid) FROM ResourceLink t WHERE t.myTargetResourcePid = :resId") Integer countResourcesTargetingPid(@Param("resId") Long theTargetPid); - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 23c6f53b01e8..d660ba7150a9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -78,71 +78,71 @@ public abstract class BaseJpaResourceProviderPatient ex * Patient/123/$everything */ @Operation( - name = JpaConstants.OPERATION_EVERYTHING, - canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-everything", - idempotent = true, - bundleType = BundleTypeEnum.SEARCHSET) + name = JpaConstants.OPERATION_EVERYTHING, + canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-everything", + idempotent = true, + bundleType = BundleTypeEnum.SEARCHSET) public IBundleProvider patientInstanceEverything( - jakarta.servlet.http.HttpServletRequest theServletRequest, - @IdParam IIdType theId, - @Description( - shortDefinition = - "Results from this method are returned across multiple pages. This parameter controls the size of those pages.") - @OperationParam(name = Constants.PARAM_COUNT, typeName = "unsignedInt") - IPrimitiveType theCount, - @Description( - shortDefinition = - "Results from this method are returned across multiple pages. This parameter controls the offset when fetching a page.") - @OperationParam(name = Constants.PARAM_OFFSET, typeName = "unsignedInt") - IPrimitiveType theOffset, - @Description( - shortDefinition = - "Only return resources which were last updated as specified by the given range") - @OperationParam(name = Constants.PARAM_LASTUPDATED, min = 0, max = 1) - DateRangeParam theLastUpdated, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _content filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_CONTENT, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theContent, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _text filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_TEXT, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theNarrative, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _filter filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_FILTER, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theFilter, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_TYPE, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theTypes, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam(name = Constants.PARAM_MDM, min = 0, max = 1, typeName = "boolean") - IPrimitiveType theMdmExpand, - @Sort SortSpec theSortSpec, - RequestDetails theRequestDetails) { + jakarta.servlet.http.HttpServletRequest theServletRequest, + @IdParam IIdType theId, + @Description( + shortDefinition = + "Results from this method are returned across multiple pages. This parameter controls the size of those pages.") + @OperationParam(name = Constants.PARAM_COUNT, typeName = "unsignedInt") + IPrimitiveType theCount, + @Description( + shortDefinition = + "Results from this method are returned across multiple pages. This parameter controls the offset when fetching a page.") + @OperationParam(name = Constants.PARAM_OFFSET, typeName = "unsignedInt") + IPrimitiveType theOffset, + @Description( + shortDefinition = + "Only return resources which were last updated as specified by the given range") + @OperationParam(name = Constants.PARAM_LASTUPDATED, min = 0, max = 1) + DateRangeParam theLastUpdated, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _content filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_CONTENT, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theContent, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _text filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_TEXT, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theNarrative, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _filter filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_FILTER, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theFilter, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_TYPE, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theTypes, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam(name = Constants.PARAM_MDM, min = 0, max = 1, typeName = "boolean") + IPrimitiveType theMdmExpand, + @Sort SortSpec theSortSpec, + RequestDetails theRequestDetails) { startRequest(theServletRequest); try { @@ -158,7 +158,7 @@ public IBundleProvider patientInstanceEverything( everythingParams.setMdmExpand(resolveNullValue(theMdmExpand)); return ((IFhirResourceDaoPatient) getDao()) - .patientInstanceEverything(theServletRequest, theRequestDetails, everythingParams, theId); + .patientInstanceEverything(theServletRequest, theRequestDetails, everythingParams, theId); } finally { endRequest(theServletRequest); } @@ -168,77 +168,77 @@ public IBundleProvider patientInstanceEverything( * /Patient/$everything */ @Operation( - name = JpaConstants.OPERATION_EVERYTHING, - canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-everything", - idempotent = true, - bundleType = BundleTypeEnum.SEARCHSET) + name = JpaConstants.OPERATION_EVERYTHING, + canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-everything", + idempotent = true, + bundleType = BundleTypeEnum.SEARCHSET) public IBundleProvider patientTypeEverything( - jakarta.servlet.http.HttpServletRequest theServletRequest, - @Description( - shortDefinition = - "Results from this method are returned across multiple pages. This parameter controls the size of those pages.") - @OperationParam(name = Constants.PARAM_COUNT, typeName = "unsignedInt") - IPrimitiveType theCount, - @Description( - shortDefinition = - "Results from this method are returned across multiple pages. This parameter controls the offset when fetching a page.") - @OperationParam(name = Constants.PARAM_OFFSET, typeName = "unsignedInt") - IPrimitiveType theOffset, - @Description( - shortDefinition = - "Only return resources which were last updated as specified by the given range") - @OperationParam(name = Constants.PARAM_LASTUPDATED, min = 0, max = 1) - DateRangeParam theLastUpdated, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _content filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_CONTENT, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theContent, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _text filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_TEXT, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theNarrative, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _filter filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_FILTER, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theFilter, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam( - name = Constants.PARAM_TYPE, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "string") - List> theTypes, - @Description(shortDefinition = "Filter the resources to return based on the patient ids provided.") - @OperationParam( - name = Constants.PARAM_ID, - min = 0, - max = OperationParam.MAX_UNLIMITED, - typeName = "id") - List theId, - @Description( - shortDefinition = - "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") - @OperationParam(name = Constants.PARAM_MDM, min = 0, max = 1, typeName = "boolean") - IPrimitiveType theMdmExpand, - @Sort SortSpec theSortSpec, - RequestDetails theRequestDetails) { + jakarta.servlet.http.HttpServletRequest theServletRequest, + @Description( + shortDefinition = + "Results from this method are returned across multiple pages. This parameter controls the size of those pages.") + @OperationParam(name = Constants.PARAM_COUNT, typeName = "unsignedInt") + IPrimitiveType theCount, + @Description( + shortDefinition = + "Results from this method are returned across multiple pages. This parameter controls the offset when fetching a page.") + @OperationParam(name = Constants.PARAM_OFFSET, typeName = "unsignedInt") + IPrimitiveType theOffset, + @Description( + shortDefinition = + "Only return resources which were last updated as specified by the given range") + @OperationParam(name = Constants.PARAM_LASTUPDATED, min = 0, max = 1) + DateRangeParam theLastUpdated, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _content filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_CONTENT, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theContent, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _text filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_TEXT, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theNarrative, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _filter filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_FILTER, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theFilter, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam( + name = Constants.PARAM_TYPE, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "string") + List> theTypes, + @Description(shortDefinition = "Filter the resources to return based on the patient ids provided.") + @OperationParam( + name = Constants.PARAM_ID, + min = 0, + max = OperationParam.MAX_UNLIMITED, + typeName = "id") + List theId, + @Description( + shortDefinition = + "Filter the resources to return only resources matching the given _type filter (note that this filter is applied only to results which link to the given patient, not to the patient itself or to supporting resources linked to by the matched resources)") + @OperationParam(name = Constants.PARAM_MDM, min = 0, max = 1, typeName = "boolean") + IPrimitiveType theMdmExpand, + @Sort SortSpec theSortSpec, + RequestDetails theRequestDetails) { startRequest(theServletRequest); try { @@ -254,11 +254,11 @@ public IBundleProvider patientTypeEverything( everythingParams.setMdmExpand(resolveNullValue(theMdmExpand)); return ((IFhirResourceDaoPatient) getDao()) - .patientTypeEverything( - theServletRequest, - theRequestDetails, - everythingParams, - toFlattenedPatientIdTokenParamList(theId)); + .patientTypeEverything( + theServletRequest, + theRequestDetails, + everythingParams, + toFlattenedPatientIdTokenParamList(theId)); } finally { endRequest(theServletRequest); } @@ -268,40 +268,43 @@ public IBundleProvider patientTypeEverything( * /Patient/$merge */ @Operation( - name = ProviderConstants.OPERATION_MERGE, - canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge") + name = ProviderConstants.OPERATION_MERGE, + canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge") public IBaseParameters patientMerge( - HttpServletRequest theServletRequest, - HttpServletResponse theServletResponse, - ServletRequestDetails theRequestDetails, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER) - List theSourcePatientIdentifier, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER) - List theTargetPatientIdentifier, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT, max = 1) - IBaseReference theSourcePatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT, max = 1) - IBaseReference theTargetPatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PREVIEW, typeName = "boolean", max = 1) - IPrimitiveType thePreview, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_DELETE_SOURCE, typeName = "boolean", max = 1) - IPrimitiveType theDeleteSource, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_RESULT_PATIENT, max = 1) - IBaseResource theResultPatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PAGE_SIZE, typeName = "unsignedInt") IPrimitiveType thePageSize) { + HttpServletRequest theServletRequest, + HttpServletResponse theServletResponse, + ServletRequestDetails theRequestDetails, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER) + List theSourcePatientIdentifier, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER) + List theTargetPatientIdentifier, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT, max = 1) + IBaseReference theSourcePatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT, max = 1) + IBaseReference theTargetPatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PREVIEW, typeName = "boolean", max = 1) + IPrimitiveType thePreview, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_DELETE_SOURCE, typeName = "boolean", max = 1) + IPrimitiveType theDeleteSource, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_RESULT_PATIENT, max = 1) + IBaseResource theResultPatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PAGE_SIZE, typeName = "unsignedInt") + IPrimitiveType thePageSize) { startRequest(theServletRequest); - @Nonnull Integer pageSize = defaultIfNull(IPrimitiveType.toValueOrNull(thePageSize), myStorageSettings.getInternalSynchronousSearchSize()); + @Nonnull + Integer pageSize = defaultIfNull( + IPrimitiveType.toValueOrNull(thePageSize), myStorageSettings.getInternalSynchronousSearchSize()); try { MergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( - theSourcePatientIdentifier, - theTargetPatientIdentifier, - theSourcePatient, - theTargetPatient, - thePreview, - theDeleteSource, - theResultPatient, - pageSize); + theSourcePatientIdentifier, + theTargetPatientIdentifier, + theSourcePatient, + theTargetPatient, + thePreview, + theDeleteSource, + theResultPatient, + pageSize); IFhirResourceDaoPatient dao = (IFhirResourceDaoPatient) getDao(); ResourceMergeService resourceMergeService = new ResourceMergeService(dao, myReplaceReferencesSvc); @@ -309,7 +312,7 @@ public IBaseParameters patientMerge( FhirContext fhirContext = dao.getContext(); MergeOperationOutcome mergeOutcome = - resourceMergeService.merge(mergeOperationParameters, theRequestDetails); + resourceMergeService.merge(mergeOperationParameters, theRequestDetails); theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); return buildMergeOperationOutputParameters(fhirContext, mergeOutcome, theRequestDetails.getResource()); @@ -319,48 +322,48 @@ public IBaseParameters patientMerge( } private IBaseParameters buildMergeOperationOutputParameters( - FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome, IBaseResource theInputParameters) { + FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome, IBaseResource theInputParameters) { IBaseParameters retVal = ParametersUtil.newInstance(theFhirContext); ParametersUtil.addParameterToParameters( - theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT, theInputParameters); + theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT, theInputParameters); ParametersUtil.addParameterToParameters( - theFhirContext, - retVal, - ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME, - theMergeOutcome.getOperationOutcome()); + theFhirContext, + retVal, + ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME, + theMergeOutcome.getOperationOutcome()); if (theMergeOutcome.getUpdatedTargetResource() != null) { ParametersUtil.addParameterToParameters( - theFhirContext, - retVal, - OPERATION_MERGE_OUTPUT_PARAM_RESULT, - theMergeOutcome.getUpdatedTargetResource()); + theFhirContext, + retVal, + OPERATION_MERGE_OUTPUT_PARAM_RESULT, + theMergeOutcome.getUpdatedTargetResource()); } return retVal; } private MergeOperationInputParameters buildMergeOperationInputParameters( - List theSourcePatientIdentifier, - List theTargetPatientIdentifier, - IBaseReference theSourcePatient, - IBaseReference theTargetPatient, - IPrimitiveType thePreview, - IPrimitiveType theDeleteSource, - IBaseResource theResultPatient, - int thePageSize) { + List theSourcePatientIdentifier, + List theTargetPatientIdentifier, + IBaseReference theSourcePatient, + IBaseReference theTargetPatient, + IPrimitiveType thePreview, + IPrimitiveType theDeleteSource, + IBaseResource theResultPatient, + int thePageSize) { MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(thePageSize); if (theSourcePatientIdentifier != null) { List sourceResourceIdentifiers = theSourcePatientIdentifier.stream() - .map(IdentifierUtil::identifierDtFromIdentifier) - .collect(Collectors.toList()); + .map(IdentifierUtil::identifierDtFromIdentifier) + .collect(Collectors.toList()); mergeOperationParameters.setSourceResourceIdentifiers(sourceResourceIdentifiers); } if (theTargetPatientIdentifier != null) { List targetResourceIdentifiers = theTargetPatientIdentifier.stream() - .map(IdentifierUtil::identifierDtFromIdentifier) - .collect(Collectors.toList()); + .map(IdentifierUtil::identifierDtFromIdentifier) + .collect(Collectors.toList()); mergeOperationParameters.setTargetResourceIdentifiers(targetResourceIdentifiers); } mergeOperationParameters.setSourceResource(theSourcePatient); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index f0b7b03cd261..05e84bc26b68 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -51,18 +51,18 @@ public final class JpaSystemProvider extends BaseJpaSystemProvider { @Description( - "Marks all currently existing resources of a given type, or all resources of all types, for reindexing.") + "Marks all currently existing resources of a given type, or all resources of all types, for reindexing.") @Operation( - name = MARK_ALL_RESOURCES_FOR_REINDEXING, - idempotent = false, - returnParameters = {@OperationParam(name = "status")}) + name = MARK_ALL_RESOURCES_FOR_REINDEXING, + idempotent = false, + returnParameters = {@OperationParam(name = "status")}) /** * @deprecated * @see ReindexProvider#Reindex(List, IPrimitiveType, RequestDetails) */ @Deprecated public IBaseResource markAllResourcesForReindexing( - @OperationParam(name = "type", min = 0, max = 1, typeName = "code") IPrimitiveType theType) { + @OperationParam(name = "type", min = 0, max = 1, typeName = "code") IPrimitiveType theType) { if (theType != null && isNotBlank(theType.getValueAsString())) { getResourceReindexingSvc().markAllResourcesForReindexing(theType.getValueAsString()); @@ -80,9 +80,9 @@ public IBaseResource markAllResourcesForReindexing( @Description("Forces a single pass of the resource reindexing processor") @Operation( - name = PERFORM_REINDEXING_PASS, - idempotent = false, - returnParameters = {@OperationParam(name = "status")}) + name = PERFORM_REINDEXING_PASS, + idempotent = false, + returnParameters = {@OperationParam(name = "status")}) /** * @deprecated * @see ReindexProvider#Reindex(List, IPrimitiveType, RequestDetails) @@ -106,8 +106,8 @@ public IBaseResource performReindexingPass() { @Operation(name = JpaConstants.OPERATION_GET_RESOURCE_COUNTS, idempotent = true) @Description( - shortDefinition = - "Provides the number of resources currently stored on the server, broken down by resource type") + shortDefinition = + "Provides the number of resources currently stored on the server, broken down by resource type") public IBaseParameters getResourceCounts() { IBaseParameters retVal = ParametersUtil.newInstance(getContext()); @@ -116,23 +116,23 @@ public IBaseParameters getResourceCounts() { counts = new TreeMap<>(counts); for (Map.Entry nextEntry : counts.entrySet()) { ParametersUtil.addParameterToParametersInteger( - getContext(), - retVal, - nextEntry.getKey(), - nextEntry.getValue().intValue()); + getContext(), + retVal, + nextEntry.getKey(), + nextEntry.getValue().intValue()); } return retVal; } @Operation( - name = ProviderConstants.OPERATION_META, - idempotent = true, - returnParameters = {@OperationParam(name = "return", typeName = "Meta")}) + name = ProviderConstants.OPERATION_META, + idempotent = true, + returnParameters = {@OperationParam(name = "return", typeName = "Meta")}) public IBaseParameters meta(RequestDetails theRequestDetails) { IBaseParameters retVal = ParametersUtil.newInstance(getContext()); ParametersUtil.addParameterToParameters( - getContext(), retVal, "return", getDao().metaGetOperation(theRequestDetails)); + getContext(), retVal, "return", getDao().metaGetOperation(theRequestDetails)); return retVal; } @@ -150,29 +150,34 @@ public IBaseBundle transaction(RequestDetails theRequestDetails, @TransactionPar @Operation(name = ProviderConstants.OPERATION_REPLACE_REFERENCES, global = true) @Description( - value = - "This operation searches for all references matching the provided id and updates them to references to the provided newReferenceTargetId.", - shortDefinition = "Repoints referencing resources to another resources instance") + value = + "This operation searches for all references matching the provided id and updates them to references to the provided newReferenceTargetId.", + shortDefinition = "Repoints referencing resources to another resources instance") public IBaseParameters replaceReferences( - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID) String theSourceId, - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID) String theTargetId, - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PAGE_SIZE, typeName = "unsignedInt") IPrimitiveType thePageSize, - RequestDetails theRequestDetails) { + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID) + String theSourceId, + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID) + String theTargetId, + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PAGE_SIZE, typeName = "unsignedInt") + IPrimitiveType thePageSize, + RequestDetails theRequestDetails) { validateReplaceReferencesParams(theSourceId, theTargetId); - Integer pageSize = defaultIfNull(IPrimitiveType.toValueOrNull(thePageSize), myStorageSettings.getInternalSynchronousSearchSize()); - ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest(new IdDt(theSourceId), new IdDt(theTargetId), pageSize); + Integer pageSize = defaultIfNull( + IPrimitiveType.toValueOrNull(thePageSize), myStorageSettings.getInternalSynchronousSearchSize()); + ReplaceReferenceRequest replaceReferenceRequest = + new ReplaceReferenceRequest(new IdDt(theSourceId), new IdDt(theTargetId), pageSize); return getReplaceReferencesSvc().replaceReferences(replaceReferenceRequest, theRequestDetails); } private static void validateReplaceReferencesParams(String theSourceId, String theTargetId) { if (isBlank(theSourceId)) { - throw new InvalidParameterException( - Msg.code(2583) + "Parameter '" + OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID + "' is blank"); + throw new InvalidParameterException(Msg.code(2583) + "Parameter '" + + OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID + "' is blank"); } if (isBlank(theTargetId)) { - throw new InvalidParameterException( - Msg.code(2584) + "Parameter '" + OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID + "' is blank"); + throw new InvalidParameterException(Msg.code(2584) + "Parameter '" + + OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID + "' is blank"); } } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 6e0874e2e2dd..763d142ce7be 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -52,7 +52,6 @@ import java.util.HashMap; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Stream; @@ -71,7 +70,12 @@ public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { private final IdHelperService myIdHelperService; private final IResourceLinkDao myResourceLinkDao; - public ReplaceReferencesSvcImpl(FhirContext theFhirContext, DaoRegistry theDaoRegistry, HapiTransactionService theHapiTransactionService, IdHelperService theIdHelperService, IResourceLinkDao theResourceLinkDao) { + public ReplaceReferencesSvcImpl( + FhirContext theFhirContext, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IdHelperService theIdHelperService, + IResourceLinkDao theResourceLinkDao) { myFhirContext = theFhirContext; myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; @@ -80,7 +84,8 @@ public ReplaceReferencesSvcImpl(FhirContext theFhirContext, DaoRegistry theDaoRe } @Override - public IBaseParameters replaceReferences(ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + public IBaseParameters replaceReferences( + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { theReplaceReferenceRequest.validateOrThrowInvalidParameterException(); if (theRequestDetails.isPreferAsync()) { @@ -92,15 +97,16 @@ public IBaseParameters replaceReferences(ReplaceReferenceRequest theReplaceRefer @Override public Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails) { - return myHapiTransactionService.withRequest(theRequestDetails).execute( - () -> { - // FIXME KHS get partition from request - JpaPid sourcePid = myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); - return myResourceLinkDao.countResourcesTargetingPid(sourcePid.getId()); - }); + return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { + // FIXME KHS get partition from request + JpaPid sourcePid = + myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); + return myResourceLinkDao.countResourcesTargetingPid(sourcePid.getId()); + }); } - private IBaseParameters replaceReferencesPreferAsync(ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + private IBaseParameters replaceReferencesPreferAsync( + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // FIXME KHS return null; } @@ -108,45 +114,55 @@ private IBaseParameters replaceReferencesPreferAsync(ReplaceReferenceRequest the /** * Try to perform the operation synchronously. However if there is more than a page of results, fall back to asynchronous operation */ - private @NotNull IBaseParameters replaceReferencesPreferSync(ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + private @NotNull IBaseParameters replaceReferencesPreferSync( + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // todo jm: this could be problematic depending on referenceing object set size, however we are adding // batch job option to handle that case as part of this feature IFhirResourceDao dao = getDao(theReplaceReferenceRequest.sourceId.getResourceType()); if (dao == null) { - throw new InternalErrorException( - Msg.code(2582) + "Couldn't obtain DAO for resource type" + theReplaceReferenceRequest.sourceId.getResourceType()); + throw new InternalErrorException(Msg.code(2582) + "Couldn't obtain DAO for resource type" + + theReplaceReferenceRequest.sourceId.getResourceType()); } - return myHapiTransactionService.withRequest(theRequestDetails).execute( - () -> performReplaceInTransaction(theReplaceReferenceRequest, theRequestDetails, dao)); + return myHapiTransactionService + .withRequest(theRequestDetails) + .execute(() -> performReplaceInTransaction(theReplaceReferenceRequest, theRequestDetails, dao)); } - private @Nullable IBaseParameters performReplaceInTransaction(ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails, IFhirResourceDao dao) { + private @Nullable IBaseParameters performReplaceInTransaction( + ReplaceReferenceRequest theReplaceReferenceRequest, + RequestDetails theRequestDetails, + IFhirResourceDao dao) { // FIXME KHS get partition from request - JpaPid sourcePid = myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); + JpaPid sourcePid = myIdHelperService.getPidOrThrowException( + RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); - Stream pidStream = myResourceLinkDao.streamSourcePidsForTargetPid(sourcePid.getId()).map(JpaPid::fromId); - StopLimitAccumulator accumulator = StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.pageSize); + Stream pidStream = myResourceLinkDao + .streamSourcePidsForTargetPid(sourcePid.getId()) + .map(JpaPid::fromId); + StopLimitAccumulator accumulator = + StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.pageSize); if (accumulator.isTruncated()) { ourLog.info("Too many results. Switching to asynchronous reference replacement."); return replaceReferencesPreferAsync(theReplaceReferenceRequest, theRequestDetails); } - Stream referencingResourceStream = accumulator.getItemList().stream().map(myIdHelperService::translatePidIdToForcedIdWithCache) - .filter(Optional::isPresent) - .map(Optional::get) - .map(IdDt::new) - .map(id -> getDao(id.getResourceType()).read(id, theRequestDetails)); + Stream referencingResourceStream = accumulator.getItemList().stream() + .map(myIdHelperService::translatePidIdToForcedIdWithCache) + .filter(Optional::isPresent) + .map(Optional::get) + .map(IdDt::new) + .map(id -> getDao(id.getResourceType()).read(id, theRequestDetails)); return replaceReferencesInTransaction(referencingResourceStream, theReplaceReferenceRequest, theRequestDetails); } private IBaseParameters replaceReferencesInTransaction( - Stream theReferencingResourceStream, - ReplaceReferenceRequest theReplaceReferenceRequest, - RequestDetails theRequestDetails) { + Stream theReferencingResourceStream, + ReplaceReferenceRequest theReplaceReferenceRequest, + RequestDetails theRequestDetails) { Parameters resultParams = new Parameters(); @@ -156,14 +172,14 @@ private IBaseParameters replaceReferencesInTransaction( // Process each resource in the stream theReferencingResourceStream.forEach(referencingResource -> { for (ResourceReferenceInfo refInfo : - myFhirContext.newTerser().getAllResourceReferences(referencingResource)) { + myFhirContext.newTerser().getAllResourceReferences(referencingResource)) { addReferenceToMapIfForSource( - theReplaceReferenceRequest.sourceId, - theReplaceReferenceRequest.targetId, - referencingResource, - refInfo, - parametersMap); + theReplaceReferenceRequest.sourceId, + theReplaceReferenceRequest.targetId, + referencingResource, + refInfo, + parametersMap); } }); @@ -172,13 +188,13 @@ private IBaseParameters replaceReferencesInTransaction( IFhirResourceDao resDao = myDaoRegistry.getResourceDao(resourceType); if (resDao == null) { throw new InternalErrorException( - Msg.code(2588) + "No DAO registered for resource type: " + resourceType); + Msg.code(2588) + "No DAO registered for resource type: " + resourceType); } // Patch each resource of the resourceType resourceIdMap.forEach((resourceId, parameters) -> { - MethodOutcome result = - resDao.patch(resourceId, null, PatchTypeEnum.FHIR_PATCH_JSON, null, parameters, theRequestDetails); + MethodOutcome result = resDao.patch( + resourceId, null, PatchTypeEnum.FHIR_PATCH_JSON, null, parameters, theRequestDetails); resultParams.addParameter().setResource((Resource) result.getOperationOutcome()); }); @@ -188,37 +204,37 @@ private IBaseParameters replaceReferencesInTransaction( } private void addReferenceToMapIfForSource( - IIdType theCurrentReferencedResourceId, - IIdType theNewReferencedResourceId, - IBaseResource referencingResource, - ResourceReferenceInfo refInfo, - Map> paramsMap) { + IIdType theCurrentReferencedResourceId, + IIdType theNewReferencedResourceId, + IBaseResource referencingResource, + ResourceReferenceInfo refInfo, + Map> paramsMap) { if (!refInfo.getResourceReference() - .getReferenceElement() - .toUnqualifiedVersionless() - .getValueAsString() - .equals(theCurrentReferencedResourceId + .getReferenceElement() .toUnqualifiedVersionless() - .getValueAsString())) { + .getValueAsString() + .equals(theCurrentReferencedResourceId + .toUnqualifiedVersionless() + .getValueAsString())) { // Not a reference to the resource being replaced return; } Parameters.ParametersParameterComponent paramComponent = createReplaceReferencePatchOperation( - referencingResource.fhirType() + "." + refInfo.getName(), - new Reference( - theNewReferencedResourceId.toUnqualifiedVersionless().getValueAsString())); + referencingResource.fhirType() + "." + refInfo.getName(), + new Reference( + theNewReferencedResourceId.toUnqualifiedVersionless().getValueAsString())); paramsMap - .computeIfAbsent(referencingResource.fhirType(), k -> new LinkedHashMap<>()) - .computeIfAbsent(referencingResource.getIdElement(), k -> new Parameters()) - .addParameter(paramComponent); + .computeIfAbsent(referencingResource.fhirType(), k -> new LinkedHashMap<>()) + .computeIfAbsent(referencingResource.getIdElement(), k -> new Parameters()) + .addParameter(paramComponent); } @Nonnull private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( - String thePath, Type theValue) { + String thePath, Type theValue) { Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); operation.setName(PARAMETER_OPERATION); @@ -231,5 +247,4 @@ private Parameters.ParametersParameterComponent createReplaceReferencePatchOpera private IFhirResourceDao getDao(String theResourceName) { return myDaoRegistry.getResourceDao(theResourceName); } - } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java index 7d707c29152c..9dfd75ee4be7 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServerUtils.java @@ -801,7 +801,7 @@ public static PreferHeader parsePreferHeader(IRestfulServer theServer, String } @Nonnull - public static PreferHeader parsePreferHeader(String theValue) { + public static PreferHeader parsePreferHeader(String theValue) { PreferHeader retVal = new PreferHeader(); if (isNotBlank(theValue)) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index 26d83713aa7e..189a1ab1f99b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -270,6 +270,7 @@ public class ProviderConstants { * $replace-references output Parameters names */ public static final String OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK = "task"; + public static final String OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME = "outcome"; /** diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 93a77cc026d1..f8d53868b6b3 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -159,7 +159,10 @@ private void doMerge( return; } - ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest(sourceResource.getIdElement(), targetResource.getIdElement(), theMergeOperationParameters.getPageSize()); + ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest( + sourceResource.getIdElement(), + targetResource.getIdElement(), + theMergeOperationParameters.getPageSize()); myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); Patient patientToUpdate = prepareTargetPatientForUpdate( diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java index c8b8c40e05eb..ff6b9751587e 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java @@ -21,18 +21,15 @@ import ca.uhn.fhir.rest.api.server.RequestDetails; import org.hl7.fhir.instance.model.api.IBaseParameters; -import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; -import java.util.List; - /** * Contract for service which replaces references */ public interface IReplaceReferencesSvc { - IBaseParameters replaceReferences(ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails); + IBaseParameters replaceReferences( + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails); Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails); - } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java index 4847e8af9a9b..05c998595301 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java @@ -17,8 +17,10 @@ public class ReplaceReferenceRequest { @Nonnull public final IIdType sourceId; + @Nonnull public final IIdType targetId; + @Nonnull public final int pageSize; @@ -31,21 +33,23 @@ public ReplaceReferenceRequest(@Nonnull IIdType theSourceId, @Nonnull IIdType th public void validateOrThrowInvalidParameterException() { if (isBlank(sourceId.getResourceType())) { throw new InvalidParameterException( - Msg.code(2585) + "'" + OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID + "' must be a resource type qualified id"); + Msg.code(2585) + "'" + OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID + + "' must be a resource type qualified id"); } if (isBlank(targetId.getResourceType())) { throw new InvalidParameterException( - Msg.code(2586) + "'" + OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID + "' must be a resource type qualified id"); + Msg.code(2586) + "'" + OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID + + "' must be a resource type qualified id"); } if (!targetId.getResourceType().equals(sourceId.getResourceType())) { throw new InvalidParameterException( - Msg.code(2587) + "Source and target id parameters must be for the same resource type"); + Msg.code(2587) + "Source and target id parameters must be for the same resource type"); } } -// FIXME KHS remove + // FIXME KHS remove public SearchParameterMap getSearchParameterMap() { SearchParameterMap retval = SearchParameterMap.newSynchronous(); retval.add(PARAM_ID, new StringParam(sourceId.getValue())); From 999bd3939833062322342842d5ad8308d8d28f5b Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 6 Dec 2024 13:04:47 -0500 Subject: [PATCH 039/148] change page size -> batch size and create new default max in storage settings --- .../BaseJpaResourceProviderPatient.java | 10 +- .../fhir/jpa/provider/JpaSystemProvider.java | 6 +- .../provider/ReplaceReferencesSvcImpl.java | 114 ++++++------------ .../server/provider/ProviderConstants.java | 2 +- .../jpa/api/config/JpaStorageSettings.java | 19 +++ .../merge/MergeOperationInputParameters.java | 10 +- .../PatientMergeOperationInputParameters.java | 4 +- .../jpa/dao/merge/ResourceMergeService.java | 2 +- .../jpa/provider/ReplaceReferenceRequest.java | 10 +- 9 files changed, 79 insertions(+), 98 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 23c6f53b01e8..c87971085038 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -288,10 +288,10 @@ public IBaseParameters patientMerge( IPrimitiveType theDeleteSource, @OperationParam(name = ProviderConstants.OPERATION_MERGE_RESULT_PATIENT, max = 1) IBaseResource theResultPatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PAGE_SIZE, typeName = "unsignedInt") IPrimitiveType thePageSize) { + @OperationParam(name = ProviderConstants.OPERATION_MERGE_BATCH_SIZE, typeName = "unsignedInt") IPrimitiveType theBatchSize) { startRequest(theServletRequest); - @Nonnull Integer pageSize = defaultIfNull(IPrimitiveType.toValueOrNull(thePageSize), myStorageSettings.getInternalSynchronousSearchSize()); + @Nonnull Integer batchSize = defaultIfNull(IPrimitiveType.toValueOrNull(theBatchSize), myStorageSettings.getMaxTransactionEntriesForWrite()); try { MergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( theSourcePatientIdentifier, @@ -301,7 +301,7 @@ public IBaseParameters patientMerge( thePreview, theDeleteSource, theResultPatient, - pageSize); + batchSize); IFhirResourceDaoPatient dao = (IFhirResourceDaoPatient) getDao(); ResourceMergeService resourceMergeService = new ResourceMergeService(dao, myReplaceReferencesSvc); @@ -349,8 +349,8 @@ private MergeOperationInputParameters buildMergeOperationInputParameters( IPrimitiveType thePreview, IPrimitiveType theDeleteSource, IBaseResource theResultPatient, - int thePageSize) { - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(thePageSize); + int theBatchSize) { + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(theBatchSize); if (theSourcePatientIdentifier != null) { List sourceResourceIdentifiers = theSourcePatientIdentifier.stream() .map(IdentifierUtil::identifierDtFromIdentifier) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index f0b7b03cd261..7d9c91ffe809 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -156,11 +156,11 @@ public IBaseBundle transaction(RequestDetails theRequestDetails, @TransactionPar public IBaseParameters replaceReferences( @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID) String theSourceId, @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID) String theTargetId, - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PAGE_SIZE, typeName = "unsignedInt") IPrimitiveType thePageSize, + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PAGE_SIZE, typeName = "unsignedInt") IPrimitiveType theBatchSize, RequestDetails theRequestDetails) { validateReplaceReferencesParams(theSourceId, theTargetId); - Integer pageSize = defaultIfNull(IPrimitiveType.toValueOrNull(thePageSize), myStorageSettings.getInternalSynchronousSearchSize()); - ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest(new IdDt(theSourceId), new IdDt(theTargetId), pageSize); + Integer batchSize = defaultIfNull(IPrimitiveType.toValueOrNull(theBatchSize), myStorageSettings.getMaxTransactionEntriesForWrite()); + ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest(new IdDt(theSourceId), new IdDt(theTargetId), batchSize); return getReplaceReferencesSvc().replaceReferences(replaceReferenceRequest, theRequestDetails); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index f3d9476b0b15..c9efb3b0bedf 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -50,10 +50,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.stream.Stream; @@ -117,7 +113,7 @@ private IBaseParameters replaceReferencesPreferAsync(ReplaceReferenceRequest the JpaPid sourcePid = myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); Stream pidStream = myResourceLinkDao.streamSourcePidsForTargetPid(sourcePid.getId()).map(JpaPid::fromId); - StopLimitAccumulator accumulator = StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.pageSize); + StopLimitAccumulator accumulator = StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); if (accumulator.isTruncated()) { ourLog.info("Too many results. Switching to asynchronous reference replacement."); @@ -140,86 +136,52 @@ private IBaseParameters replaceReferencesInTransaction( Parameters resultParams = new Parameters(); - // Map resourceType -> map resourceId -> patch Parameters - Map> parametersMap = new HashMap<>(); - - // Process each resource in the stream theReferencingResourceStream.forEach(referencingResource -> { - for (ResourceReferenceInfo refInfo : - myFhirContext.newTerser().getAllResourceReferences(referencingResource)) { - - addReferenceToMapIfForSource( - theReplaceReferenceRequest.sourceId, - theReplaceReferenceRequest.targetId, - referencingResource, - refInfo, - parametersMap); - } - }); + Parameters params = new Parameters(); - // Apply patches for each resourceType - parametersMap.forEach((resourceType, resourceIdMap) -> { - IFhirResourceDao resDao = myDaoRegistry.getResourceDao(resourceType); - if (resDao == null) { - throw new InternalErrorException( - Msg.code(2588) + "No DAO registered for resource type: " + resourceType); - } - - // Patch each resource of the resourceType - resourceIdMap.forEach((resourceId, parameters) -> { - MethodOutcome result = - resDao.patch(resourceId, null, PatchTypeEnum.FHIR_PATCH_JSON, null, parameters, theRequestDetails); - - resultParams.addParameter().setResource((Resource) result.getOperationOutcome()); - }); - }); + String fhirType = referencingResource.fhirType(); + myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() + .filter(refInfo -> matches(refInfo, theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource + .map(refInfo -> createReplaceReferencePatchOperation( + fhirType + "." + refInfo.getName(), + new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) + .forEach(params::addParameter); // Add each operation to parameters - return resultParams; - } + IFhirResourceDao resDao = myDaoRegistry.getResourceDao(fhirType); - private void addReferenceToMapIfForSource( - IIdType theCurrentReferencedResourceId, - IIdType theNewReferencedResourceId, - IBaseResource referencingResource, - ResourceReferenceInfo refInfo, - Map> paramsMap) { - - if (!refInfo.getResourceReference() - .getReferenceElement() - .toUnqualifiedVersionless() - .getValueAsString() - .equals(theCurrentReferencedResourceId - .toUnqualifiedVersionless() - .getValueAsString())) { - // Not a reference to the resource being replaced - return; - } + IIdType resourceId = referencingResource.getIdElement(); - Parameters.ParametersParameterComponent paramComponent = createReplaceReferencePatchOperation( - referencingResource.fhirType() + "." + refInfo.getName(), - new Reference( - theNewReferencedResourceId.toUnqualifiedVersionless().getValueAsString())); + MethodOutcome result = + resDao.patch(resourceId, null, PatchTypeEnum.FHIR_PATCH_JSON, null, params, theRequestDetails); + + resultParams.addParameter().setResource((Resource) result.getOperationOutcome()); + }); - paramsMap - .computeIfAbsent(referencingResource.fhirType(), k -> new LinkedHashMap<>()) - .computeIfAbsent(referencingResource.getIdElement(), k -> new Parameters()) - .addParameter(paramComponent); + return resultParams; } - @Nonnull - private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( - String thePath, Type theValue) { +private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { + return refInfo.getResourceReference() + .getReferenceElement() + .toUnqualifiedVersionless() + .getValueAsString() + .equals(theSourceId.getValueAsString()); +} - Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); - operation.setName(PARAMETER_OPERATION); - operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE)); - operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath)); - operation.addPart().setName(PARAMETER_VALUE).setValue(theValue); - return operation; - } +@Nonnull +private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( + String thePath, Type theValue) { - private IFhirResourceDao getDao(String theResourceName) { - return myDaoRegistry.getResourceDao(theResourceName); - } + Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); + operation.setName(PARAMETER_OPERATION); + operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE)); + operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath)); + operation.addPart().setName(PARAMETER_VALUE).setValue(theValue); + return operation; +} + +private IFhirResourceDao getDao(String theResourceName) { + return myDaoRegistry.getResourceDao(theResourceName); +} } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index 26d83713aa7e..2f807bcf9d14 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -286,7 +286,7 @@ public class ProviderConstants { public static final String OPERATION_MERGE_TARGET_PATIENT = "target-patient"; public static final String OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER = "target-patient-identifier"; public static final String OPERATION_MERGE_RESULT_PATIENT = "result-patient"; - public static final String OPERATION_MERGE_PAGE_SIZE = "page-size"; + public static final String OPERATION_MERGE_BATCH_SIZE = "batch-size"; public static final String OPERATION_MERGE_PREVIEW = "preview"; public static final String OPERATION_MERGE_DELETE_SOURCE = "delete-source"; public static final String OPERATION_MERGE_OUTPUT_PARAM_INPUT = "input"; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java index ed7b90b9da91..1bd5b37519af 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java @@ -117,6 +117,8 @@ public class JpaStorageSettings extends StorageSettings { private static final boolean DEFAULT_PREVENT_INVALIDATING_CONDITIONAL_MATCH_CRITERIA = false; private static final long DEFAULT_REST_DELETE_BY_URL_RESOURCE_ID_THRESHOLD = 10000; + public static final int DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE = 512; + /** * Do not change default of {@code 0}! * @@ -383,6 +385,7 @@ public class JpaStorageSettings extends StorageSettings { */ @Beta private boolean myIncludeHashIdentityForTokenSearches = false; + private int myMaxTransactionEntriesForWrite = DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE; /** * Constructor @@ -2594,6 +2597,22 @@ public void setRestDeleteByUrlResourceIdThreshold(long theRestDeleteByUrlResourc myRestDeleteByUrlResourceIdThreshold = theRestDeleteByUrlResourceIdThreshold; } + /** + * If we are batching write operations in transactions, what should the maximum number of write operations per + * transaction be? + */ + public int getMaxTransactionEntriesForWrite() { + return myMaxTransactionEntriesForWrite; + } + + /** + * If we are batching write operations in transactions, what should the maximum number of write operations per + * transaction be? + */ + public void setMaxTransactionEntriesForWrite(int theMaxTransactionEntriesForWrite) { + myMaxTransactionEntriesForWrite = theMaxTransactionEntriesForWrite; + } + public enum StoreMetaSourceInformationEnum { NONE(false, false), SOURCE_URI(true, false), diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationInputParameters.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationInputParameters.java index 0df95d13daac..1a8adaa4d2d1 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationInputParameters.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationInputParameters.java @@ -34,10 +34,10 @@ public abstract class MergeOperationInputParameters { private boolean myPreview; private boolean myDeleteSource; private IBaseResource myResultResource; - private final int myPageSize; + private final int myBatchSize; - protected MergeOperationInputParameters(int thePageSize) { - myPageSize = thePageSize; + protected MergeOperationInputParameters(int theBatchSize) { + myBatchSize = theBatchSize; } public abstract String getSourceResourceParameterName(); @@ -114,7 +114,7 @@ public void setTargetResource(IBaseReference theTargetResource) { this.myTargetResource = theTargetResource; } - public int getPageSize() { - return myPageSize; + public int getBatchSize() { + return myBatchSize; } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationInputParameters.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationInputParameters.java index ed8a65b49519..e2a9b67f309e 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationInputParameters.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationInputParameters.java @@ -26,8 +26,8 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER; public class PatientMergeOperationInputParameters extends MergeOperationInputParameters { - public PatientMergeOperationInputParameters(int thePageSize) { - super(thePageSize); + public PatientMergeOperationInputParameters(int theBatchSize) { + super(theBatchSize); } @Override diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index b7753824a66f..7793aaf1a462 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -150,7 +150,7 @@ private void doMerge( return; } - ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest(sourceResource.getIdElement(), targetResource.getIdElement(), theMergeOperationParameters.getPageSize()); + ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest(sourceResource.getIdElement(), targetResource.getIdElement(), theMergeOperationParameters.getBatchSize()); myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); Patient patientToUpdate = prepareTargetPatientForUpdate( diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java index 4847e8af9a9b..9853b4d69f61 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java @@ -20,12 +20,12 @@ public class ReplaceReferenceRequest { @Nonnull public final IIdType targetId; @Nonnull - public final int pageSize; + public final int batchSize; - public ReplaceReferenceRequest(@Nonnull IIdType theSourceId, @Nonnull IIdType theTargetId, int thePageSize) { - sourceId = theSourceId; - targetId = theTargetId; - pageSize = thePageSize; + public ReplaceReferenceRequest(@Nonnull IIdType theSourceId, @Nonnull IIdType theTargetId, int theBatchSize) { + sourceId = theSourceId.toUnqualifiedVersionless(); + targetId = theTargetId.toUnqualifiedVersionless(); + batchSize = theBatchSize; } public void validateOrThrowInvalidParameterException() { From 39d41f2d0d3e1821169f85ca98b6601c38af9bec Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 6 Dec 2024 13:09:13 -0500 Subject: [PATCH 040/148] spotless --- .../BaseJpaResourceProviderPatient.java | 75 ++++++++++--------- .../fhir/jpa/provider/JpaSystemProvider.java | 15 ++-- .../provider/ReplaceReferencesSvcImpl.java | 62 ++++++++------- .../jpa/api/config/JpaStorageSettings.java | 1 + .../jpa/dao/merge/ResourceMergeService.java | 5 +- 5 files changed, 88 insertions(+), 70 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 66b263632874..5fcdc6d1f4a8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -254,11 +254,11 @@ public IBundleProvider patientTypeEverything( everythingParams.setMdmExpand(resolveNullValue(theMdmExpand)); return ((IFhirResourceDaoPatient) getDao()) - .patientTypeEverything( - theServletRequest, - theRequestDetails, - everythingParams, - toFlattenedPatientIdTokenParamList(theId)); + .patientTypeEverything( + theServletRequest, + theRequestDetails, + everythingParams, + toFlattenedPatientIdTokenParamList(theId)); } finally { endRequest(theServletRequest); } @@ -268,40 +268,43 @@ public IBundleProvider patientTypeEverything( * /Patient/$merge */ @Operation( - name = ProviderConstants.OPERATION_MERGE, - canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge") + name = ProviderConstants.OPERATION_MERGE, + canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge") public IBaseParameters patientMerge( - HttpServletRequest theServletRequest, - HttpServletResponse theServletResponse, - ServletRequestDetails theRequestDetails, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER) - List theSourcePatientIdentifier, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER) - List theTargetPatientIdentifier, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT, max = 1) - IBaseReference theSourcePatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT, max = 1) - IBaseReference theTargetPatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PREVIEW, typeName = "boolean", max = 1) - IPrimitiveType thePreview, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_DELETE_SOURCE, typeName = "boolean", max = 1) - IPrimitiveType theDeleteSource, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_RESULT_PATIENT, max = 1) - IBaseResource theResultPatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_BATCH_SIZE, typeName = "unsignedInt") IPrimitiveType theBatchSize) { + HttpServletRequest theServletRequest, + HttpServletResponse theServletResponse, + ServletRequestDetails theRequestDetails, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER) + List theSourcePatientIdentifier, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER) + List theTargetPatientIdentifier, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT, max = 1) + IBaseReference theSourcePatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT, max = 1) + IBaseReference theTargetPatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PREVIEW, typeName = "boolean", max = 1) + IPrimitiveType thePreview, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_DELETE_SOURCE, typeName = "boolean", max = 1) + IPrimitiveType theDeleteSource, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_RESULT_PATIENT, max = 1) + IBaseResource theResultPatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_BATCH_SIZE, typeName = "unsignedInt") + IPrimitiveType theBatchSize) { startRequest(theServletRequest); - @Nonnull Integer batchSize = defaultIfNull(IPrimitiveType.toValueOrNull(theBatchSize), myStorageSettings.getMaxTransactionEntriesForWrite()); + @Nonnull + Integer batchSize = defaultIfNull( + IPrimitiveType.toValueOrNull(theBatchSize), myStorageSettings.getMaxTransactionEntriesForWrite()); try { MergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( - theSourcePatientIdentifier, - theTargetPatientIdentifier, - theSourcePatient, - theTargetPatient, - thePreview, - theDeleteSource, - theResultPatient, - batchSize); + theSourcePatientIdentifier, + theTargetPatientIdentifier, + theSourcePatient, + theTargetPatient, + thePreview, + theDeleteSource, + theResultPatient, + batchSize); IFhirResourceDaoPatient dao = (IFhirResourceDaoPatient) getDao(); ResourceMergeService resourceMergeService = new ResourceMergeService(dao, myReplaceReferencesSvc); @@ -309,7 +312,7 @@ public IBaseParameters patientMerge( FhirContext fhirContext = dao.getContext(); MergeOperationOutcome mergeOutcome = - resourceMergeService.merge(mergeOperationParameters, theRequestDetails); + resourceMergeService.merge(mergeOperationParameters, theRequestDetails); theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); return buildMergeOperationOutputParameters(fhirContext, mergeOutcome, theRequestDetails.getResource()); @@ -349,7 +352,7 @@ private MergeOperationInputParameters buildMergeOperationInputParameters( IPrimitiveType thePreview, IPrimitiveType theDeleteSource, IBaseResource theResultPatient, - int theBatchSize) { + int theBatchSize) { MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(theBatchSize); if (theSourcePatientIdentifier != null) { List sourceResourceIdentifiers = theSourcePatientIdentifier.stream() diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index 41dbf51878d5..fb707d428835 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -154,13 +154,18 @@ public IBaseBundle transaction(RequestDetails theRequestDetails, @TransactionPar "This operation searches for all references matching the provided id and updates them to references to the provided newReferenceTargetId.", shortDefinition = "Repoints referencing resources to another resources instance") public IBaseParameters replaceReferences( - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID) String theSourceId, - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID) String theTargetId, - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PAGE_SIZE, typeName = "unsignedInt") IPrimitiveType theBatchSize, + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID) + String theSourceId, + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID) + String theTargetId, + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PAGE_SIZE, typeName = "unsignedInt") + IPrimitiveType theBatchSize, RequestDetails theRequestDetails) { validateReplaceReferencesParams(theSourceId, theTargetId); - Integer batchSize = defaultIfNull(IPrimitiveType.toValueOrNull(theBatchSize), myStorageSettings.getMaxTransactionEntriesForWrite()); - ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest(new IdDt(theSourceId), new IdDt(theTargetId), batchSize); + Integer batchSize = defaultIfNull( + IPrimitiveType.toValueOrNull(theBatchSize), myStorageSettings.getMaxTransactionEntriesForWrite()); + ReplaceReferenceRequest replaceReferenceRequest = + new ReplaceReferenceRequest(new IdDt(theSourceId), new IdDt(theTargetId), batchSize); return getReplaceReferencesSvc().replaceReferences(replaceReferenceRequest, theRequestDetails); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 28a1707b4d8a..be4b7351e497 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -135,8 +135,11 @@ private IBaseParameters replaceReferencesPreferAsync( JpaPid sourcePid = myIdHelperService.getPidOrThrowException( RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); - Stream pidStream = myResourceLinkDao.streamSourcePidsForTargetPid(sourcePid.getId()).map(JpaPid::fromId); - StopLimitAccumulator accumulator = StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); + Stream pidStream = myResourceLinkDao + .streamSourcePidsForTargetPid(sourcePid.getId()) + .map(JpaPid::fromId); + StopLimitAccumulator accumulator = + StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); if (accumulator.isTruncated()) { ourLog.info("Too many results. Switching to asynchronous reference replacement."); @@ -165,18 +168,21 @@ private IBaseParameters replaceReferencesInTransaction( String fhirType = referencingResource.fhirType(); myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() - .filter(refInfo -> matches(refInfo, theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource - .map(refInfo -> createReplaceReferencePatchOperation( - fhirType + "." + refInfo.getName(), - new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) - .forEach(params::addParameter); // Add each operation to parameters + .filter(refInfo -> matches( + refInfo, + theReplaceReferenceRequest + .sourceId)) // We only care about references to our source resource + .map(refInfo -> createReplaceReferencePatchOperation( + fhirType + "." + refInfo.getName(), + new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) + .forEach(params::addParameter); // Add each operation to parameters IFhirResourceDao resDao = myDaoRegistry.getResourceDao(fhirType); IIdType resourceId = referencingResource.getIdElement(); MethodOutcome result = - resDao.patch(resourceId, null, PatchTypeEnum.FHIR_PATCH_JSON, null, params, theRequestDetails); + resDao.patch(resourceId, null, PatchTypeEnum.FHIR_PATCH_JSON, null, params, theRequestDetails); resultParams.addParameter().setResource((Resource) result.getOperationOutcome()); }); @@ -184,27 +190,27 @@ private IBaseParameters replaceReferencesInTransaction( return resultParams; } -private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { - return refInfo.getResourceReference() - .getReferenceElement() - .toUnqualifiedVersionless() - .getValueAsString() - .equals(theSourceId.getValueAsString()); -} + private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { + return refInfo.getResourceReference() + .getReferenceElement() + .toUnqualifiedVersionless() + .getValueAsString() + .equals(theSourceId.getValueAsString()); + } -@Nonnull -private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( - String thePath, Type theValue) { + @Nonnull + private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( + String thePath, Type theValue) { - Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); - operation.setName(PARAMETER_OPERATION); - operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE)); - operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath)); - operation.addPart().setName(PARAMETER_VALUE).setValue(theValue); - return operation; -} + Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); + operation.setName(PARAMETER_OPERATION); + operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE)); + operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath)); + operation.addPart().setName(PARAMETER_VALUE).setValue(theValue); + return operation; + } -private IFhirResourceDao getDao(String theResourceName) { - return myDaoRegistry.getResourceDao(theResourceName); -} + private IFhirResourceDao getDao(String theResourceName) { + return myDaoRegistry.getResourceDao(theResourceName); + } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java index 1bd5b37519af..028e1472e0f0 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java @@ -385,6 +385,7 @@ public class JpaStorageSettings extends StorageSettings { */ @Beta private boolean myIncludeHashIdentityForTokenSearches = false; + private int myMaxTransactionEntriesForWrite = DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE; /** diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index d0d76ebdc3ed..d2116c492ec3 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -159,7 +159,10 @@ private void doMerge( return; } - ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest(sourceResource.getIdElement(), targetResource.getIdElement(), theMergeOperationParameters.getBatchSize()); + ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest( + sourceResource.getIdElement(), + targetResource.getIdElement(), + theMergeOperationParameters.getBatchSize()); myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); Patient patientToUpdate = prepareTargetPatientForUpdate( From 6e2294048a47b228c77bf9b9c5c970d4221a6a7a Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 6 Dec 2024 14:18:34 -0500 Subject: [PATCH 041/148] switched to patch transaction --- .../provider/ReplaceReferencesSvcImpl.java | 158 ++++++++---------- .../jpa/provider/r4/PatientMergeR4Test.java | 18 +- .../jpa/dao/merge/ResourceMergeService.java | 1 + 3 files changed, 85 insertions(+), 92 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index be4b7351e497..2a0fd16dba9c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -20,33 +20,31 @@ package ca.uhn.fhir.jpa.provider; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.api.PatchTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.util.ResourceReferenceInfo; import ca.uhn.fhir.util.StopLimitAccumulator; import jakarta.annotation.Nonnull; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.Meta; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Type; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,6 +56,7 @@ import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_PATH; import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_TYPE; import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_VALUE; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { private static final Logger ourLog = LoggerFactory.getLogger(ReplaceReferencesSvcImpl.class); @@ -68,11 +67,11 @@ public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { private final IResourceLinkDao myResourceLinkDao; public ReplaceReferencesSvcImpl( - FhirContext theFhirContext, - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IdHelperService theIdHelperService, - IResourceLinkDao theResourceLinkDao) { + FhirContext theFhirContext, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IdHelperService theIdHelperService, + IResourceLinkDao theResourceLinkDao) { myFhirContext = theFhirContext; myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; @@ -82,7 +81,7 @@ public ReplaceReferencesSvcImpl( @Override public IBaseParameters replaceReferences( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { theReplaceReferenceRequest.validateOrThrowInvalidParameterException(); if (theRequestDetails.isPreferAsync()) { @@ -97,13 +96,13 @@ public Integer countResourcesReferencingResource(IIdType theResourceId, RequestD return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { // FIXME KHS get partition from request JpaPid sourcePid = - myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); + myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); return myResourceLinkDao.countResourcesTargetingPid(sourcePid.getId()); }); } private IBaseParameters replaceReferencesPreferAsync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // FIXME KHS return null; } @@ -111,96 +110,87 @@ private IBaseParameters replaceReferencesPreferAsync( /** * Try to perform the operation synchronously. However if there is more than a page of results, fall back to asynchronous operation */ - private @NotNull IBaseParameters replaceReferencesPreferSync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { - - // todo jm: this could be problematic depending on referenceing object set size, however we are adding - // batch job option to handle that case as part of this feature - IFhirResourceDao dao = getDao(theReplaceReferenceRequest.sourceId.getResourceType()); - if (dao == null) { - throw new InternalErrorException(Msg.code(2582) + "Couldn't obtain DAO for resource type" - + theReplaceReferenceRequest.sourceId.getResourceType()); - } - - return myHapiTransactionService - .withRequest(theRequestDetails) - .execute(() -> performReplaceInTransaction(theReplaceReferenceRequest, theRequestDetails, dao)); - } - - private @Nullable IBaseParameters performReplaceInTransaction( - ReplaceReferenceRequest theReplaceReferenceRequest, - RequestDetails theRequestDetails, - IFhirResourceDao dao) { - // FIXME KHS get partition from request - JpaPid sourcePid = myIdHelperService.getPidOrThrowException( - RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); + @Nonnull + private IBaseParameters replaceReferencesPreferSync( + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { - Stream pidStream = myResourceLinkDao - .streamSourcePidsForTargetPid(sourcePid.getId()) - .map(JpaPid::fromId); - StopLimitAccumulator accumulator = - StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); + // TODO KHS get partition from request + StopLimitAccumulator accumulator = myHapiTransactionService + .withRequest(theRequestDetails) + .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); if (accumulator.isTruncated()) { - ourLog.info("Too many results. Switching to asynchronous reference replacement."); + ourLog.warn("Too many results. Switching to asynchronous reference replacement."); return replaceReferencesPreferAsync(theReplaceReferenceRequest, theRequestDetails); } - Stream referencingResourceStream = accumulator.getItemList().stream() - .map(myIdHelperService::translatePidIdToForcedIdWithCache) - .filter(Optional::isPresent) - .map(Optional::get) - .map(IdDt::new) - .map(id -> getDao(id.getResourceType()).read(id, theRequestDetails)); - - return replaceReferencesInTransaction(referencingResourceStream, theReplaceReferenceRequest, theRequestDetails); - } - - private IBaseParameters replaceReferencesInTransaction( - Stream theReferencingResourceStream, - ReplaceReferenceRequest theReplaceReferenceRequest, - RequestDetails theRequestDetails) { - - Parameters resultParams = new Parameters(); - - theReferencingResourceStream.forEach(referencingResource -> { - Parameters params = new Parameters(); + Bundle patchBundle = buildPatchBundle(theReplaceReferenceRequest, theRequestDetails, accumulator); - String fhirType = referencingResource.fhirType(); - myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() - .filter(refInfo -> matches( - refInfo, - theReplaceReferenceRequest - .sourceId)) // We only care about references to our source resource - .map(refInfo -> createReplaceReferencePatchOperation( - fhirType + "." + refInfo.getName(), - new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) - .forEach(params::addParameter); // Add each operation to parameters + IFhirSystemDao systemDao = myDaoRegistry.getSystemDao(); + Bundle result = systemDao.transaction(theRequestDetails, patchBundle); - IFhirResourceDao resDao = myDaoRegistry.getResourceDao(fhirType); + Parameters retval = new Parameters(); + retval.addParameter().setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME).setResource(result); + return retval; + } - IIdType resourceId = referencingResource.getIdElement(); + private @NotNull StopLimitAccumulator getAllPidsWithLimit(ReplaceReferenceRequest theReplaceReferenceRequest) { + JpaPid sourcePid = myIdHelperService.getPidOrThrowException( + RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); - MethodOutcome result = - resDao.patch(resourceId, null, PatchTypeEnum.FHIR_PATCH_JSON, null, params, theRequestDetails); + Stream pidStream = myResourceLinkDao + .streamSourcePidsForTargetPid(sourcePid.getId()) + .map(JpaPid::fromId); + StopLimitAccumulator accumulator = + StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); + return accumulator; + } - resultParams.addParameter().setResource((Resource) result.getOperationOutcome()); - }); + private Bundle buildPatchBundle(ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails, StopLimitAccumulator accumulator) { + BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); + + accumulator.getItemList().stream() + .map(myIdHelperService::translatePidIdToForcedIdWithCache) + .filter(Optional::isPresent) + .map(Optional::get) + .map(IdDt::new) + .forEach(referencingResourceId -> { + IFhirResourceDao dao = getDao(referencingResourceId.getResourceType()); + IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); + Parameters patchParams = buildPatchParams(theReplaceReferenceRequest, resource); + IIdType resourceId = resource.getIdElement(); + bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); + }); + Bundle patchBundle = bundleBuilder.getBundleTyped(); + return patchBundle; + } - return resultParams; + private @NotNull Parameters buildPatchParams(ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { + Parameters params = new Parameters(); + + myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() + .filter(refInfo -> matches( + refInfo, + theReplaceReferenceRequest + .sourceId)) // We only care about references to our source resource + .map(refInfo -> createReplaceReferencePatchOperation( + referencingResource.fhirType() + "." + refInfo.getName(), + new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) + .forEach(params::addParameter); // Add each operation to parameters + return params; } private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { return refInfo.getResourceReference() - .getReferenceElement() - .toUnqualifiedVersionless() - .getValueAsString() - .equals(theSourceId.getValueAsString()); + .getReferenceElement() + .toUnqualifiedVersionless() + .getValueAsString() + .equals(theSourceId.getValueAsString()); } @Nonnull private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( - String thePath, Type theValue) { + String thePath, Type theValue) { Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); operation.setName(PARAMETER_OPERATION); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 0839862a10dc..1dcad265441f 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -65,6 +65,7 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_RESULT_PATIENT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; @@ -462,11 +463,12 @@ void testReplaceReferences(boolean isAsync) throws IOException { .execute(); // validate - Pattern expectedPatchIssuePattern = Pattern.compile("Successfully patched resource \"(Observation|Encounter|CarePlan)/\\d+/_history/\\d+\"\\. Took \\d+ms\\."); - assertThat(outParams.getParameter()) - .hasSize(23) - .allSatisfy(component -> - assertThat(component.getResource()) + Pattern expectedPatchIssuePattern = Pattern.compile("Successfully patched resource \"(Observation|Encounter|CarePlan)/\\d+/_history/\\d+\"."); + assertThat(outParams.getParameter()).hasSize(1); + Bundle patchResultBundle = (Bundle) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME).getResource(); + assertThat(patchResultBundle.getEntry()).hasSize(23) + .allSatisfy(entry -> + assertThat(entry.getResponse().getOutcome()) .isInstanceOf(OperationOutcome.class) .extracting(OperationOutcome.class::cast) .extracting(OperationOutcome::getIssue) @@ -478,12 +480,12 @@ void testReplaceReferences(boolean isAsync) throws IOException { // Check that the linked resources were updated - Bundle bundle = fetchBundle(myServerBase + "/" + myTargetPatId + "/$everything?_format=json&_count=100"); + Bundle everythingBundle = fetchBundle(myServerBase + "/" + myTargetPatId + "/$everything?_format=json&_count=100"); - assertNull(bundle.getLink("next")); + assertNull(everythingBundle.getLink("next")); Set actual = new HashSet<>(); - for (BundleEntryComponent nextEntry : bundle.getEntry()) { + for (BundleEntryComponent nextEntry : everythingBundle.getEntry()) { actual.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless()); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index d2116c492ec3..dc783bff85f8 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -163,6 +163,7 @@ private void doMerge( sourceResource.getIdElement(), targetResource.getIdElement(), theMergeOperationParameters.getBatchSize()); + // FIXME KHS check if it needs to go async myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); Patient patientToUpdate = prepareTargetPatientForUpdate( From 81af354f197cbb53134b58b73b091b000f4ac1e9 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 6 Dec 2024 14:18:54 -0500 Subject: [PATCH 042/148] switched to patch transaction --- .../provider/ReplaceReferencesSvcImpl.java | 92 ++++++++++--------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 2a0fd16dba9c..c9df57de6cb5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -67,11 +67,11 @@ public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { private final IResourceLinkDao myResourceLinkDao; public ReplaceReferencesSvcImpl( - FhirContext theFhirContext, - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IdHelperService theIdHelperService, - IResourceLinkDao theResourceLinkDao) { + FhirContext theFhirContext, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IdHelperService theIdHelperService, + IResourceLinkDao theResourceLinkDao) { myFhirContext = theFhirContext; myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; @@ -81,7 +81,7 @@ public ReplaceReferencesSvcImpl( @Override public IBaseParameters replaceReferences( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { theReplaceReferenceRequest.validateOrThrowInvalidParameterException(); if (theRequestDetails.isPreferAsync()) { @@ -96,13 +96,13 @@ public Integer countResourcesReferencingResource(IIdType theResourceId, RequestD return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { // FIXME KHS get partition from request JpaPid sourcePid = - myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); + myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); return myResourceLinkDao.countResourcesTargetingPid(sourcePid.getId()); }); } private IBaseParameters replaceReferencesPreferAsync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // FIXME KHS return null; } @@ -112,12 +112,12 @@ private IBaseParameters replaceReferencesPreferAsync( */ @Nonnull private IBaseParameters replaceReferencesPreferSync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // TODO KHS get partition from request StopLimitAccumulator accumulator = myHapiTransactionService - .withRequest(theRequestDetails) - .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); + .withRequest(theRequestDetails) + .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); if (accumulator.isTruncated()) { ourLog.warn("Too many results. Switching to asynchronous reference replacement."); @@ -130,67 +130,73 @@ private IBaseParameters replaceReferencesPreferSync( Bundle result = systemDao.transaction(theRequestDetails, patchBundle); Parameters retval = new Parameters(); - retval.addParameter().setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME).setResource(result); + retval.addParameter() + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME) + .setResource(result); return retval; } - private @NotNull StopLimitAccumulator getAllPidsWithLimit(ReplaceReferenceRequest theReplaceReferenceRequest) { + private @NotNull StopLimitAccumulator getAllPidsWithLimit( + ReplaceReferenceRequest theReplaceReferenceRequest) { JpaPid sourcePid = myIdHelperService.getPidOrThrowException( - RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); + RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); Stream pidStream = myResourceLinkDao - .streamSourcePidsForTargetPid(sourcePid.getId()) - .map(JpaPid::fromId); + .streamSourcePidsForTargetPid(sourcePid.getId()) + .map(JpaPid::fromId); StopLimitAccumulator accumulator = - StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); + StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); return accumulator; } - private Bundle buildPatchBundle(ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails, StopLimitAccumulator accumulator) { + private Bundle buildPatchBundle( + ReplaceReferenceRequest theReplaceReferenceRequest, + RequestDetails theRequestDetails, + StopLimitAccumulator accumulator) { BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); accumulator.getItemList().stream() - .map(myIdHelperService::translatePidIdToForcedIdWithCache) - .filter(Optional::isPresent) - .map(Optional::get) - .map(IdDt::new) - .forEach(referencingResourceId -> { - IFhirResourceDao dao = getDao(referencingResourceId.getResourceType()); - IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); - Parameters patchParams = buildPatchParams(theReplaceReferenceRequest, resource); - IIdType resourceId = resource.getIdElement(); - bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); - }); + .map(myIdHelperService::translatePidIdToForcedIdWithCache) + .filter(Optional::isPresent) + .map(Optional::get) + .map(IdDt::new) + .forEach(referencingResourceId -> { + IFhirResourceDao dao = getDao(referencingResourceId.getResourceType()); + IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); + Parameters patchParams = buildPatchParams(theReplaceReferenceRequest, resource); + IIdType resourceId = resource.getIdElement(); + bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); + }); Bundle patchBundle = bundleBuilder.getBundleTyped(); return patchBundle; } - private @NotNull Parameters buildPatchParams(ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { + private @NotNull Parameters buildPatchParams( + ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { Parameters params = new Parameters(); myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() - .filter(refInfo -> matches( - refInfo, - theReplaceReferenceRequest - .sourceId)) // We only care about references to our source resource - .map(refInfo -> createReplaceReferencePatchOperation( - referencingResource.fhirType() + "." + refInfo.getName(), - new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) - .forEach(params::addParameter); // Add each operation to parameters + .filter(refInfo -> matches( + refInfo, + theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource + .map(refInfo -> createReplaceReferencePatchOperation( + referencingResource.fhirType() + "." + refInfo.getName(), + new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) + .forEach(params::addParameter); // Add each operation to parameters return params; } private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { return refInfo.getResourceReference() - .getReferenceElement() - .toUnqualifiedVersionless() - .getValueAsString() - .equals(theSourceId.getValueAsString()); + .getReferenceElement() + .toUnqualifiedVersionless() + .getValueAsString() + .equals(theSourceId.getValueAsString()); } @Nonnull private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( - String thePath, Type theValue) { + String thePath, Type theValue) { Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); operation.setName(PARAMETER_OPERATION); From 07c2d7d8127975355eec1e672fa7bcd2b821681d Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 6 Dec 2024 14:31:34 -0500 Subject: [PATCH 043/148] switched to patch transaction --- .../provider/ReplaceReferencesSvcImpl.java | 11 +++++-- .../jpa/provider/r4/PatientMergeR4Test.java | 32 ++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index c9df57de6cb5..566c36393dab 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -43,6 +43,7 @@ import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.Type; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -57,6 +58,7 @@ import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_TYPE; import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_VALUE; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { private static final Logger ourLog = LoggerFactory.getLogger(ReplaceReferencesSvcImpl.class); @@ -103,8 +105,13 @@ public Integer countResourcesReferencingResource(IIdType theResourceId, RequestD private IBaseParameters replaceReferencesPreferAsync( ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { - // FIXME KHS - return null; + // FIXME KHS actually start the job + Task task = new Task(); + task.setStatus(Task.TaskStatus.INPROGRESS); + myDaoRegistry.getResourceDao(Task.class).create(task, theRequestDetails); + Parameters retval = new Parameters(); + retval.addParameter().setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).setResource(task); + return retval; } /** diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 1dcad265441f..bc9e4e33e4b1 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -66,6 +66,7 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_RESULT_PATIENT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; @@ -462,9 +463,38 @@ void testReplaceReferences(boolean isAsync) throws IOException { .returnResourceType(Parameters.class) .execute(); + assertThat(outParams.getParameter()).hasSize(1); + + if (isAsync) { + Task task = (Task) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).getResource(); + + await().until(() -> taskCompleted(task.getIdElement())); + + Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); + + // FIXME KHS the rest of these asserts will likely need to be tweaked + Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); + + // Assert on the output type + Coding taskType = taskOutput.getType().getCodingFirstRep(); + assertEquals("http://hl7.org/fhir/ValueSet/resource-types", taskType.getSystem()); + assertEquals("OperationOutcome", taskType.getCode()); + + List containedResources = taskWithOutput.getContained(); + assertThat(containedResources) + .hasSize(1) + .element(0) + .isInstanceOf(OperationOutcome.class); + + OperationOutcome containedOutcome = (OperationOutcome) containedResources.get(0); + + Reference outputRef = (Reference) taskOutput.getValue(); + OperationOutcome outcome = (OperationOutcome) outputRef.getResource(); + assertTrue(containedOutcome.equalsDeep(outcome)); + } + // validate Pattern expectedPatchIssuePattern = Pattern.compile("Successfully patched resource \"(Observation|Encounter|CarePlan)/\\d+/_history/\\d+\"."); - assertThat(outParams.getParameter()).hasSize(1); Bundle patchResultBundle = (Bundle) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME).getResource(); assertThat(patchResultBundle.getEntry()).hasSize(23) .allSatisfy(entry -> From d78ebe00cb150f0cd3ab5ee21f037fc299aa9393 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 6 Dec 2024 14:31:47 -0500 Subject: [PATCH 044/148] switched to patch transaction --- .../ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 566c36393dab..62fc345879ef 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -110,7 +110,9 @@ private IBaseParameters replaceReferencesPreferAsync( task.setStatus(Task.TaskStatus.INPROGRESS); myDaoRegistry.getResourceDao(Task.class).create(task, theRequestDetails); Parameters retval = new Parameters(); - retval.addParameter().setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).setResource(task); + retval.addParameter() + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) + .setResource(task); return retval; } From 570ec4fac42e2994cb363598b8993c3ec488bd7f Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 6 Dec 2024 16:21:04 -0500 Subject: [PATCH 045/148] strip version from returned task --- .../provider/ReplaceReferencesSvcImpl.java | 128 +++++++++++------- .../jpa/provider/r4/PatientMergeR4Test.java | 9 +- 2 files changed, 87 insertions(+), 50 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 62fc345879ef..7f1cabf62eca 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -30,10 +30,12 @@ import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.util.ResourceReferenceInfo; import ca.uhn.fhir.util.StopLimitAccumulator; import jakarta.annotation.Nonnull; +import jakarta.annotation.PreDestroy; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -50,6 +52,8 @@ import org.slf4j.LoggerFactory; import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.stream.Stream; import static ca.uhn.fhir.jpa.patch.FhirPatch.OPERATION_REPLACE; @@ -67,13 +71,22 @@ public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { private final HapiTransactionService myHapiTransactionService; private final IdHelperService myIdHelperService; private final IResourceLinkDao myResourceLinkDao; + // FIXME remove + private final ExecutorService myFakeExecutor = Executors.newSingleThreadExecutor(); + + + // FIXME remove + @PreDestroy + public void preDestroy() { + myFakeExecutor.shutdown(); + } public ReplaceReferencesSvcImpl( - FhirContext theFhirContext, - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IdHelperService theIdHelperService, - IResourceLinkDao theResourceLinkDao) { + FhirContext theFhirContext, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IdHelperService theIdHelperService, + IResourceLinkDao theResourceLinkDao) { myFhirContext = theFhirContext; myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; @@ -83,7 +96,7 @@ public ReplaceReferencesSvcImpl( @Override public IBaseParameters replaceReferences( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { theReplaceReferenceRequest.validateOrThrowInvalidParameterException(); if (theRequestDetails.isPreferAsync()) { @@ -98,35 +111,56 @@ public Integer countResourcesReferencingResource(IIdType theResourceId, RequestD return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { // FIXME KHS get partition from request JpaPid sourcePid = - myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); + myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); return myResourceLinkDao.countResourcesTargetingPid(sourcePid.getId()); }); } private IBaseParameters replaceReferencesPreferAsync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // FIXME KHS actually start the job Task task = new Task(); task.setStatus(Task.TaskStatus.INPROGRESS); myDaoRegistry.getResourceDao(Task.class).create(task, theRequestDetails); + // Make a copy so we can strip the version number so they don't accidentally keep polling for an + // out of date version + Task returnedTask = task.copy(); + returnedTask.setIdElement(task.getIdElement().toUnqualifiedVersionless()); + returnedTask.getMeta().setVersionId(null); Parameters retval = new Parameters(); retval.addParameter() - .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) - .setResource(task); + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) + .setResource(returnedTask); + + fakeBackgroundTaskUpdate(task); return retval; } + // FIXME KHS remove this + private void fakeBackgroundTaskUpdate(Task theTask) { + myFakeExecutor.submit(() -> { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + theTask.setStatus(Task.TaskStatus.COMPLETED); + myDaoRegistry.getResourceDao(Task.class).update(theTask, new SystemRequestDetails()); + ourLog.info("Updated task {} to COMPLETED.", theTask.getId()); + }); + } + /** * Try to perform the operation synchronously. However if there is more than a page of results, fall back to asynchronous operation */ @Nonnull private IBaseParameters replaceReferencesPreferSync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // TODO KHS get partition from request StopLimitAccumulator accumulator = myHapiTransactionService - .withRequest(theRequestDetails) - .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); + .withRequest(theRequestDetails) + .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); if (accumulator.isTruncated()) { ourLog.warn("Too many results. Switching to asynchronous reference replacement."); @@ -140,72 +174,72 @@ private IBaseParameters replaceReferencesPreferSync( Parameters retval = new Parameters(); retval.addParameter() - .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME) - .setResource(result); + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME) + .setResource(result); return retval; } private @NotNull StopLimitAccumulator getAllPidsWithLimit( - ReplaceReferenceRequest theReplaceReferenceRequest) { + ReplaceReferenceRequest theReplaceReferenceRequest) { JpaPid sourcePid = myIdHelperService.getPidOrThrowException( - RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); + RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); Stream pidStream = myResourceLinkDao - .streamSourcePidsForTargetPid(sourcePid.getId()) - .map(JpaPid::fromId); + .streamSourcePidsForTargetPid(sourcePid.getId()) + .map(JpaPid::fromId); StopLimitAccumulator accumulator = - StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); + StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); return accumulator; } private Bundle buildPatchBundle( - ReplaceReferenceRequest theReplaceReferenceRequest, - RequestDetails theRequestDetails, - StopLimitAccumulator accumulator) { + ReplaceReferenceRequest theReplaceReferenceRequest, + RequestDetails theRequestDetails, + StopLimitAccumulator accumulator) { BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); accumulator.getItemList().stream() - .map(myIdHelperService::translatePidIdToForcedIdWithCache) - .filter(Optional::isPresent) - .map(Optional::get) - .map(IdDt::new) - .forEach(referencingResourceId -> { - IFhirResourceDao dao = getDao(referencingResourceId.getResourceType()); - IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); - Parameters patchParams = buildPatchParams(theReplaceReferenceRequest, resource); - IIdType resourceId = resource.getIdElement(); - bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); - }); + .map(myIdHelperService::translatePidIdToForcedIdWithCache) + .filter(Optional::isPresent) + .map(Optional::get) + .map(IdDt::new) + .forEach(referencingResourceId -> { + IFhirResourceDao dao = getDao(referencingResourceId.getResourceType()); + IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); + Parameters patchParams = buildPatchParams(theReplaceReferenceRequest, resource); + IIdType resourceId = resource.getIdElement(); + bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); + }); Bundle patchBundle = bundleBuilder.getBundleTyped(); return patchBundle; } private @NotNull Parameters buildPatchParams( - ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { + ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { Parameters params = new Parameters(); myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() - .filter(refInfo -> matches( - refInfo, - theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource - .map(refInfo -> createReplaceReferencePatchOperation( - referencingResource.fhirType() + "." + refInfo.getName(), - new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) - .forEach(params::addParameter); // Add each operation to parameters + .filter(refInfo -> matches( + refInfo, + theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource + .map(refInfo -> createReplaceReferencePatchOperation( + referencingResource.fhirType() + "." + refInfo.getName(), + new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) + .forEach(params::addParameter); // Add each operation to parameters return params; } private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { return refInfo.getResourceReference() - .getReferenceElement() - .toUnqualifiedVersionless() - .getValueAsString() - .equals(theSourceId.getValueAsString()); + .getReferenceElement() + .toUnqualifiedVersionless() + .getValueAsString() + .equals(theSourceId.getValueAsString()); } @Nonnull private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( - String thePath, Type theValue) { + String thePath, Type theValue) { Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); operation.setName(PARAMETER_OPERATION); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index bc9e4e33e4b1..35a65e72eb93 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -236,6 +236,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa // Assert Task if (isAsync) { Task task = (Task) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource(); + ourLog.info("Got task {}", task.getId()); await().until(() -> taskCompleted(task.getIdElement())); Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); @@ -343,8 +344,9 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa } } - private Boolean taskCompleted(IdType theTask) { - Task updatedTask = myTaskDao.read(theTask, mySrd); + private Boolean taskCompleted(IdType theTaskId) { + Task updatedTask = myTaskDao.read(theTaskId, mySrd); + ourLog.info("Task {} status is {}", theTaskId, updatedTask.getStatus()); return updatedTask.getStatus() == Task.TaskStatus.COMPLETED; } @@ -467,7 +469,8 @@ void testReplaceReferences(boolean isAsync) throws IOException { if (isAsync) { Task task = (Task) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).getResource(); - + assertNull(task.getIdElement().getVersionIdPart()); + ourLog.info("Got task {}", task.getId()); await().until(() -> taskCompleted(task.getIdElement())); Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); From 1d0a114bb66f740ef07c10c01554b1d2819f28b1 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 6 Dec 2024 16:21:18 -0500 Subject: [PATCH 046/148] spotless --- .../provider/ReplaceReferencesSvcImpl.java | 95 +++++++++---------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 7f1cabf62eca..9b13bf2f7fa6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -74,7 +74,6 @@ public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { // FIXME remove private final ExecutorService myFakeExecutor = Executors.newSingleThreadExecutor(); - // FIXME remove @PreDestroy public void preDestroy() { @@ -82,11 +81,11 @@ public void preDestroy() { } public ReplaceReferencesSvcImpl( - FhirContext theFhirContext, - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IdHelperService theIdHelperService, - IResourceLinkDao theResourceLinkDao) { + FhirContext theFhirContext, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IdHelperService theIdHelperService, + IResourceLinkDao theResourceLinkDao) { myFhirContext = theFhirContext; myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; @@ -96,7 +95,7 @@ public ReplaceReferencesSvcImpl( @Override public IBaseParameters replaceReferences( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { theReplaceReferenceRequest.validateOrThrowInvalidParameterException(); if (theRequestDetails.isPreferAsync()) { @@ -111,13 +110,13 @@ public Integer countResourcesReferencingResource(IIdType theResourceId, RequestD return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { // FIXME KHS get partition from request JpaPid sourcePid = - myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); + myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); return myResourceLinkDao.countResourcesTargetingPid(sourcePid.getId()); }); } private IBaseParameters replaceReferencesPreferAsync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // FIXME KHS actually start the job Task task = new Task(); task.setStatus(Task.TaskStatus.INPROGRESS); @@ -129,8 +128,8 @@ private IBaseParameters replaceReferencesPreferAsync( returnedTask.getMeta().setVersionId(null); Parameters retval = new Parameters(); retval.addParameter() - .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) - .setResource(returnedTask); + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) + .setResource(returnedTask); fakeBackgroundTaskUpdate(task); return retval; @@ -155,12 +154,12 @@ private void fakeBackgroundTaskUpdate(Task theTask) { */ @Nonnull private IBaseParameters replaceReferencesPreferSync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // TODO KHS get partition from request StopLimitAccumulator accumulator = myHapiTransactionService - .withRequest(theRequestDetails) - .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); + .withRequest(theRequestDetails) + .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); if (accumulator.isTruncated()) { ourLog.warn("Too many results. Switching to asynchronous reference replacement."); @@ -174,72 +173,72 @@ private IBaseParameters replaceReferencesPreferSync( Parameters retval = new Parameters(); retval.addParameter() - .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME) - .setResource(result); + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME) + .setResource(result); return retval; } private @NotNull StopLimitAccumulator getAllPidsWithLimit( - ReplaceReferenceRequest theReplaceReferenceRequest) { + ReplaceReferenceRequest theReplaceReferenceRequest) { JpaPid sourcePid = myIdHelperService.getPidOrThrowException( - RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); + RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); Stream pidStream = myResourceLinkDao - .streamSourcePidsForTargetPid(sourcePid.getId()) - .map(JpaPid::fromId); + .streamSourcePidsForTargetPid(sourcePid.getId()) + .map(JpaPid::fromId); StopLimitAccumulator accumulator = - StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); + StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); return accumulator; } private Bundle buildPatchBundle( - ReplaceReferenceRequest theReplaceReferenceRequest, - RequestDetails theRequestDetails, - StopLimitAccumulator accumulator) { + ReplaceReferenceRequest theReplaceReferenceRequest, + RequestDetails theRequestDetails, + StopLimitAccumulator accumulator) { BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); accumulator.getItemList().stream() - .map(myIdHelperService::translatePidIdToForcedIdWithCache) - .filter(Optional::isPresent) - .map(Optional::get) - .map(IdDt::new) - .forEach(referencingResourceId -> { - IFhirResourceDao dao = getDao(referencingResourceId.getResourceType()); - IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); - Parameters patchParams = buildPatchParams(theReplaceReferenceRequest, resource); - IIdType resourceId = resource.getIdElement(); - bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); - }); + .map(myIdHelperService::translatePidIdToForcedIdWithCache) + .filter(Optional::isPresent) + .map(Optional::get) + .map(IdDt::new) + .forEach(referencingResourceId -> { + IFhirResourceDao dao = getDao(referencingResourceId.getResourceType()); + IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); + Parameters patchParams = buildPatchParams(theReplaceReferenceRequest, resource); + IIdType resourceId = resource.getIdElement(); + bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); + }); Bundle patchBundle = bundleBuilder.getBundleTyped(); return patchBundle; } private @NotNull Parameters buildPatchParams( - ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { + ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { Parameters params = new Parameters(); myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() - .filter(refInfo -> matches( - refInfo, - theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource - .map(refInfo -> createReplaceReferencePatchOperation( - referencingResource.fhirType() + "." + refInfo.getName(), - new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) - .forEach(params::addParameter); // Add each operation to parameters + .filter(refInfo -> matches( + refInfo, + theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource + .map(refInfo -> createReplaceReferencePatchOperation( + referencingResource.fhirType() + "." + refInfo.getName(), + new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) + .forEach(params::addParameter); // Add each operation to parameters return params; } private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { return refInfo.getResourceReference() - .getReferenceElement() - .toUnqualifiedVersionless() - .getValueAsString() - .equals(theSourceId.getValueAsString()); + .getReferenceElement() + .toUnqualifiedVersionless() + .getValueAsString() + .equals(theSourceId.getValueAsString()); } @Nonnull private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( - String thePath, Type theValue) { + String thePath, Type theValue) { Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); operation.setName(PARAMETER_OPERATION); From e63c00b2b94843ef106eed89b42d215d8e0ea2df Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Sun, 8 Dec 2024 18:27:49 -0500 Subject: [PATCH 047/148] spotless --- .../fhir/jpa/dao/JpaPersistedResourceValidationSupport.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java index 61230093b92d..271cc51cdccc 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/JpaPersistedResourceValidationSupport.java @@ -236,7 +236,8 @@ private IBaseResource doFetchResource(@Nullable Class< SearchParameterMap params = new SearchParameterMap(); params.setLoadSynchronousUpTo(1); int versionSeparator = theUri.lastIndexOf('|'); - if (versionSeparator != -1) {params.add(StructureDefinition.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); + if (versionSeparator != -1) { + params.add(StructureDefinition.SP_VERSION, new TokenParam(theUri.substring(versionSeparator + 1))); params.add(StructureDefinition.SP_URL, new UriParam(theUri.substring(0, versionSeparator))); } else { params.add(StructureDefinition.SP_URL, new UriParam(theUri)); From 1feadbf90d1e6d0225db18c9dfe621988977cb03 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Sun, 8 Dec 2024 20:46:26 -0500 Subject: [PATCH 048/148] async replace references test passes --- .../BaseJpaResourceProviderPatient.java | 7 +- .../fhir/jpa/provider/JpaSystemProvider.java | 5 +- .../provider/ReplaceReferencesSvcImpl.java | 217 +++++++++++------- .../jpa/provider/r4/PatientMergeR4Test.java | 21 +- .../jpa/provider/ReplaceReferenceRequest.java | 2 - 5 files changed, 154 insertions(+), 98 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 5fcdc6d1f4a8..bd40e469eb13 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -292,9 +292,12 @@ public IBaseParameters patientMerge( IPrimitiveType theBatchSize) { startRequest(theServletRequest); - @Nonnull - Integer batchSize = defaultIfNull( + int batchSize = defaultIfNull( IPrimitiveType.toValueOrNull(theBatchSize), myStorageSettings.getMaxTransactionEntriesForWrite()); + if (batchSize > myStorageSettings.getMaxTransactionEntriesForWrite()) { + batchSize = myStorageSettings.getMaxTransactionEntriesForWrite(); + } + try { MergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( theSourcePatientIdentifier, diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index fb707d428835..c80a78e397f4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -162,8 +162,11 @@ public IBaseParameters replaceReferences( IPrimitiveType theBatchSize, RequestDetails theRequestDetails) { validateReplaceReferencesParams(theSourceId, theTargetId); - Integer batchSize = defaultIfNull( + int batchSize = defaultIfNull( IPrimitiveType.toValueOrNull(theBatchSize), myStorageSettings.getMaxTransactionEntriesForWrite()); + if (batchSize > myStorageSettings.getMaxTransactionEntriesForWrite()) { + batchSize = myStorageSettings.getMaxTransactionEntriesForWrite(); + } ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest(new IdDt(theSourceId), new IdDt(theTargetId), batchSize); return getReplaceReferencesSvc().replaceReferences(replaceReferenceRequest, theRequestDetails); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 9b13bf2f7fa6..8037e085669f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -34,6 +34,7 @@ import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.util.ResourceReferenceInfo; import ca.uhn.fhir.util.StopLimitAccumulator; +import com.google.common.collect.Lists; import jakarta.annotation.Nonnull; import jakarta.annotation.PreDestroy; import org.hl7.fhir.instance.model.api.IBaseParameters; @@ -41,19 +42,23 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Meta; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.Type; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.stream.Collectors; import java.util.stream.Stream; import static ca.uhn.fhir.jpa.patch.FhirPatch.OPERATION_REPLACE; @@ -66,6 +71,7 @@ public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { private static final Logger ourLog = LoggerFactory.getLogger(ReplaceReferencesSvcImpl.class); + public static final String RESOURCE_TYPES_SYSTEM = "http://hl7.org/fhir/ValueSet/resource-types"; private final FhirContext myFhirContext; private final DaoRegistry myDaoRegistry; private final HapiTransactionService myHapiTransactionService; @@ -81,11 +87,11 @@ public void preDestroy() { } public ReplaceReferencesSvcImpl( - FhirContext theFhirContext, - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IdHelperService theIdHelperService, - IResourceLinkDao theResourceLinkDao) { + FhirContext theFhirContext, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IdHelperService theIdHelperService, + IResourceLinkDao theResourceLinkDao) { myFhirContext = theFhirContext; myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; @@ -95,7 +101,7 @@ public ReplaceReferencesSvcImpl( @Override public IBaseParameters replaceReferences( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { theReplaceReferenceRequest.validateOrThrowInvalidParameterException(); if (theRequestDetails.isPreferAsync()) { @@ -110,13 +116,13 @@ public Integer countResourcesReferencingResource(IIdType theResourceId, RequestD return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { // FIXME KHS get partition from request JpaPid sourcePid = - myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); + myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); return myResourceLinkDao.countResourcesTargetingPid(sourcePid.getId()); }); } private IBaseParameters replaceReferencesPreferAsync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // FIXME KHS actually start the job Task task = new Task(); task.setStatus(Task.TaskStatus.INPROGRESS); @@ -126,78 +132,119 @@ private IBaseParameters replaceReferencesPreferAsync( Task returnedTask = task.copy(); returnedTask.setIdElement(task.getIdElement().toUnqualifiedVersionless()); returnedTask.getMeta().setVersionId(null); + Parameters retval = new Parameters(); retval.addParameter() - .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) - .setResource(returnedTask); + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) + .setResource(returnedTask); - fakeBackgroundTaskUpdate(task); + // FIXME KHS set partitions from request + fakeBackgroundTaskUpdate(theReplaceReferenceRequest, task, RequestPartitionId.allPartitions()); return retval; } - // FIXME KHS remove this - private void fakeBackgroundTaskUpdate(Task theTask) { + // FIXME KHS replace this with a proper batch job + private void fakeBackgroundTaskUpdate(ReplaceReferenceRequest theReplaceReferenceRequest, Task theTask, RequestPartitionId thePartitionId) { + SystemRequestDetails systemRequestDetails = new SystemRequestDetails(); + systemRequestDetails.setRequestPartitionId(thePartitionId); myFakeExecutor.submit(() -> { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new RuntimeException(e); + try { + + List pidList = myHapiTransactionService + .withSystemRequestOnPartition(thePartitionId) + .execute(() -> + getReferencingResourcePidStream(theReplaceReferenceRequest).collect(Collectors.toUnmodifiableList())); + + List> chunks = Lists.partition(pidList, theReplaceReferenceRequest.batchSize); + List outputBundles = new ArrayList<>(); + + chunks.forEach(chunk -> { + Bundle result = patchReferencingResources(theReplaceReferenceRequest, chunk, systemRequestDetails); + outputBundles.add(result); + }); + + theTask.setStatus(Task.TaskStatus.COMPLETED); + outputBundles.forEach(outputBundle -> { + Task.TaskOutputComponent output = theTask.addOutput(); + Coding coding = output.getType().getCodingFirstRep(); + coding.setSystem(RESOURCE_TYPES_SYSTEM); + coding.setCode("Bundle"); + Reference outputBundleReference = new Reference("#" + outputBundle.getIdElement().toUnqualifiedVersionless()); + output.setValue(outputBundleReference); + theTask.addContained(outputBundle); + }); + + myDaoRegistry.getResourceDao(Task.class).update(theTask, new SystemRequestDetails()); + ourLog.info("Updated task {} to COMPLETED.", theTask.getId()); + } catch (Exception e) { + ourLog.error("Patch failed", e); + theTask.setStatus(Task.TaskStatus.FAILED); + myDaoRegistry.getResourceDao(Task.class).update(theTask, new SystemRequestDetails()); + ourLog.info("Updated task {} to FAILED.", theTask.getId()); + }}); + } - theTask.setStatus(Task.TaskStatus.COMPLETED); - myDaoRegistry.getResourceDao(Task.class).update(theTask, new SystemRequestDetails()); - ourLog.info("Updated task {} to COMPLETED.", theTask.getId()); - }); - } - /** - * Try to perform the operation synchronously. However if there is more than a page of results, fall back to asynchronous operation - */ - @Nonnull - private IBaseParameters replaceReferencesPreferSync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + /** + * Try to perform the operation synchronously. However if there is more than a page of results, fall back to asynchronous operation + */ + @Nonnull + private IBaseParameters replaceReferencesPreferSync ( + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails){ - // TODO KHS get partition from request - StopLimitAccumulator accumulator = myHapiTransactionService + // TODO KHS get partition from request + StopLimitAccumulator accumulator = myHapiTransactionService .withRequest(theRequestDetails) .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); - if (accumulator.isTruncated()) { - ourLog.warn("Too many results. Switching to asynchronous reference replacement."); - return replaceReferencesPreferAsync(theReplaceReferenceRequest, theRequestDetails); - } - - Bundle patchBundle = buildPatchBundle(theReplaceReferenceRequest, theRequestDetails, accumulator); + if (accumulator.isTruncated()) { + ourLog.warn("Too many results. Switching to asynchronous reference replacement."); + return replaceReferencesPreferAsync(theReplaceReferenceRequest, theRequestDetails); + } - IFhirSystemDao systemDao = myDaoRegistry.getSystemDao(); - Bundle result = systemDao.transaction(theRequestDetails, patchBundle); + Bundle result = patchReferencingResources(theReplaceReferenceRequest, accumulator.getItemList(), theRequestDetails); - Parameters retval = new Parameters(); - retval.addParameter() + Parameters retval = new Parameters(); + retval.addParameter() .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME) .setResource(result); - return retval; - } + return retval; + } - private @NotNull StopLimitAccumulator getAllPidsWithLimit( - ReplaceReferenceRequest theReplaceReferenceRequest) { - JpaPid sourcePid = myIdHelperService.getPidOrThrowException( + private Bundle patchReferencingResources (ReplaceReferenceRequest + theReplaceReferenceRequest, List < JpaPid > thePidList, RequestDetails theRequestDetails){ + Bundle patchBundle = buildPatchBundle(theReplaceReferenceRequest, theRequestDetails, thePidList); + IFhirSystemDao systemDao = myDaoRegistry.getSystemDao(); + Bundle result = systemDao.transaction(theRequestDetails, patchBundle); + // TODO KHS shouldn't transaction response bundles have ids? + result.setId(UUID.randomUUID().toString()); + return result; + } + + private @Nonnull StopLimitAccumulator getAllPidsWithLimit ( + ReplaceReferenceRequest theReplaceReferenceRequest){ + Stream pidStream = getReferencingResourcePidStream(theReplaceReferenceRequest); + StopLimitAccumulator accumulator = + StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); + return accumulator; + } + + private @Nonnull Stream getReferencingResourcePidStream (ReplaceReferenceRequest + theReplaceReferenceRequest){ + JpaPid sourcePid = myIdHelperService.getPidOrThrowException( RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); - Stream pidStream = myResourceLinkDao + return myResourceLinkDao .streamSourcePidsForTargetPid(sourcePid.getId()) .map(JpaPid::fromId); - StopLimitAccumulator accumulator = - StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); - return accumulator; - } + } - private Bundle buildPatchBundle( + private Bundle buildPatchBundle ( ReplaceReferenceRequest theReplaceReferenceRequest, - RequestDetails theRequestDetails, - StopLimitAccumulator accumulator) { - BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); + RequestDetails theRequestDetails, List < JpaPid > thePidList){ + BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); - accumulator.getItemList().stream() + thePidList.stream() .map(myIdHelperService::translatePidIdToForcedIdWithCache) .filter(Optional::isPresent) .map(Optional::get) @@ -209,46 +256,46 @@ private Bundle buildPatchBundle( IIdType resourceId = resource.getIdElement(); bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); }); - Bundle patchBundle = bundleBuilder.getBundleTyped(); - return patchBundle; - } + Bundle patchBundle = bundleBuilder.getBundleTyped(); + return patchBundle; + } - private @NotNull Parameters buildPatchParams( - ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { - Parameters params = new Parameters(); + private @Nonnull Parameters buildPatchParams ( + ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource){ + Parameters params = new Parameters(); - myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() + myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() .filter(refInfo -> matches( - refInfo, - theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource + refInfo, + theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource .map(refInfo -> createReplaceReferencePatchOperation( - referencingResource.fhirType() + "." + refInfo.getName(), - new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) + referencingResource.fhirType() + "." + refInfo.getName(), + new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) .forEach(params::addParameter); // Add each operation to parameters - return params; - } + return params; + } - private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { - return refInfo.getResourceReference() + private static boolean matches (ResourceReferenceInfo refInfo, IIdType theSourceId){ + return refInfo.getResourceReference() .getReferenceElement() .toUnqualifiedVersionless() .getValueAsString() .equals(theSourceId.getValueAsString()); - } + } - @Nonnull - private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( - String thePath, Type theValue) { + @Nonnull + private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation ( + String thePath, Type theValue){ - Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); - operation.setName(PARAMETER_OPERATION); - operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE)); - operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath)); - operation.addPart().setName(PARAMETER_VALUE).setValue(theValue); - return operation; - } + Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); + operation.setName(PARAMETER_OPERATION); + operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE)); + operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath)); + operation.addPart().setName(PARAMETER_VALUE).setValue(theValue); + return operation; + } - private IFhirResourceDao getDao(String theResourceName) { - return myDaoRegistry.getResourceDao(theResourceName); + private IFhirResourceDao getDao (String theResourceName){ + return myDaoRegistry.getResourceDao(theResourceName); + } } -} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 35a65e72eb93..4ef0eff604de 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -56,6 +56,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER_RESPOND_ASYNC; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE; @@ -245,7 +246,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa // Assert on the output type Coding taskType = taskOutput.getType().getCodingFirstRep(); - assertEquals("http://hl7.org/fhir/ValueSet/resource-types", taskType.getSystem()); + assertEquals(RESOURCE_TYPES_SYSTEM, taskType.getSystem()); assertEquals("OperationOutcome", taskType.getCode()); List containedResources = taskWithOutput.getContained(); @@ -467,6 +468,8 @@ void testReplaceReferences(boolean isAsync) throws IOException { assertThat(outParams.getParameter()).hasSize(1); + + Bundle patchResultBundle; if (isAsync) { Task task = (Task) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).getResource(); assertNull(task.getIdElement().getVersionIdPart()); @@ -474,31 +477,33 @@ void testReplaceReferences(boolean isAsync) throws IOException { await().until(() -> taskCompleted(task.getIdElement())); Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); + ourLog.info("Complete Task: {}", ourFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); // FIXME KHS the rest of these asserts will likely need to be tweaked Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); // Assert on the output type Coding taskType = taskOutput.getType().getCodingFirstRep(); - assertEquals("http://hl7.org/fhir/ValueSet/resource-types", taskType.getSystem()); - assertEquals("OperationOutcome", taskType.getCode()); + assertEquals(RESOURCE_TYPES_SYSTEM, taskType.getSystem()); + assertEquals("Bundle", taskType.getCode()); List containedResources = taskWithOutput.getContained(); assertThat(containedResources) .hasSize(1) .element(0) - .isInstanceOf(OperationOutcome.class); + .isInstanceOf(Bundle.class); - OperationOutcome containedOutcome = (OperationOutcome) containedResources.get(0); + Bundle containedBundle = (Bundle) containedResources.get(0); Reference outputRef = (Reference) taskOutput.getValue(); - OperationOutcome outcome = (OperationOutcome) outputRef.getResource(); - assertTrue(containedOutcome.equalsDeep(outcome)); + patchResultBundle = (Bundle) outputRef.getResource(); + assertTrue(containedBundle.equalsDeep(patchResultBundle)); + } else { + patchResultBundle = (Bundle) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME).getResource(); } // validate Pattern expectedPatchIssuePattern = Pattern.compile("Successfully patched resource \"(Observation|Encounter|CarePlan)/\\d+/_history/\\d+\"."); - Bundle patchResultBundle = (Bundle) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME).getResource(); assertThat(patchResultBundle.getEntry()).hasSize(23) .allSatisfy(entry -> assertThat(entry.getResponse().getOutcome()) diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java index 29d4438238a7..4e4610012983 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java @@ -20,8 +20,6 @@ public class ReplaceReferenceRequest { @Nonnull public final IIdType targetId; - - @Nonnull public final int batchSize; public ReplaceReferenceRequest(@Nonnull IIdType theSourceId, @Nonnull IIdType theTargetId, int theBatchSize) { From 7999dd28f5b0b372fc4329847681740edfeb3a98 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Sun, 8 Dec 2024 22:44:20 -0500 Subject: [PATCH 049/148] align tests --- .../BaseJpaResourceProviderPatient.java | 1 - .../provider/ReplaceReferencesSvcImpl.java | 234 +++++++++--------- .../jpa/provider/r4/PatientMergeR4Test.java | 138 ++++++----- .../jpa/provider/ReplaceReferenceRequest.java | 1 + 4 files changed, 193 insertions(+), 181 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index bd40e469eb13..149be3fb65f5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -49,7 +49,6 @@ import ca.uhn.fhir.util.CanonicalIdentifier; import ca.uhn.fhir.util.IdentifierUtil; import ca.uhn.fhir.util.ParametersUtil; -import jakarta.annotation.Nonnull; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.hl7.fhir.instance.model.api.IBaseParameters; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 8037e085669f..a21d797bd74c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -87,11 +87,11 @@ public void preDestroy() { } public ReplaceReferencesSvcImpl( - FhirContext theFhirContext, - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IdHelperService theIdHelperService, - IResourceLinkDao theResourceLinkDao) { + FhirContext theFhirContext, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IdHelperService theIdHelperService, + IResourceLinkDao theResourceLinkDao) { myFhirContext = theFhirContext; myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; @@ -101,7 +101,7 @@ public ReplaceReferencesSvcImpl( @Override public IBaseParameters replaceReferences( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { theReplaceReferenceRequest.validateOrThrowInvalidParameterException(); if (theRequestDetails.isPreferAsync()) { @@ -116,13 +116,13 @@ public Integer countResourcesReferencingResource(IIdType theResourceId, RequestD return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { // FIXME KHS get partition from request JpaPid sourcePid = - myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); + myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); return myResourceLinkDao.countResourcesTargetingPid(sourcePid.getId()); }); } private IBaseParameters replaceReferencesPreferAsync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // FIXME KHS actually start the job Task task = new Task(); task.setStatus(Task.TaskStatus.INPROGRESS); @@ -135,8 +135,8 @@ private IBaseParameters replaceReferencesPreferAsync( Parameters retval = new Parameters(); retval.addParameter() - .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) - .setResource(returnedTask); + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) + .setResource(returnedTask); // FIXME KHS set partitions from request fakeBackgroundTaskUpdate(theReplaceReferenceRequest, task, RequestPartitionId.allPartitions()); @@ -144,107 +144,111 @@ private IBaseParameters replaceReferencesPreferAsync( } // FIXME KHS replace this with a proper batch job - private void fakeBackgroundTaskUpdate(ReplaceReferenceRequest theReplaceReferenceRequest, Task theTask, RequestPartitionId thePartitionId) { + private void fakeBackgroundTaskUpdate( + ReplaceReferenceRequest theReplaceReferenceRequest, Task theTask, RequestPartitionId thePartitionId) { SystemRequestDetails systemRequestDetails = new SystemRequestDetails(); systemRequestDetails.setRequestPartitionId(thePartitionId); myFakeExecutor.submit(() -> { - try { + try { - List pidList = myHapiTransactionService + List pidList = myHapiTransactionService .withSystemRequestOnPartition(thePartitionId) - .execute(() -> - getReferencingResourcePidStream(theReplaceReferenceRequest).collect(Collectors.toUnmodifiableList())); - - List> chunks = Lists.partition(pidList, theReplaceReferenceRequest.batchSize); - List outputBundles = new ArrayList<>(); - - chunks.forEach(chunk -> { - Bundle result = patchReferencingResources(theReplaceReferenceRequest, chunk, systemRequestDetails); - outputBundles.add(result); - }); - - theTask.setStatus(Task.TaskStatus.COMPLETED); - outputBundles.forEach(outputBundle -> { - Task.TaskOutputComponent output = theTask.addOutput(); - Coding coding = output.getType().getCodingFirstRep(); - coding.setSystem(RESOURCE_TYPES_SYSTEM); - coding.setCode("Bundle"); - Reference outputBundleReference = new Reference("#" + outputBundle.getIdElement().toUnqualifiedVersionless()); - output.setValue(outputBundleReference); - theTask.addContained(outputBundle); - }); - - myDaoRegistry.getResourceDao(Task.class).update(theTask, new SystemRequestDetails()); - ourLog.info("Updated task {} to COMPLETED.", theTask.getId()); - } catch (Exception e) { - ourLog.error("Patch failed", e); - theTask.setStatus(Task.TaskStatus.FAILED); - myDaoRegistry.getResourceDao(Task.class).update(theTask, new SystemRequestDetails()); - ourLog.info("Updated task {} to FAILED.", theTask.getId()); - }}); + .execute(() -> getReferencingResourcePidStream(theReplaceReferenceRequest) + .collect(Collectors.toUnmodifiableList())); + List> chunks = Lists.partition(pidList, theReplaceReferenceRequest.batchSize); + List outputBundles = new ArrayList<>(); + + chunks.forEach(chunk -> { + Bundle result = patchReferencingResources(theReplaceReferenceRequest, chunk, systemRequestDetails); + outputBundles.add(result); + }); + + theTask.setStatus(Task.TaskStatus.COMPLETED); + outputBundles.forEach(outputBundle -> { + Task.TaskOutputComponent output = theTask.addOutput(); + Coding coding = output.getType().getCodingFirstRep(); + coding.setSystem(RESOURCE_TYPES_SYSTEM); + coding.setCode("Bundle"); + Reference outputBundleReference = + new Reference("#" + outputBundle.getIdElement().toUnqualifiedVersionless()); + output.setValue(outputBundleReference); + theTask.addContained(outputBundle); + }); + + myDaoRegistry.getResourceDao(Task.class).update(theTask, new SystemRequestDetails()); + ourLog.info("Updated task {} to COMPLETED.", theTask.getId()); + } catch (Exception e) { + ourLog.error("Patch failed", e); + theTask.setStatus(Task.TaskStatus.FAILED); + myDaoRegistry.getResourceDao(Task.class).update(theTask, new SystemRequestDetails()); + ourLog.info("Updated task {} to FAILED.", theTask.getId()); } + }); + } - /** - * Try to perform the operation synchronously. However if there is more than a page of results, fall back to asynchronous operation - */ - @Nonnull - private IBaseParameters replaceReferencesPreferSync ( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails){ + /** + * Try to perform the operation synchronously. However if there is more than a page of results, fall back to asynchronous operation + */ + @Nonnull + private IBaseParameters replaceReferencesPreferSync( + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { - // TODO KHS get partition from request - StopLimitAccumulator accumulator = myHapiTransactionService + // TODO KHS get partition from request + StopLimitAccumulator accumulator = myHapiTransactionService .withRequest(theRequestDetails) .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); - if (accumulator.isTruncated()) { - ourLog.warn("Too many results. Switching to asynchronous reference replacement."); - return replaceReferencesPreferAsync(theReplaceReferenceRequest, theRequestDetails); - } + if (accumulator.isTruncated()) { + ourLog.warn("Too many results. Switching to asynchronous reference replacement."); + return replaceReferencesPreferAsync(theReplaceReferenceRequest, theRequestDetails); + } - Bundle result = patchReferencingResources(theReplaceReferenceRequest, accumulator.getItemList(), theRequestDetails); + Bundle result = + patchReferencingResources(theReplaceReferenceRequest, accumulator.getItemList(), theRequestDetails); - Parameters retval = new Parameters(); - retval.addParameter() + Parameters retval = new Parameters(); + retval.addParameter() .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME) .setResource(result); - return retval; - } + return retval; + } - private Bundle patchReferencingResources (ReplaceReferenceRequest - theReplaceReferenceRequest, List < JpaPid > thePidList, RequestDetails theRequestDetails){ - Bundle patchBundle = buildPatchBundle(theReplaceReferenceRequest, theRequestDetails, thePidList); - IFhirSystemDao systemDao = myDaoRegistry.getSystemDao(); - Bundle result = systemDao.transaction(theRequestDetails, patchBundle); - // TODO KHS shouldn't transaction response bundles have ids? - result.setId(UUID.randomUUID().toString()); - return result; - } + private Bundle patchReferencingResources( + ReplaceReferenceRequest theReplaceReferenceRequest, + List thePidList, + RequestDetails theRequestDetails) { + Bundle patchBundle = buildPatchBundle(theReplaceReferenceRequest, theRequestDetails, thePidList); + IFhirSystemDao systemDao = myDaoRegistry.getSystemDao(); + Bundle result = systemDao.transaction(theRequestDetails, patchBundle); + // TODO KHS shouldn't transaction response bundles have ids? + result.setId(UUID.randomUUID().toString()); + return result; + } - private @Nonnull StopLimitAccumulator getAllPidsWithLimit ( - ReplaceReferenceRequest theReplaceReferenceRequest){ - Stream pidStream = getReferencingResourcePidStream(theReplaceReferenceRequest); - StopLimitAccumulator accumulator = + private @Nonnull StopLimitAccumulator getAllPidsWithLimit( + ReplaceReferenceRequest theReplaceReferenceRequest) { + Stream pidStream = getReferencingResourcePidStream(theReplaceReferenceRequest); + StopLimitAccumulator accumulator = StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); - return accumulator; - } + return accumulator; + } - private @Nonnull Stream getReferencingResourcePidStream (ReplaceReferenceRequest - theReplaceReferenceRequest){ - JpaPid sourcePid = myIdHelperService.getPidOrThrowException( + private @Nonnull Stream getReferencingResourcePidStream( + ReplaceReferenceRequest theReplaceReferenceRequest) { + JpaPid sourcePid = myIdHelperService.getPidOrThrowException( RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); - return myResourceLinkDao - .streamSourcePidsForTargetPid(sourcePid.getId()) - .map(JpaPid::fromId); - } + return myResourceLinkDao.streamSourcePidsForTargetPid(sourcePid.getId()).map(JpaPid::fromId); + } - private Bundle buildPatchBundle ( + private Bundle buildPatchBundle( ReplaceReferenceRequest theReplaceReferenceRequest, - RequestDetails theRequestDetails, List < JpaPid > thePidList){ - BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); + RequestDetails theRequestDetails, + List thePidList) { + BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); - thePidList.stream() + thePidList.stream() .map(myIdHelperService::translatePidIdToForcedIdWithCache) .filter(Optional::isPresent) .map(Optional::get) @@ -256,46 +260,46 @@ private Bundle buildPatchBundle ( IIdType resourceId = resource.getIdElement(); bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); }); - Bundle patchBundle = bundleBuilder.getBundleTyped(); - return patchBundle; - } + Bundle patchBundle = bundleBuilder.getBundleTyped(); + return patchBundle; + } - private @Nonnull Parameters buildPatchParams ( - ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource){ - Parameters params = new Parameters(); + private @Nonnull Parameters buildPatchParams( + ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { + Parameters params = new Parameters(); - myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() + myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() .filter(refInfo -> matches( - refInfo, - theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource + refInfo, + theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource .map(refInfo -> createReplaceReferencePatchOperation( - referencingResource.fhirType() + "." + refInfo.getName(), - new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) + referencingResource.fhirType() + "." + refInfo.getName(), + new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) .forEach(params::addParameter); // Add each operation to parameters - return params; - } + return params; + } - private static boolean matches (ResourceReferenceInfo refInfo, IIdType theSourceId){ - return refInfo.getResourceReference() + private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { + return refInfo.getResourceReference() .getReferenceElement() .toUnqualifiedVersionless() .getValueAsString() .equals(theSourceId.getValueAsString()); - } + } - @Nonnull - private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation ( - String thePath, Type theValue){ + @Nonnull + private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( + String thePath, Type theValue) { - Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); - operation.setName(PARAMETER_OPERATION); - operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE)); - operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath)); - operation.addPart().setName(PARAMETER_VALUE).setValue(theValue); - return operation; - } + Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); + operation.setName(PARAMETER_OPERATION); + operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE)); + operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath)); + operation.addPart().setName(PARAMETER_VALUE).setValue(theValue); + return operation; + } - private IFhirResourceDao getDao (String theResourceName){ - return myDaoRegistry.getResourceDao(theResourceName); - } + private IFhirResourceDao getDao(String theResourceName) { + return myDaoRegistry.getResourceDao(theResourceName); } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 4ef0eff604de..71ff9914a4fe 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -231,82 +231,86 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa } assertTrue(input.equalsDeep(inParameters)); - // Assert outcome - OperationOutcome outcome; + // Assert Task if (isAsync) { Task task = (Task) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource(); + assertNull(task.getIdElement().getVersionIdPart()); ourLog.info("Got task {}", task.getId()); await().until(() -> taskCompleted(task.getIdElement())); Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); + ourLog.info("Complete Task: {}", ourFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); + // FIXME KHS the rest of these asserts will likely need to be tweaked Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); // Assert on the output type Coding taskType = taskOutput.getType().getCodingFirstRep(); assertEquals(RESOURCE_TYPES_SYSTEM, taskType.getSystem()); - assertEquals("OperationOutcome", taskType.getCode()); + assertEquals("Bundle", taskType.getCode()); List containedResources = taskWithOutput.getContained(); assertThat(containedResources) .hasSize(1) .element(0) - .isInstanceOf(OperationOutcome.class); + .isInstanceOf(Bundle.class); - OperationOutcome containedOutcome = (OperationOutcome) containedResources.get(0); + Bundle containedBundle = (Bundle) containedResources.get(0); Reference outputRef = (Reference) taskOutput.getValue(); - outcome = (OperationOutcome) outputRef.getResource(); - assertTrue(containedOutcome.equalsDeep(outcome)); - } else { - outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); - } - - if (withPreview) { - assertThat(outcome.getIssue()) - .hasSize(1) - .element(0) - .satisfies(issue -> { - assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - assertThat(issue.getDetails().getText()).isEqualTo("Preview only merge operation - no issues detected"); - assertThat(issue.getDiagnostics()).isEqualTo("Merge would update 25 resources"); - }); - } else { - assertThat(outcome.getIssue()) - .hasSize(1) - .element(0) - .satisfies(issue -> { - assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - assertThat(issue.getDetails().getText()).isEqualTo("Merge operation completed successfully."); - }); - } + Bundle patchResultBundle = (Bundle) outputRef.getResource(); + assertTrue(containedBundle.equalsDeep(patchResultBundle)); + validatePatchResultBundle(patchResultBundle); + } else { // Synchronous case + // Assert outcome + OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); + + if (withPreview) { + assertThat(outcome.getIssue()) + .hasSize(1) + .element(0) + .satisfies(issue -> { + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(issue.getDetails().getText()).isEqualTo("Preview only merge operation - no issues detected"); + assertThat(issue.getDiagnostics()).isEqualTo("Merge would update 25 resources"); + }); + } else { + assertThat(outcome.getIssue()) + .hasSize(1) + .element(0) + .satisfies(issue -> { + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(issue.getDetails().getText()).isEqualTo("Merge operation completed successfully."); + }); + } - // Assert Merged Patient - Patient mergedPatient = (Patient) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_RESULT).getResource(); - List identifiers = mergedPatient.getIdentifier(); - if (withInputResultPatient) { - assertThat(identifiers).hasSize(1); - assertThat(identifiers.get(0).getSystem()).isEqualTo("SYS1A"); - assertThat(identifiers.get(0).getValue()).isEqualTo("VAL1A"); - } else { - assertThat(identifiers).hasSize(5); - assertThat(identifiers) - .extracting(Identifier::getSystem) - .containsExactlyInAnyOrder("SYS1A", "SYS1B", "SYS2A", "SYS2B", "SYSC"); - assertThat(identifiers) - .extracting(Identifier::getValue) - .containsExactlyInAnyOrder("VAL1A", "VAL1B", "VAL2A", "VAL2B", "VALC"); - } - if (!withPreview && !withDelete) { - // assert source has link to target - Patient source = myPatientDao.read(mySourcePatId, mySrd); - assertThat(source.getLink()) - .hasSize(1) - .element(0) - .extracting(link -> link.getOther().getReferenceElement()) - .isEqualTo(myTargetPatId); + // Assert Merged Patient + Patient mergedPatient = (Patient) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_RESULT).getResource(); + List identifiers = mergedPatient.getIdentifier(); + if (withInputResultPatient) { + assertThat(identifiers).hasSize(1); + assertThat(identifiers.get(0).getSystem()).isEqualTo("SYS1A"); + assertThat(identifiers.get(0).getValue()).isEqualTo("VAL1A"); + } else { + assertThat(identifiers).hasSize(5); + assertThat(identifiers) + .extracting(Identifier::getSystem) + .containsExactlyInAnyOrder("SYS1A", "SYS1B", "SYS2A", "SYS2B", "SYSC"); + assertThat(identifiers) + .extracting(Identifier::getValue) + .containsExactlyInAnyOrder("VAL1A", "VAL1B", "VAL2A", "VAL2B", "VALC"); + } + if (!withPreview && !withDelete) { + // assert source has link to target + Patient source = myPatientDao.read(mySourcePatId, mySrd); + assertThat(source.getLink()) + .hasSize(1) + .element(0) + .extracting(link -> link.getOther().getReferenceElement()) + .isEqualTo(myTargetPatId); + } } // Check that the linked resources were updated @@ -503,18 +507,7 @@ void testReplaceReferences(boolean isAsync) throws IOException { } // validate - Pattern expectedPatchIssuePattern = Pattern.compile("Successfully patched resource \"(Observation|Encounter|CarePlan)/\\d+/_history/\\d+\"."); - assertThat(patchResultBundle.getEntry()).hasSize(23) - .allSatisfy(entry -> - assertThat(entry.getResponse().getOutcome()) - .isInstanceOf(OperationOutcome.class) - .extracting(OperationOutcome.class::cast) - .extracting(OperationOutcome::getIssue) - .satisfies(issues -> - assertThat(issues).hasSize(1) - .element(0) - .extracting(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics) - .satisfies(diagnostics -> assertThat(diagnostics).matches(expectedPatchIssuePattern)))); + validatePatchResultBundle(patchResultBundle); // Check that the linked resources were updated @@ -538,6 +531,21 @@ void testReplaceReferences(boolean isAsync) throws IOException { assertThat(actual).contains(myTargetEnc1); } + private static void validatePatchResultBundle(Bundle patchResultBundle) { + Pattern expectedPatchIssuePattern = Pattern.compile("Successfully patched resource \"(Observation|Encounter|CarePlan)/\\d+/_history/\\d+\"."); + assertThat(patchResultBundle.getEntry()).hasSize(23) + .allSatisfy(entry -> + assertThat(entry.getResponse().getOutcome()) + .isInstanceOf(OperationOutcome.class) + .extracting(OperationOutcome.class::cast) + .extracting(OperationOutcome::getIssue) + .satisfies(issues -> + assertThat(issues).hasSize(1) + .element(0) + .extracting(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics) + .satisfies(diagnostics -> assertThat(diagnostics).matches(expectedPatchIssuePattern)))); + } + // FIXME KHS look at PatientEverythingR4Test for ideas for other tests private Bundle fetchBundle(String theUrl) throws IOException { diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java index 4e4610012983..0140b5d3366e 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java @@ -20,6 +20,7 @@ public class ReplaceReferenceRequest { @Nonnull public final IIdType targetId; + public final int batchSize; public ReplaceReferenceRequest(@Nonnull IIdType theSourceId, @Nonnull IIdType theTargetId, int theBatchSize) { From eee16131edfc8ef1865a7135ebea547d0608797b Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 9 Dec 2024 10:27:24 -0500 Subject: [PATCH 050/148] test small batch size --- .../fhir/jpa/provider/JpaSystemProvider.java | 2 +- .../jpa/provider/r4/PatientMergeR4Test.java | 86 +++++++++++++++++-- .../server/provider/ProviderConstants.java | 2 +- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index c80a78e397f4..81af890b37a5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -158,7 +158,7 @@ public IBaseParameters replaceReferences( String theSourceId, @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID) String theTargetId, - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PAGE_SIZE, typeName = "unsignedInt") + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_BATCH_SIZE, typeName = "unsignedInt") IPrimitiveType theBatchSize, RequestDetails theRequestDetails) { validateReplaceReferencesParams(theSourceId, theTargetId); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 71ff9914a4fe..ae171a36b799 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -26,6 +26,7 @@ import org.hl7.fhir.r4.model.Encounter.EncounterStatus; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.IntegerType; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Observation.ObservationStatus; import org.hl7.fhir.r4.model.OperationOutcome; @@ -85,6 +86,10 @@ public class PatientMergeR4Test extends BaseResourceProviderR4Test { static final Identifier pat2IdentifierA = new Identifier().setSystem("SYS2A").setValue("VAL2A"); static final Identifier pat2IdentifierB = new Identifier().setSystem("SYS2B").setValue("VAL2B"); static final Identifier patBothIdentifierC = new Identifier().setSystem("SYSC").setValue("VALC"); + static final int TOTAL_EXPECTED_PATCHES = 23; + static final int SMALL_BATCH_SIZE = 5; + static final int EXPECTED_SMALL_BATCHES = (TOTAL_EXPECTED_PATCHES + SMALL_BATCH_SIZE - 1) / SMALL_BATCH_SIZE; + IIdType myOrgId; IIdType mySourcePatId; @@ -232,7 +237,6 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa assertTrue(input.equalsDeep(inParameters)); - // Assert Task if (isAsync) { Task task = (Task) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource(); @@ -262,7 +266,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa Reference outputRef = (Reference) taskOutput.getValue(); Bundle patchResultBundle = (Bundle) outputRef.getResource(); assertTrue(containedBundle.equalsDeep(patchResultBundle)); - validatePatchResultBundle(patchResultBundle); + validatePatchResultBundle(patchResultBundle, TOTAL_EXPECTED_PATCHES); } else { // Synchronous case // Assert outcome OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); @@ -472,7 +476,6 @@ void testReplaceReferences(boolean isAsync) throws IOException { assertThat(outParams.getParameter()).hasSize(1); - Bundle patchResultBundle; if (isAsync) { Task task = (Task) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).getResource(); @@ -483,7 +486,6 @@ void testReplaceReferences(boolean isAsync) throws IOException { Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); ourLog.info("Complete Task: {}", ourFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); - // FIXME KHS the rest of these asserts will likely need to be tweaked Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); // Assert on the output type @@ -507,10 +509,80 @@ void testReplaceReferences(boolean isAsync) throws IOException { } // validate - validatePatchResultBundle(patchResultBundle); + validatePatchResultBundle(patchResultBundle, TOTAL_EXPECTED_PATCHES); + + // Check that the linked resources were updated + + validateLinksUsingEverything(); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { + // exec + IOperationUntypedWithInput request = myClient.operation() + .onServer() + .named(OPERATION_REPLACE_REFERENCES) + .withParameter(Parameters.class, ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID, new StringType(mySourcePatId.getValue())) + .andParameter(ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID, new StringType(myTargetPatId.getValue())) + .andParameter(ProviderConstants.OPERATION_REPLACE_REFERENCES_BATCH_SIZE, new IntegerType(SMALL_BATCH_SIZE)); + + if (isAsync) { + request.withAdditionalHeader(HEADER_PREFER, HEADER_PREFER_RESPOND_ASYNC); + } + + Parameters outParams = request + .returnResourceType(Parameters.class) + .execute(); + + assertThat(outParams.getParameter()).hasSize(1); + + Bundle patchResultBundle; + Task task = (Task) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).getResource(); + assertNull(task.getIdElement().getVersionIdPart()); + ourLog.info("Got task {}", task.getId()); + await().until(() -> taskCompleted(task.getIdElement())); + + Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); + ourLog.info("Complete Task: {}", ourFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); + + assertThat(taskWithOutput.getOutput()).hasSize(EXPECTED_SMALL_BATCHES); + List containedResources = taskWithOutput.getContained(); + + assertThat(containedResources) + .hasSize(EXPECTED_SMALL_BATCHES) + .element(0) + .isInstanceOf(Bundle.class); + + int entriesLeft = TOTAL_EXPECTED_PATCHES; + for (int i = 1; i < EXPECTED_SMALL_BATCHES; i++) { + + Task.TaskOutputComponent taskOutput = taskWithOutput.getOutput().get(i); + + // Assert on the output type + Coding taskType = taskOutput.getType().getCodingFirstRep(); + assertEquals(RESOURCE_TYPES_SYSTEM, taskType.getSystem()); + assertEquals("Bundle", taskType.getCode()); + + Bundle containedBundle = (Bundle) containedResources.get(i); + + Reference outputRef = (Reference) taskOutput.getValue(); + patchResultBundle = (Bundle) outputRef.getResource(); + assertTrue(containedBundle.equalsDeep(patchResultBundle)); + + // validate + entriesLeft -= SMALL_BATCH_SIZE; + int expectedNumberOfEntries = entriesLeft > SMALL_BATCH_SIZE ? SMALL_BATCH_SIZE : entriesLeft; + validatePatchResultBundle(patchResultBundle, expectedNumberOfEntries); + } + // Check that the linked resources were updated + validateLinksUsingEverything(); + } + + private void validateLinksUsingEverything() throws IOException { Bundle everythingBundle = fetchBundle(myServerBase + "/" + myTargetPatId + "/$everything?_format=json&_count=100"); assertNull(everythingBundle.getLink("next")); @@ -531,9 +603,9 @@ void testReplaceReferences(boolean isAsync) throws IOException { assertThat(actual).contains(myTargetEnc1); } - private static void validatePatchResultBundle(Bundle patchResultBundle) { + private static void validatePatchResultBundle(Bundle patchResultBundle, int theTotalExpectedPatches) { Pattern expectedPatchIssuePattern = Pattern.compile("Successfully patched resource \"(Observation|Encounter|CarePlan)/\\d+/_history/\\d+\"."); - assertThat(patchResultBundle.getEntry()).hasSize(23) + assertThat(patchResultBundle.getEntry()).hasSize(theTotalExpectedPatches) .allSatisfy(entry -> assertThat(entry.getResponse().getOutcome()) .isInstanceOf(OperationOutcome.class) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index dd84fee17585..c99eb22de950 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -264,7 +264,7 @@ public class ProviderConstants { * The number of resources that will be modified at a time. If the number of resources that need to change * exceeds this amount, the operation will switch to async mode. */ - public static final String OPERATION_REPLACE_REFERENCES_PAGE_SIZE = "page-size"; + public static final String OPERATION_REPLACE_REFERENCES_BATCH_SIZE = "batch-size"; /** * $replace-references output Parameters names From 6a481e951964fa9308ae83d90664214149aa4e90 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Mon, 9 Dec 2024 12:15:34 -0500 Subject: [PATCH 051/148] do src and target updates in trx, add validation src is not already replaced --- .../BaseJpaResourceProviderPatient.java | 7 +- .../jpa/dao/merge/ResourceMergeService.java | 116 +++++----- .../dao/merge/ResourceMergeServiceTest.java | 200 +++++++++++------- 3 files changed, 199 insertions(+), 124 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 149be3fb65f5..161cf1818617 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.jpa.dao.merge.MergeOperationOutcome; import ca.uhn.fhir.jpa.dao.merge.PatientMergeOperationInputParameters; import ca.uhn.fhir.jpa.dao.merge.ResourceMergeService; +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.primitive.IdDt; @@ -73,6 +74,9 @@ public abstract class BaseJpaResourceProviderPatient ex @Autowired private IReplaceReferencesSvc myReplaceReferencesSvc; + @Autowired + private IHapiTransactionService myHapiTransactionService; + /** * Patient/123/$everything */ @@ -309,7 +313,8 @@ public IBaseParameters patientMerge( batchSize); IFhirResourceDaoPatient dao = (IFhirResourceDaoPatient) getDao(); - ResourceMergeService resourceMergeService = new ResourceMergeService(dao, myReplaceReferencesSvc); + ResourceMergeService resourceMergeService = + new ResourceMergeService(dao, myReplaceReferencesSvc, myHapiTransactionService); FhirContext fhirContext = dao.getContext(); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index dc783bff85f8..205032e52eee 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; import ca.uhn.fhir.jpa.provider.ReplaceReferenceRequest; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; @@ -60,13 +61,17 @@ public class ResourceMergeService { private final IFhirResourceDaoPatient myDao; private final IReplaceReferencesSvc myReplaceReferencesSvc; + private final IHapiTransactionService myHapiTransactionService; private final FhirContext myFhirContext; public ResourceMergeService( - IFhirResourceDaoPatient thePatientDao, IReplaceReferencesSvc theReplaceReferencesSvc) { + IFhirResourceDaoPatient thePatientDao, + IReplaceReferencesSvc theReplaceReferencesSvc, + IHapiTransactionService theHapiTransactionService) { myDao = thePatientDao; myReplaceReferencesSvc = theReplaceReferencesSvc; myFhirContext = myDao.getContext(); + myHapiTransactionService = theHapiTransactionService; } /** @@ -86,7 +91,7 @@ public MergeOperationOutcome merge( mergeOutcome.setHttpStatusCode(STATUS_HTTP_200_OK); try { - doMerge(theMergeOperationParameters, theRequestDetails, mergeOutcome); + validateAndMerge(theMergeOperationParameters, theRequestDetails, mergeOutcome); } catch (Exception e) { ourLog.error("Resource merge failed", e); if (e instanceof BaseServerResponseException) { @@ -99,7 +104,7 @@ public MergeOperationOutcome merge( return mergeOutcome; } - private void doMerge( + private void validateAndMerge( MergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails, MergeOperationOutcome theMergeOutcome) { @@ -129,7 +134,7 @@ private void doMerge( return; } - if (!validateSourceAndTargetAreMergable(sourceResource, targetResource, operationOutcome)) { + if (!validateSourceAndTargetAreSuitableForMerge(sourceResource, targetResource, operationOutcome)) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); return; } @@ -140,47 +145,64 @@ private void doMerge( return; } - Patient resultResource = (Patient) theMergeOperationParameters.getResultResource(); if (theMergeOperationParameters.getPreview()) { Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( sourceResource.getIdElement(), theRequestDetails); // in preview mode, we should also return how the target would look like + Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); Patient targetPatientAsIfUpdated = prepareTargetPatientForUpdate( - targetResource, sourceResource, resultResource, theMergeOperationParameters.getDeleteSource()); + targetResource, sourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); // adding +2 because the source and the target resources themselved would be updated as well - // TODO: but what if target resource is already referencing source resource as a link? how do we handle - // that case? String diagnosticsMsg = String.format("Merge would update %d resources", referencingResourceCount + 2); String detailsText = "Preview only merge operation - no issues detected"; addInfoToOperationOutcome(operationOutcome, diagnosticsMsg, detailsText); return; } + mergeInTransaction( + theMergeOperationParameters, sourceResource, targetResource, theRequestDetails, theMergeOutcome); + + String detailsText = "Merge operation completed successfully."; + addInfoToOperationOutcome(operationOutcome, null, detailsText); + } + + private void mergeInTransaction( + MergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { + + // TODO: cannot do this in transaction yet, because systemDAO.transaction called by replaceReferences complains + // that there is an active transaction already. ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest( - sourceResource.getIdElement(), - targetResource.getIdElement(), + theSourceResource.getIdElement(), + theTargetResource.getIdElement(), theMergeOperationParameters.getBatchSize()); // FIXME KHS check if it needs to go async myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); - Patient patientToUpdate = prepareTargetPatientForUpdate( - targetResource, sourceResource, resultResource, theMergeOperationParameters.getDeleteSource()); - // update the target patient resource after the references are updated - Patient targetPatientAfterUpdate = updateResource(patientToUpdate, theRequestDetails); - theMergeOutcome.setUpdatedTargetResource(targetPatientAfterUpdate); - - if (theMergeOperationParameters.getDeleteSource()) { - deleteResource(sourceResource, theRequestDetails); - } else { - prepareSourceResourceForUpdate(sourceResource, targetResource); - updateResource(sourceResource, theRequestDetails); - } - - String detailsText = "Merge operation completed successfully."; - addInfoToOperationOutcome(operationOutcome, null, detailsText); + myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { + Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); + Patient patientToUpdate = prepareTargetPatientForUpdate( + theTargetResource, + theSourceResource, + theResultResource, + theMergeOperationParameters.getDeleteSource()); + // update the target patient resource after the references are updated + Patient targetPatientAfterUpdate = updateResource(patientToUpdate, theRequestDetails); + theMergeOutcome.setUpdatedTargetResource(targetPatientAfterUpdate); + + if (theMergeOperationParameters.getDeleteSource()) { + deleteResource(theSourceResource, theRequestDetails); + } else { + prepareSourceResourceForUpdate(theSourceResource, theTargetResource); + updateResource(theSourceResource, theRequestDetails); + } + }); } private boolean validateResultResourceIfExists( @@ -261,25 +283,6 @@ private List getLinksToResource( .collect(Collectors.toList()); } - private boolean validateResultResourceDoesNotHaveReplacesLinkToSourceResource( - Patient theResultResource, - Patient theResolvedSourceResource, - String theResultResourceParameterName, - IBaseOperationOutcome theOperationOutcome) { - - List replacesLinkToSourceResource = getLinksToResource( - theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); - - if (!replacesLinkToSourceResource.isEmpty()) { - String msg = String.format( - "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); - addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); - return false; - } - - return true; - } - private boolean validateResultResourceReplacesLinkToSourceResource( Patient theResultResource, Patient theResolvedSourceResource, @@ -331,7 +334,7 @@ protected List getLinksOfType(Patient theResource, Patient.LinkType t return links; } - private boolean validateSourceAndTargetAreMergable( + private boolean validateSourceAndTargetAreSuitableForMerge( Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { if (theSourceResource.getId().equalsIgnoreCase(theTargetResource.getId())) { @@ -347,15 +350,28 @@ private boolean validateSourceAndTargetAreMergable( return false; } - List replacedByLinks = getLinksOfType(theTargetResource, Patient.LinkType.REPLACEDBY); - if (!replacedByLinks.isEmpty()) { - String msg = "Target resource was previously replaced by another resource, it is not a suitable target " - + "for merging."; + List replacedByLinksInTarget = getLinksOfType(theTargetResource, Patient.LinkType.REPLACEDBY); + if (!replacedByLinksInTarget.isEmpty()) { + String ref = replacedByLinksInTarget.get(0).getReference(); + String msg = String.format( + "Target resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable target for merging.", + ref); + addErrorToOperationOutcome(outcome, msg, "invalid"); + return false; + } + + List replacedByLinksInSource = getLinksOfType(theSourceResource, Patient.LinkType.REPLACEDBY); + if (!replacedByLinksInSource.isEmpty()) { + String ref = replacedByLinksInSource.get(0).getReference(); + String msg = String.format( + "Source resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable source for merging.", + ref); addErrorToOperationOutcome(outcome, msg, "invalid"); return false; } - // how about the source patient? should we check it active status and whether it was merged previously as well? return true; } diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index ea3a5fa1ad3b..ee1e7facae0e 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -3,6 +3,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -11,10 +12,8 @@ import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.CanonicalIdentifier; -import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; @@ -32,6 +31,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -52,7 +53,11 @@ public class ResourceMergeServiceTest { "Target resource must be provided either by 'target-patient' or by 'target-patient-identifier', not both."; private static final String SUCCESSFUL_MERGE_MSG = "Merge operation completed successfully"; private static final String SOURCE_PATIENT_TEST_ID = "Patient/123"; + private static final String SOURCE_PATIENT_TEST_ID_WITH_VERSION_1= SOURCE_PATIENT_TEST_ID + "/_history/1"; + private static final String SOURCE_PATIENT_TEST_ID_WITH_VERSION_2= SOURCE_PATIENT_TEST_ID + "/_history/2"; private static final String TARGET_PATIENT_TEST_ID = "Patient/456"; + private static final String TARGET_PATIENT_TEST_ID_WITH_VERSION_1 = TARGET_PATIENT_TEST_ID + "/_history/1"; + private static final String TARGET_PATIENT_TEST_ID_WITH_VERSION_2 = TARGET_PATIENT_TEST_ID + "/_history/2"; @Mock private IFhirResourceDaoPatient myDaoMock; @@ -63,6 +68,9 @@ public class ResourceMergeServiceTest { @Mock RequestDetails myRequestDetailsMock; + @Mock + IHapiTransactionService myTransactionServiceMock; + private ResourceMergeService myResourceMergeService; private final FhirContext myFhirContext = FhirContext.forR4Cached(); @@ -70,7 +78,7 @@ public class ResourceMergeServiceTest { @BeforeEach void setup() { when(myDaoMock.getContext()).thenReturn(myFhirContext); - myResourceMergeService = new ResourceMergeService(myDaoMock, myReplaceReferencesSvcMock); + myResourceMergeService = new ResourceMergeService(myDaoMock, myReplaceReferencesSvcMock, myTransactionServiceMock); } // SUCCESS CASES @@ -84,10 +92,10 @@ void testMerge_WithoutResultResource_Success() { Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); - setupDaoMockForSuccessfulSourcePatientUpdate(sourcePatient, new Patient()); Patient patientReturnedFromDaoAfterTargetUpdate = new Patient(); setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientReturnedFromDaoAfterTargetUpdate, true); + setupTransactionServiceMock(); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -122,7 +130,7 @@ void testMerge_WithResultResource_Success() { setupDaoMockForSuccessfulSourcePatientUpdate(sourcePatient, new Patient()); Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); setupDaoMockForSuccessfulTargetPatientUpdate(resultPatient, patientToBeReturnedFromDaoAfterTargetUpdate, true); - + setupTransactionServiceMock(); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -153,6 +161,7 @@ void testMerge_WithDeleteSourceTrue_Success() { when(myDaoMock.delete(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenReturn(new DaoMethodOutcome()); Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientToBeReturnedFromDaoAfterTargetUpdate, false); + setupTransactionServiceMock(); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -204,14 +213,14 @@ void testMerge_WithPreviewTrue_Success() { void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersionAreTheSame_Success() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123/_history/2")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345/_history/2")); - Patient sourcePatient = createPatient("Patient/123/_history/2"); - Patient targetPatient = createPatient("Patient/345/_history/2"); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID_WITH_VERSION_2)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID_WITH_VERSION_2)); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_2); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_2); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); when(myDaoMock.update(any(), eq(myRequestDetailsMock))).thenReturn(new DaoMethodOutcome()); - + setupTransactionServiceMock(); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -233,8 +242,8 @@ void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersio void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheException() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); ForbiddenOperationException ex = new ForbiddenOperationException("this is the exception message"); when(myDaoMock.read(any(), eq(myRequestDetailsMock))).thenThrow(ex); @@ -258,8 +267,8 @@ void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheExcepti void testMerge_UnhandledExceptionThrown_Uses500StatusCode() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); RuntimeException ex = new RuntimeException("this is the exception message"); when(myDaoMock.read(any(), eq(myRequestDetailsMock))).thenThrow(ex); @@ -283,7 +292,7 @@ void testMerge_UnhandledExceptionThrown_Uses500StatusCode() { void testMerge_ValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorWith400Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setTargetResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -306,7 +315,7 @@ void testMerge_ValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorW void testMerge_ValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorWith400Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -354,9 +363,9 @@ void testMerge_ValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ void testMerge_ValidatesInputParameters_BothSourceResourceAndSourceIdentifierParamsProvided_ReturnsErrorWith400Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); - mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -380,9 +389,9 @@ void testMerge_ValidatesInputParameters_BothSourceResourceAndSourceIdentifierPar void testMerge_ValidatesInputParameters_BothTargetResourceAndTargetIdentifiersParamsProvided_ReturnsErrorWith400Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setTargetResource(new Reference("Patient/123")); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); - mergeOperationParameters.setSourceResource(new Reference("Patient/345")); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -455,9 +464,9 @@ void testMerge_ValidatesInputParameters_TargetResourceParamHasNoReferenceElement void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWith422Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345")); - when(myDaoMock.read(new IdType("Patient/123"), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + when(myDaoMock.read(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -479,11 +488,11 @@ void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWi void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWith422Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345")); - Patient sourcePatient = createPatient("Patient/123"); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); setupDaoMockForSuccessfulRead(sourcePatient); - when(myDaoMock.read(new IdType("Patient/345"), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); + when(myDaoMock.read(new IdType(TARGET_PATIENT_TEST_ID), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -508,7 +517,7 @@ void testMerge_ResolvesSourceResourceByIdentifiers_NoMatchFound_ReturnsErrorWith mergeOperationParameters.setSourceResourceIdentifiers(List.of( new CanonicalIdentifier().setSystem("sys").setValue("val1"), new CanonicalIdentifier().setSystem("sys").setValue("val2"))); - mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); setupDaoMockSearchForIdentifiers(List.of(Collections.emptyList())); // When @@ -566,12 +575,12 @@ void testMerge_ResolvesSourceResourceByIdentifiers_MultipleMatchesFound_ReturnsE void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith422Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of( new CanonicalIdentifier().setSystem("sys").setValue("val1"), new CanonicalIdentifier().setSystem("sys").setValue("val2"))); setupDaoMockSearchForIdentifiers(List.of(Collections.emptyList())); - Patient sourcePatient = createPatient("Patient/123"); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); setupDaoMockForSuccessfulRead(sourcePatient); // When @@ -630,9 +639,10 @@ void testMerge_ResolvesTargetResourceByIdentifiers_MultipleMatchesFound_ReturnsE void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123/_history/1")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345")); - Patient sourcePatient = createPatient("Patient/123/_history/2"); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + //make resolved patient has a more recent version than the one specified in the reference + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_2); setupDaoMockForSuccessfulRead(sourcePatient); // When @@ -655,10 +665,11 @@ void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVe void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345/_history/1")); - Patient sourcePatient = createPatient("Patient/123"); - Patient targetPatient = createPatient("Patient/345/_history/2"); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID_WITH_VERSION_1)); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + // make resolved target patient has a more recent version than the one specified in the reference + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_2); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); @@ -689,8 +700,8 @@ void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status() MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val2"))); - Patient sourcePatient = createPatient("Patient/123"); - Patient targetPatient = createPatient("Patient/123"); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + Patient targetPatient = createPatient(SOURCE_PATIENT_TEST_ID); setupDaoMockSearchForIdentifiers(List.of(List.of(sourcePatient), List.of(targetPatient))); // When @@ -715,10 +726,10 @@ void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status() void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345")); - Patient sourcePatient = createPatient("Patient/123"); - Patient targetPatient = createPatient("Patient/345"); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); targetPatient.setActive(false); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); @@ -742,11 +753,11 @@ void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status() { void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsErrorWith422Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345")); - Patient sourcePatient = createPatient("Patient/123"); - Patient targetPatient = createPatient("Patient/345"); - addReplacedByLink(targetPatient, "Patient/678"); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + addReplacedByLink(targetPatient, "Patient/replacing-res-id"); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); @@ -760,7 +771,37 @@ void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsError assertThat(operationOutcome.getIssue()).hasSize(1); OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); - assertThat(issue.getDiagnostics()).contains("Target resource was previously replaced by another resource, it is not a suitable target for merging."); + assertThat(issue.getDiagnostics()).contains("Target resource was previously replaced by a resource with " + + "reference 'Patient/replacing-res-id', it is " + + "not a suitable target for merging."); + + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_SourceResourceWasPreviouslyReplacedByAnotherResource_ReturnsErrorWith422Status() { + // Given + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + addReplacedByLink(sourcePatient, "Patient/replacing-res-id"); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + // When + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(422); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("Source resource was previously replaced by a resource with " + + "reference 'Patient/replacing-res-id', it is not a suitable source for merging."); verifyNoMoreInteractions(myDaoMock); } @@ -769,14 +810,14 @@ void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsError void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetResource_ReturnsErrorWith400Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345")); - Patient resultPatient = createPatient("Patient/678"); - addReplacesLink(resultPatient, "Patient/123"); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + Patient resultPatient = createPatient("Patient/not-the-target-id"); + addReplacesLink(resultPatient, SOURCE_PATIENT_TEST_ID); mergeOperationParameters.setResultResource(resultPatient); - Patient sourcePatient = createPatient("Patient/123/_history/1"); - Patient targetPatient = createPatient("Patient/345/_history/1"); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_1); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); @@ -790,7 +831,9 @@ void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetRes assertThat(operationOutcome.getIssue()).hasSize(1); OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); - assertThat(issue.getDiagnostics()).contains("'result-patient' must have the same versionless id as the actual resolved target resource. The actual resolved target resource's id is: 'Patient/345'"); + assertThat(issue.getDiagnostics()).contains("'result-patient' must have the same versionless id " + + "as the actual" + + " resolved target resource. The actual resolved target resource's id is: '" + TARGET_PATIENT_TEST_ID +"'"); verifyNoMoreInteractions(myDaoMock); } @@ -800,19 +843,19 @@ void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetRes void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersProvidedInTargetIdentifiers_ReturnsErrorWith400Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of( new CanonicalIdentifier().setSystem("sys").setValue("val1"), new CanonicalIdentifier().setSystem("sys").setValue("val2") )); // the result patient has only one of the identifiers that were provided in the target identifiers - Patient resultPatient = createPatient("Patient/345"); + Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); resultPatient.addIdentifier().setSystem("sys").setValue("val"); - addReplacesLink(resultPatient, "Patient/123"); + addReplacesLink(resultPatient, SOURCE_PATIENT_TEST_ID); mergeOperationParameters.setResultResource(resultPatient); - Patient sourcePatient = createPatient("Patient/123/_history/1"); - Patient targetPatient = createPatient("Patient/345/_history/1"); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_1); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockSearchForIdentifiers(List.of(List.of(targetPatient))); @@ -836,13 +879,13 @@ void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersPr void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLink_ReturnsErrorWith400Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); - Patient resultPatient = createPatient("Patient/345"); + Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); mergeOperationParameters.setResultResource(resultPatient); - Patient sourcePatient = createPatient("Patient/123/_history/1"); - Patient targetPatient = createPatient("Patient/345/_history/1"); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_1); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); @@ -897,17 +940,17 @@ void testMerge_ValidatesResultResource_ResultResourceHasReplacesLinkAndDeleteSou void testMerge_ValidatesResultResource_ResultResourceHasRedundantReplacesLinksToSource_ReturnsErrorWith400Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); - mergeOperationParameters.setSourceResource(new Reference("Patient/123")); - mergeOperationParameters.setTargetResource(new Reference("Patient/345")); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); - Patient resultPatient = createPatient("Patient/345"); + Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); //add the link twice - addReplacesLink(resultPatient, "Patient/123"); - addReplacesLink(resultPatient, "Patient/123"); + addReplacesLink(resultPatient, SOURCE_PATIENT_TEST_ID); + addReplacesLink(resultPatient, SOURCE_PATIENT_TEST_ID); mergeOperationParameters.setResultResource(resultPatient); - Patient sourcePatient = createPatient("Patient/123/_history/1"); - Patient targetPatient = createPatient("Patient/345/_history/1"); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_1); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); @@ -940,6 +983,17 @@ private void addReplacesLink(Patient patient, String theReplacedResourceId) { patient.addLink().setType(Patient.LinkType.REPLACES).setOther(new Reference(theReplacedResourceId)); } + private void setupTransactionServiceMock() { + IHapiTransactionService.IExecutionBuilder executionBuilderMock = + mock(IHapiTransactionService.IExecutionBuilder.class); + when(myTransactionServiceMock.withRequest(myRequestDetailsMock)).thenReturn(executionBuilderMock); + doAnswer(invocation -> { + Runnable runnable = invocation.getArgument(0); + runnable.run(); + return null; + }).when(executionBuilderMock).execute(isA(Runnable.class)); + } + private void setupDaoMockForSuccessfulRead(Patient resource) { assertThat(resource.getIdElement()).isNotNull(); //dao reads the versionless id From dbb17de28adaa0c255f8c92cbe073ec5ddde4593 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 9 Dec 2024 14:19:22 -0500 Subject: [PATCH 052/148] start building batch 2 improve dao --- .../fhir/jpa/dao/data/IResourceLinkDao.java | 10 ++-- .../provider/ReplaceReferencesSvcImpl.java | 47 ++++++----------- .../batch2/jobs/config/Batch2JobsConfig.java | 2 + .../ReplaceReferenceResults.java | 6 +++ .../ReplaceReferenceUpdateStep.java | 17 +++++++ .../ReplaceReferenceUpdateTaskStep.java | 17 +++++++ .../ReplaceReferencesAppCtx.java | 50 +++++++++++++++++++ .../ReplaceReferencesJobParameters.java | 7 +++ .../ReplaceReferencesQueryIdsStep.java | 18 +++++++ 9 files changed, 138 insertions(+), 36 deletions(-) create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResults.java create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java index a696409aef06..6aeb93206ec4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java @@ -20,6 +20,8 @@ package ca.uhn.fhir.jpa.dao.data; import ca.uhn.fhir.jpa.model.entity.ResourceLink; +import ca.uhn.fhir.model.primitive.IdDt; +import org.hl7.fhir.instance.model.api.IIdType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -47,9 +49,9 @@ public interface IResourceLinkDao extends JpaRepository, IHa @Query("SELECT t FROM ResourceLink t LEFT JOIN FETCH t.myTargetResource tr WHERE t.myId in :pids") List findByPidAndFetchTargetDetails(@Param("pids") List thePids); - @Query("SELECT DISTINCT t.mySourceResourcePid FROM ResourceLink t WHERE t.myTargetResourcePid = :resId") - Stream streamSourcePidsForTargetPid(@Param("resId") Long theTargetPid); + @Query("SELECT DISTINCT new ca.uhn.fhir.model.primitive.IdDt(t.mySourceResourceType, t.mySourceResource.myFhirId) FROM ResourceLink t WHERE t.myTargetResourceType = :resourceType AND t.myTargetResource.myFhirId = :resourceFhirId") + Stream streamSourceIdsForTargetPid(@Param("resourceType") String theTargetResourceType, @Param("resourceFhirId") String theTargetResourceFhirId); - @Query("SELECT COUNT(DISTINCT t.mySourceResourcePid) FROM ResourceLink t WHERE t.myTargetResourcePid = :resId") - Integer countResourcesTargetingPid(@Param("resId") Long theTargetPid); + @Query("SELECT COUNT(DISTINCT t.mySourceResourcePid) FROM ResourceLink t WHERE t.myTargetResourceType = :resourceType AND t.myTargetResource.myFhirId = :resourceFhirId") + Integer countResourcesTargetingFhirTypeAndId(@Param("resourceType") String theTargetResourceType, @Param("resourceFhirId") String theTargetResourceFhirId); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index a21d797bd74c..1bd2901fed6d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -75,7 +75,6 @@ public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { private final FhirContext myFhirContext; private final DaoRegistry myDaoRegistry; private final HapiTransactionService myHapiTransactionService; - private final IdHelperService myIdHelperService; private final IResourceLinkDao myResourceLinkDao; // FIXME remove private final ExecutorService myFakeExecutor = Executors.newSingleThreadExecutor(); @@ -95,7 +94,6 @@ public ReplaceReferencesSvcImpl( myFhirContext = theFhirContext; myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; - myIdHelperService = theIdHelperService; myResourceLinkDao = theResourceLinkDao; } @@ -114,10 +112,7 @@ public IBaseParameters replaceReferences( @Override public Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails) { return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { - // FIXME KHS get partition from request - JpaPid sourcePid = - myIdHelperService.getPidOrThrowException(RequestPartitionId.allPartitions(), theResourceId); - return myResourceLinkDao.countResourcesTargetingPid(sourcePid.getId()); + return myResourceLinkDao.countResourcesTargetingFhirTypeAndId(theResourceId.getResourceType(), theResourceId.getIdPart()); }); } @@ -151,12 +146,12 @@ private void fakeBackgroundTaskUpdate( myFakeExecutor.submit(() -> { try { - List pidList = myHapiTransactionService + List pidList = myHapiTransactionService .withSystemRequestOnPartition(thePartitionId) - .execute(() -> getReferencingResourcePidStream(theReplaceReferenceRequest) + .execute(() -> myResourceLinkDao.streamSourceIdsForTargetPid(theReplaceReferenceRequest.sourceId.getResourceType(), theReplaceReferenceRequest.sourceId.getIdPart()) .collect(Collectors.toUnmodifiableList())); - List> chunks = Lists.partition(pidList, theReplaceReferenceRequest.batchSize); + List> chunks = Lists.partition(pidList, theReplaceReferenceRequest.batchSize); List outputBundles = new ArrayList<>(); chunks.forEach(chunk -> { @@ -195,7 +190,7 @@ private IBaseParameters replaceReferencesPreferSync( ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // TODO KHS get partition from request - StopLimitAccumulator accumulator = myHapiTransactionService + StopLimitAccumulator accumulator = myHapiTransactionService .withRequest(theRequestDetails) .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); @@ -216,9 +211,9 @@ private IBaseParameters replaceReferencesPreferSync( private Bundle patchReferencingResources( ReplaceReferenceRequest theReplaceReferenceRequest, - List thePidList, + List theFhirIdList, RequestDetails theRequestDetails) { - Bundle patchBundle = buildPatchBundle(theReplaceReferenceRequest, theRequestDetails, thePidList); + Bundle patchBundle = buildPatchBundle(theReplaceReferenceRequest, theRequestDetails, theFhirIdList); IFhirSystemDao systemDao = myDaoRegistry.getSystemDao(); Bundle result = systemDao.transaction(theRequestDetails, patchBundle); // TODO KHS shouldn't transaction response bundles have ids? @@ -226,33 +221,22 @@ private Bundle patchReferencingResources( return result; } - private @Nonnull StopLimitAccumulator getAllPidsWithLimit( + private @Nonnull StopLimitAccumulator getAllPidsWithLimit( ReplaceReferenceRequest theReplaceReferenceRequest) { - Stream pidStream = getReferencingResourcePidStream(theReplaceReferenceRequest); - StopLimitAccumulator accumulator = - StopLimitAccumulator.fromStreamAndLimit(pidStream, theReplaceReferenceRequest.batchSize); - return accumulator; - } - private @Nonnull Stream getReferencingResourcePidStream( - ReplaceReferenceRequest theReplaceReferenceRequest) { - JpaPid sourcePid = myIdHelperService.getPidOrThrowException( - RequestPartitionId.allPartitions(), theReplaceReferenceRequest.sourceId); - - return myResourceLinkDao.streamSourcePidsForTargetPid(sourcePid.getId()).map(JpaPid::fromId); + Stream idStream = myResourceLinkDao.streamSourceIdsForTargetPid(theReplaceReferenceRequest.sourceId.getResourceType(), theReplaceReferenceRequest.sourceId.getIdPart()); + StopLimitAccumulator accumulator = + StopLimitAccumulator.fromStreamAndLimit(idStream, theReplaceReferenceRequest.batchSize); + return accumulator; } private Bundle buildPatchBundle( ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails, - List thePidList) { + List theFhirIdList) { BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); - thePidList.stream() - .map(myIdHelperService::translatePidIdToForcedIdWithCache) - .filter(Optional::isPresent) - .map(Optional::get) - .map(IdDt::new) + theFhirIdList .forEach(referencingResourceId -> { IFhirResourceDao dao = getDao(referencingResourceId.getResourceType()); IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); @@ -260,8 +244,7 @@ private Bundle buildPatchBundle( IIdType resourceId = resource.getIdElement(); bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); }); - Bundle patchBundle = bundleBuilder.getBundleTyped(); - return patchBundle; + return bundleBuilder.getBundleTyped(); } private @Nonnull Parameters buildPatchParams( diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java index ced5e026f228..b4768653098e 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.batch2.jobs.importpull.BulkImportPullConfig; import ca.uhn.fhir.batch2.jobs.imprt.BulkImportAppCtx; import ca.uhn.fhir.batch2.jobs.reindex.ReindexAppCtx; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx; import ca.uhn.fhir.batch2.jobs.termcodesystem.TermCodeSystemJobConfig; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -38,5 +39,6 @@ BulkExportAppCtx.class, TermCodeSystemJobConfig.class, BulkImportPullConfig.class, + ReplaceReferencesAppCtx.class }) public class Batch2JobsConfig {} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResults.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResults.java new file mode 100644 index 000000000000..fa9899981ef4 --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResults.java @@ -0,0 +1,6 @@ +package ca.uhn.fhir.batch2.jobs.replacereferences; + +import ca.uhn.fhir.model.api.IModelJson; + +public class ReplaceReferenceResults implements IModelJson { +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java new file mode 100644 index 000000000000..a49e5166a686 --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java @@ -0,0 +1,17 @@ +package ca.uhn.fhir.batch2.jobs.replacereferences; + +import ca.uhn.fhir.batch2.api.IJobDataSink; +import ca.uhn.fhir.batch2.api.IJobStepWorker; +import ca.uhn.fhir.batch2.api.JobExecutionFailedException; +import ca.uhn.fhir.batch2.api.RunOutcome; +import ca.uhn.fhir.batch2.api.StepExecutionDetails; +import ca.uhn.fhir.batch2.jobs.chunk.ResourceIdListWorkChunkJson; +import jakarta.annotation.Nonnull; + +public class ReplaceReferenceUpdateStep implements IJobStepWorker { + @Nonnull + @Override + public RunOutcome run(@Nonnull StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { + return null; + } +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java new file mode 100644 index 000000000000..7a996e76b274 --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java @@ -0,0 +1,17 @@ +package ca.uhn.fhir.batch2.jobs.replacereferences; + +import ca.uhn.fhir.batch2.api.IJobDataSink; +import ca.uhn.fhir.batch2.api.IJobStepWorker; +import ca.uhn.fhir.batch2.api.JobExecutionFailedException; +import ca.uhn.fhir.batch2.api.RunOutcome; +import ca.uhn.fhir.batch2.api.StepExecutionDetails; +import ca.uhn.fhir.batch2.api.VoidModel; +import jakarta.annotation.Nonnull; + +public class ReplaceReferenceUpdateTaskStep implements IJobStepWorker { + @Nonnull + @Override + public RunOutcome run(@Nonnull StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { + return null; + } +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java new file mode 100644 index 000000000000..46fbb4838341 --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java @@ -0,0 +1,50 @@ +package ca.uhn.fhir.batch2.jobs.replacereferences; + +import ca.uhn.fhir.batch2.jobs.chunk.ResourceIdListWorkChunkJson; +import ca.uhn.fhir.batch2.jobs.imprt.BulkImportJobParameters; +import ca.uhn.fhir.batch2.jobs.imprt.NdJsonFileJson; +import ca.uhn.fhir.batch2.jobs.reindex.models.ReindexResults; +import ca.uhn.fhir.batch2.model.JobDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static ca.uhn.fhir.batch2.jobs.imprt.BulkImportAppCtx.JOB_BULK_IMPORT_PULL; + +@Configuration +public class ReplaceReferencesAppCtx { + private static final String JOB_REPLACE_REFERENCES = "REPLACE_REFERENCES"; + + @Bean + public JobDefinition bulkImport2JobDefinition(ReplaceReferencesQueryIdsStep theReplaceReferencesQueryIds, ReplaceReferenceUpdateStep theReplaceReferenceUpdateStep, ReplaceReferenceUpdateTaskStep theReplaceReferenceUpdateTaskStep) { + return JobDefinition.newBuilder() + .setJobDefinitionId(JOB_REPLACE_REFERENCES) + .setJobDescription("Replace References") + .setJobDefinitionVersion(1) + .setParametersType(ReplaceReferencesJobParameters.class) + + .addFirstStep("query-ids", + "Query IDs of resources that link to the source resource", + ResourceIdListWorkChunkJson.class, + theReplaceReferencesQueryIds) + .addIntermediateStep( + "replace-references", "Update all references from pointing to source to pointing to target", ReplaceReferenceResults.class, theReplaceReferenceUpdateStep) + .addLastStep("update-task", "Waits for replace reference work to complete and updates Task.", theReplaceReferenceUpdateTaskStep) + .build(); + } + + @Bean + public ReplaceReferencesQueryIdsStep replaceReferencesQueryIdsStep() { + return new ReplaceReferencesQueryIdsStep(); + } + + @Bean + public ReplaceReferenceUpdateStep replaceReferenceUpdateStep() { + return new ReplaceReferenceUpdateStep(); + } + + @Bean + public ReplaceReferenceUpdateTaskStep replaceReferenceUpdateTaskStep() { + return new ReplaceReferenceUpdateTaskStep(); + } + +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java new file mode 100644 index 000000000000..feb087eac9a9 --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java @@ -0,0 +1,7 @@ +package ca.uhn.fhir.batch2.jobs.replacereferences; + + +import ca.uhn.fhir.model.api.IModelJson; + +public class ReplaceReferencesJobParameters implements IModelJson { +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java new file mode 100644 index 000000000000..1614ff5e44b6 --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java @@ -0,0 +1,18 @@ +package ca.uhn.fhir.batch2.jobs.replacereferences; + +import ca.uhn.fhir.batch2.api.IJobDataSink; +import ca.uhn.fhir.batch2.api.IJobStepWorker; +import ca.uhn.fhir.batch2.api.JobExecutionFailedException; +import ca.uhn.fhir.batch2.api.RunOutcome; +import ca.uhn.fhir.batch2.api.StepExecutionDetails; +import ca.uhn.fhir.batch2.api.VoidModel; +import ca.uhn.fhir.batch2.jobs.chunk.ResourceIdListWorkChunkJson; +import jakarta.annotation.Nonnull; + +public class ReplaceReferencesQueryIdsStep implements IJobStepWorker { + @Nonnull + @Override + public RunOutcome run(@Nonnull StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { + return null; + } +} From 01d1eaf09b710cff37453f613cb006cc01fe01ec Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 9 Dec 2024 14:19:40 -0500 Subject: [PATCH 053/148] start building batch 2 improve dao --- .../uhn/fhir/jpa/dao/data/IResourceLinkDao.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java index 6aeb93206ec4..fa9862a6f1af 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java @@ -21,7 +21,6 @@ import ca.uhn.fhir.jpa.model.entity.ResourceLink; import ca.uhn.fhir.model.primitive.IdDt; -import org.hl7.fhir.instance.model.api.IIdType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -49,9 +48,15 @@ public interface IResourceLinkDao extends JpaRepository, IHa @Query("SELECT t FROM ResourceLink t LEFT JOIN FETCH t.myTargetResource tr WHERE t.myId in :pids") List findByPidAndFetchTargetDetails(@Param("pids") List thePids); - @Query("SELECT DISTINCT new ca.uhn.fhir.model.primitive.IdDt(t.mySourceResourceType, t.mySourceResource.myFhirId) FROM ResourceLink t WHERE t.myTargetResourceType = :resourceType AND t.myTargetResource.myFhirId = :resourceFhirId") - Stream streamSourceIdsForTargetPid(@Param("resourceType") String theTargetResourceType, @Param("resourceFhirId") String theTargetResourceFhirId); - - @Query("SELECT COUNT(DISTINCT t.mySourceResourcePid) FROM ResourceLink t WHERE t.myTargetResourceType = :resourceType AND t.myTargetResource.myFhirId = :resourceFhirId") - Integer countResourcesTargetingFhirTypeAndId(@Param("resourceType") String theTargetResourceType, @Param("resourceFhirId") String theTargetResourceFhirId); + @Query( + "SELECT DISTINCT new ca.uhn.fhir.model.primitive.IdDt(t.mySourceResourceType, t.mySourceResource.myFhirId) FROM ResourceLink t WHERE t.myTargetResourceType = :resourceType AND t.myTargetResource.myFhirId = :resourceFhirId") + Stream streamSourceIdsForTargetPid( + @Param("resourceType") String theTargetResourceType, + @Param("resourceFhirId") String theTargetResourceFhirId); + + @Query( + "SELECT COUNT(DISTINCT t.mySourceResourcePid) FROM ResourceLink t WHERE t.myTargetResourceType = :resourceType AND t.myTargetResource.myFhirId = :resourceFhirId") + Integer countResourcesTargetingFhirTypeAndId( + @Param("resourceType") String theTargetResourceType, + @Param("resourceFhirId") String theTargetResourceFhirId); } From aa2ea0aa043f51687c698b2f9e7e7ab70d6ff4e6 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 9 Dec 2024 14:20:11 -0500 Subject: [PATCH 054/148] start building batch 2 improve dao --- .../provider/ReplaceReferencesSvcImpl.java | 28 ++++++------ .../ReplaceReferenceResults.java | 3 +- .../ReplaceReferenceUpdateStep.java | 11 ++++- .../ReplaceReferenceUpdateTaskStep.java | 10 ++++- .../ReplaceReferencesAppCtx.java | 43 ++++++++++--------- .../ReplaceReferencesJobParameters.java | 4 +- .../ReplaceReferencesQueryIdsStep.java | 8 +++- 7 files changed, 63 insertions(+), 44 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 1bd2901fed6d..b58052a4c38d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -27,7 +27,6 @@ import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; -import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; @@ -54,7 +53,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -112,7 +110,8 @@ public IBaseParameters replaceReferences( @Override public Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails) { return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { - return myResourceLinkDao.countResourcesTargetingFhirTypeAndId(theResourceId.getResourceType(), theResourceId.getIdPart()); + return myResourceLinkDao.countResourcesTargetingFhirTypeAndId( + theResourceId.getResourceType(), theResourceId.getIdPart()); }); } @@ -148,7 +147,10 @@ private void fakeBackgroundTaskUpdate( List pidList = myHapiTransactionService .withSystemRequestOnPartition(thePartitionId) - .execute(() -> myResourceLinkDao.streamSourceIdsForTargetPid(theReplaceReferenceRequest.sourceId.getResourceType(), theReplaceReferenceRequest.sourceId.getIdPart()) + .execute(() -> myResourceLinkDao + .streamSourceIdsForTargetPid( + theReplaceReferenceRequest.sourceId.getResourceType(), + theReplaceReferenceRequest.sourceId.getIdPart()) .collect(Collectors.toUnmodifiableList())); List> chunks = Lists.partition(pidList, theReplaceReferenceRequest.batchSize); @@ -224,7 +226,8 @@ private Bundle patchReferencingResources( private @Nonnull StopLimitAccumulator getAllPidsWithLimit( ReplaceReferenceRequest theReplaceReferenceRequest) { - Stream idStream = myResourceLinkDao.streamSourceIdsForTargetPid(theReplaceReferenceRequest.sourceId.getResourceType(), theReplaceReferenceRequest.sourceId.getIdPart()); + Stream idStream = myResourceLinkDao.streamSourceIdsForTargetPid( + theReplaceReferenceRequest.sourceId.getResourceType(), theReplaceReferenceRequest.sourceId.getIdPart()); StopLimitAccumulator accumulator = StopLimitAccumulator.fromStreamAndLimit(idStream, theReplaceReferenceRequest.batchSize); return accumulator; @@ -236,14 +239,13 @@ private Bundle buildPatchBundle( List theFhirIdList) { BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); - theFhirIdList - .forEach(referencingResourceId -> { - IFhirResourceDao dao = getDao(referencingResourceId.getResourceType()); - IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); - Parameters patchParams = buildPatchParams(theReplaceReferenceRequest, resource); - IIdType resourceId = resource.getIdElement(); - bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); - }); + theFhirIdList.forEach(referencingResourceId -> { + IFhirResourceDao dao = getDao(referencingResourceId.getResourceType()); + IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); + Parameters patchParams = buildPatchParams(theReplaceReferenceRequest, resource); + IIdType resourceId = resource.getIdElement(); + bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); + }); return bundleBuilder.getBundleTyped(); } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResults.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResults.java index fa9899981ef4..22133a3910fc 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResults.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResults.java @@ -2,5 +2,4 @@ import ca.uhn.fhir.model.api.IModelJson; -public class ReplaceReferenceResults implements IModelJson { -} +public class ReplaceReferenceResults implements IModelJson {} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java index a49e5166a686..fa29dae5b6a1 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java @@ -8,10 +8,17 @@ import ca.uhn.fhir.batch2.jobs.chunk.ResourceIdListWorkChunkJson; import jakarta.annotation.Nonnull; -public class ReplaceReferenceUpdateStep implements IJobStepWorker { +public class ReplaceReferenceUpdateStep + implements IJobStepWorker< + ReplaceReferencesJobParameters, ResourceIdListWorkChunkJson, ReplaceReferenceResults> { @Nonnull @Override - public RunOutcome run(@Nonnull StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { + public RunOutcome run( + @Nonnull + StepExecutionDetails + theStepExecutionDetails, + @Nonnull IJobDataSink theDataSink) + throws JobExecutionFailedException { return null; } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java index 7a996e76b274..03164f73c3fc 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java @@ -8,10 +8,16 @@ import ca.uhn.fhir.batch2.api.VoidModel; import jakarta.annotation.Nonnull; -public class ReplaceReferenceUpdateTaskStep implements IJobStepWorker { +public class ReplaceReferenceUpdateTaskStep + implements IJobStepWorker { @Nonnull @Override - public RunOutcome run(@Nonnull StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { + public RunOutcome run( + @Nonnull + StepExecutionDetails + theStepExecutionDetails, + @Nonnull IJobDataSink theDataSink) + throws JobExecutionFailedException { return null; } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java index 46fbb4838341..23aee64477b9 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java @@ -1,35 +1,39 @@ package ca.uhn.fhir.batch2.jobs.replacereferences; import ca.uhn.fhir.batch2.jobs.chunk.ResourceIdListWorkChunkJson; -import ca.uhn.fhir.batch2.jobs.imprt.BulkImportJobParameters; -import ca.uhn.fhir.batch2.jobs.imprt.NdJsonFileJson; -import ca.uhn.fhir.batch2.jobs.reindex.models.ReindexResults; import ca.uhn.fhir.batch2.model.JobDefinition; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import static ca.uhn.fhir.batch2.jobs.imprt.BulkImportAppCtx.JOB_BULK_IMPORT_PULL; - @Configuration public class ReplaceReferencesAppCtx { private static final String JOB_REPLACE_REFERENCES = "REPLACE_REFERENCES"; @Bean - public JobDefinition bulkImport2JobDefinition(ReplaceReferencesQueryIdsStep theReplaceReferencesQueryIds, ReplaceReferenceUpdateStep theReplaceReferenceUpdateStep, ReplaceReferenceUpdateTaskStep theReplaceReferenceUpdateTaskStep) { + public JobDefinition bulkImport2JobDefinition( + ReplaceReferencesQueryIdsStep theReplaceReferencesQueryIds, + ReplaceReferenceUpdateStep theReplaceReferenceUpdateStep, + ReplaceReferenceUpdateTaskStep theReplaceReferenceUpdateTaskStep) { return JobDefinition.newBuilder() - .setJobDefinitionId(JOB_REPLACE_REFERENCES) - .setJobDescription("Replace References") - .setJobDefinitionVersion(1) - .setParametersType(ReplaceReferencesJobParameters.class) - - .addFirstStep("query-ids", - "Query IDs of resources that link to the source resource", - ResourceIdListWorkChunkJson.class, - theReplaceReferencesQueryIds) - .addIntermediateStep( - "replace-references", "Update all references from pointing to source to pointing to target", ReplaceReferenceResults.class, theReplaceReferenceUpdateStep) - .addLastStep("update-task", "Waits for replace reference work to complete and updates Task.", theReplaceReferenceUpdateTaskStep) - .build(); + .setJobDefinitionId(JOB_REPLACE_REFERENCES) + .setJobDescription("Replace References") + .setJobDefinitionVersion(1) + .setParametersType(ReplaceReferencesJobParameters.class) + .addFirstStep( + "query-ids", + "Query IDs of resources that link to the source resource", + ResourceIdListWorkChunkJson.class, + theReplaceReferencesQueryIds) + .addIntermediateStep( + "replace-references", + "Update all references from pointing to source to pointing to target", + ReplaceReferenceResults.class, + theReplaceReferenceUpdateStep) + .addLastStep( + "update-task", + "Waits for replace reference work to complete and updates Task.", + theReplaceReferenceUpdateTaskStep) + .build(); } @Bean @@ -46,5 +50,4 @@ public ReplaceReferenceUpdateStep replaceReferenceUpdateStep() { public ReplaceReferenceUpdateTaskStep replaceReferenceUpdateTaskStep() { return new ReplaceReferenceUpdateTaskStep(); } - } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java index feb087eac9a9..374c756cc4d8 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java @@ -1,7 +1,5 @@ package ca.uhn.fhir.batch2.jobs.replacereferences; - import ca.uhn.fhir.model.api.IModelJson; -public class ReplaceReferencesJobParameters implements IModelJson { -} +public class ReplaceReferencesJobParameters implements IModelJson {} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java index 1614ff5e44b6..af21bf9c00e9 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java @@ -9,10 +9,14 @@ import ca.uhn.fhir.batch2.jobs.chunk.ResourceIdListWorkChunkJson; import jakarta.annotation.Nonnull; -public class ReplaceReferencesQueryIdsStep implements IJobStepWorker { +public class ReplaceReferencesQueryIdsStep + implements IJobStepWorker { @Nonnull @Override - public RunOutcome run(@Nonnull StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { + public RunOutcome run( + @Nonnull StepExecutionDetails theStepExecutionDetails, + @Nonnull IJobDataSink theDataSink) + throws JobExecutionFailedException { return null; } } From 1a4ae51e8f8b8b40ff6ff3e4cbb146fbec2224cc Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 9 Dec 2024 19:56:16 -0500 Subject: [PATCH 055/148] move test setup out to helper --- .../ReplaceReferencesBatchTest.java | 20 ++ .../ReplaceReferencesTestHelper.java | 269 ++++++++++++++++ .../jpa/provider/r4/PatientMergeR4Test.java | 290 +++--------------- 3 files changed, 326 insertions(+), 253 deletions(-) create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesBatchTest.java create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesTestHelper.java diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesBatchTest.java new file mode 100644 index 000000000000..1d027e50a3a6 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesBatchTest.java @@ -0,0 +1,20 @@ +package ca.uhn.fhir.jpa.dao.r4.replacereferences; + +import ca.uhn.fhir.batch2.api.IJobCoordinator; +import ca.uhn.fhir.batch2.api.IJobPersistence; +import ca.uhn.fhir.jpa.test.BaseJpaR4Test; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class ReplaceReferencesBatchTest extends BaseJpaR4Test { + + @Autowired + private IJobCoordinator myJobCoordinator; + @Autowired + private IJobPersistence myJobPersistence; + + @Test + public void testSimple() { + + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesTestHelper.java new file mode 100644 index 000000000000..eeae80652548 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesTestHelper.java @@ -0,0 +1,269 @@ +package ca.uhn.fhir.jpa.dao.r4.replacereferences; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +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.server.provider.ProviderConstants; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CarePlan; +import org.hl7.fhir.r4.model.Encounter; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.Observation; +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.Reference; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Type; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Set; + +import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER; +import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER_RESPOND_ASYNC; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES; +import static org.assertj.core.api.Assertions.assertThat; + +public class ReplaceReferencesTestHelper { + private static final Logger ourLog = LoggerFactory.getLogger(ReplaceReferencesTestHelper.class); + + static final Identifier pat1IdentifierA = new Identifier().setSystem("SYS1A").setValue("VAL1A"); + static final Identifier pat1IdentifierB = new Identifier().setSystem("SYS1B").setValue("VAL1B"); + static final Identifier pat2IdentifierA = new Identifier().setSystem("SYS2A").setValue("VAL2A"); + static final Identifier pat2IdentifierB = new Identifier().setSystem("SYS2B").setValue("VAL2B"); + static final Identifier patBothIdentifierC = new Identifier().setSystem("SYSC").setValue("VALC"); + public static final int TOTAL_EXPECTED_PATCHES = 23; + public static final int SMALL_BATCH_SIZE = 5; + public static final int EXPECTED_SMALL_BATCHES = (TOTAL_EXPECTED_PATCHES + SMALL_BATCH_SIZE - 1) / SMALL_BATCH_SIZE; + private final IFhirResourceDao myPatientDao; + + IIdType myOrgId; + IIdType mySourcePatientId; + IIdType mySourceCarePlanId; + IIdType mySourceEncId1; + IIdType mySourceEncId2; + ArrayList mySourceObsIds; + IIdType myTargetPatientId; + IIdType myTargetEnc1; + Patient myResultPatient; + + private final FhirContext myFhirContext; + private final IGenericClient myFhirClient; + private final SystemRequestDetails mySrd = new SystemRequestDetails(); + + public ReplaceReferencesTestHelper(FhirContext theFhirContext, IGenericClient theFhirClient, DaoRegistry theDaoRegistry) { + myFhirContext = theFhirContext; + myFhirClient = theFhirClient; + myPatientDao = theDaoRegistry.getResourceDao(Patient.class); + } + + public void beforeEach() throws Exception { + + Organization org = new Organization(); + org.setName("an org"); + myOrgId = myFhirClient.create().resource(org).execute().getId().toUnqualifiedVersionless(); + ourLog.info("OrgId: {}", myOrgId); + + Patient patient1 = new Patient(); + patient1.getManagingOrganization().setReferenceElement(myOrgId); + patient1.addIdentifier(pat1IdentifierA); + patient1.addIdentifier(pat1IdentifierB); + patient1.addIdentifier(patBothIdentifierC); + mySourcePatientId = myFhirClient.create().resource(patient1).execute().getId().toUnqualifiedVersionless(); + + Patient patient2 = new Patient(); + patient2.addIdentifier(pat2IdentifierA); + patient2.addIdentifier(pat2IdentifierB); + patient2.addIdentifier(patBothIdentifierC); + patient2.getManagingOrganization().setReferenceElement(myOrgId); + myTargetPatientId = myFhirClient.create().resource(patient2).execute().getId().toUnqualifiedVersionless(); + + Encounter enc1 = new Encounter(); + enc1.setStatus(Encounter.EncounterStatus.CANCELLED); + enc1.getSubject().setReferenceElement(mySourcePatientId); + enc1.getServiceProvider().setReferenceElement(myOrgId); + mySourceEncId1 = myFhirClient.create().resource(enc1).execute().getId().toUnqualifiedVersionless(); + + Encounter enc2 = new Encounter(); + enc2.setStatus(Encounter.EncounterStatus.ARRIVED); + enc2.getSubject().setReferenceElement(mySourcePatientId); + enc2.getServiceProvider().setReferenceElement(myOrgId); + mySourceEncId2 = myFhirClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless(); + + CarePlan carePlan = new CarePlan(); + carePlan.setStatus(CarePlan.CarePlanStatus.ACTIVE); + carePlan.getSubject().setReferenceElement(mySourcePatientId); + mySourceCarePlanId = myFhirClient.create().resource(carePlan).execute().getId().toUnqualifiedVersionless(); + + Encounter targetEnc1 = new Encounter(); + targetEnc1.setStatus(Encounter.EncounterStatus.ARRIVED); + targetEnc1.getSubject().setReferenceElement(myTargetPatientId); + targetEnc1.getServiceProvider().setReferenceElement(myOrgId); + this.myTargetEnc1 = myFhirClient.create().resource(targetEnc1).execute().getId().toUnqualifiedVersionless(); + + mySourceObsIds = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + Observation obs = new Observation(); + obs.getSubject().setReferenceElement(mySourcePatientId); + obs.setStatus(Observation.ObservationStatus.FINAL); + IIdType obsId = myFhirClient.create().resource(obs).execute().getId().toUnqualifiedVersionless(); + mySourceObsIds.add(obsId); + } + + myResultPatient = new Patient(); + myResultPatient.setIdElement((IdType) myTargetPatientId); + myResultPatient.addIdentifier(pat1IdentifierA); + Patient.PatientLinkComponent link = myResultPatient.addLink(); + link.setOther(new Reference(mySourcePatientId)); + link.setType(Patient.LinkType.REPLACES); + } + + public void setSourceAndTarget(PatientMergeInputParameters inParams) { + inParams.sourcePatient = new Reference().setReferenceElement(mySourcePatientId); + inParams.targetPatient = new Reference().setReferenceElement(myTargetPatientId); + } + + public void setResultPatient(PatientMergeInputParameters theInParams) { + theInParams.resultPatient = myResultPatient; + } + + public Patient readSourcePatient() { + return myPatientDao.read(mySourcePatientId, mySrd); + } + + public Object getTargetPatientId() { + return myTargetPatientId; + } + + public Bundle getTargetEverythingBundle() { + return myFhirClient.operation() + .onInstance(myTargetPatientId) + .named("$everything") + .withParameter(Parameters.class, "_count", new IntegerType(100)) + .useHttpGet() + .returnResourceType(Bundle.class) + .execute(); + } + + public Parameters callReplaceReferences(boolean theIsAsync) { + return callReplaceReferencesWithBatchSize(theIsAsync, null); + } + + public Parameters callReplaceReferencesWithBatchSize(boolean theIsAsync, Integer theBatchSize) { + IOperationUntypedWithInputAndPartialOutput request = myFhirClient.operation() + .onServer() + .named(OPERATION_REPLACE_REFERENCES) + .withParameter(Parameters.class, ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID, new StringType(mySourcePatientId.getValue())) + .andParameter(ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID, new StringType(myTargetPatientId.getValue())); + if (theBatchSize != null) { + request.andParameter(ProviderConstants.OPERATION_REPLACE_REFERENCES_BATCH_SIZE, new IntegerType(theBatchSize)); + } + + if (theIsAsync) { + request.withAdditionalHeader(HEADER_PREFER, HEADER_PREFER_RESPOND_ASYNC); + } + + return request + .returnResourceType(Parameters.class) + .execute(); + } + + public void assertContainsAllResources(Set theActual, boolean theWithDelete) { + if (theWithDelete) { + assertThat(theActual).doesNotContain(mySourcePatientId); + } + assertThat(theActual).contains(mySourceEncId1); + assertThat(theActual).contains(mySourceEncId2); + assertThat(theActual).contains(myOrgId); + assertThat(theActual).contains(mySourceCarePlanId); + assertThat(theActual).containsAll(mySourceObsIds); + assertThat(theActual).contains(myTargetPatientId); + assertThat(theActual).contains(myTargetEnc1); + } + + public void assertNothingChanged(Set theActual) { + assertThat(theActual).doesNotContain(mySourcePatientId); + assertThat(theActual).doesNotContain(mySourceEncId1); + assertThat(theActual).doesNotContain(mySourceEncId2); + assertThat(theActual).contains(myOrgId); + assertThat(theActual).doesNotContain(mySourceCarePlanId); + assertThat(theActual).doesNotContainAnyElementsOf(mySourceObsIds); + assertThat(theActual).contains(myTargetPatientId); + assertThat(theActual).contains(myTargetEnc1); + } + + public PatientMergeInputParameters buildMultipleTargetMatchParameters(boolean theWithDelete, boolean theWithInputResultPatient, boolean theWithPreview) { + PatientMergeInputParameters inParams = new PatientMergeInputParameters(); + inParams.sourcePatient = new Reference().setReferenceElement(mySourcePatientId); + inParams.targetPatientIdentifier = patBothIdentifierC; + inParams.deleteSource = theWithDelete; + if (theWithInputResultPatient) { + inParams.resultPatient = myResultPatient; + } + if (theWithPreview) { + inParams.preview = true; + } + return inParams; + } + + public PatientMergeInputParameters buildMultipleSourceMatchParameters(boolean theWithDelete, boolean theWithInputResultPatient, boolean theWithPreview) { + PatientMergeInputParameters inParams = new PatientMergeInputParameters(); + inParams.sourcePatientIdentifier = patBothIdentifierC; + inParams.targetPatient = new Reference().setReferenceElement(mySourcePatientId); + inParams.deleteSource = theWithDelete; + if (theWithInputResultPatient) { + inParams.resultPatient = myResultPatient; + } + if (theWithPreview) { + inParams.preview = true; + } + return inParams; + } + + public static class PatientMergeInputParameters { + public Type sourcePatient; + public Type sourcePatientIdentifier; + public Type targetPatient; + public Type targetPatientIdentifier; + public Patient resultPatient; + public Boolean preview; + public Boolean deleteSource; + + public Parameters asParametersResource() { + Parameters inParams = new Parameters(); + if (sourcePatient != null) { + inParams.addParameter().setName("source-patient").setValue(sourcePatient); + } + if (sourcePatientIdentifier != null) { + inParams.addParameter().setName("source-patient-identifier").setValue(sourcePatientIdentifier); + } + if (targetPatient != null) { + inParams.addParameter().setName("target-patient").setValue(targetPatient); + } + if (targetPatientIdentifier != null) { + inParams.addParameter().setName("target-patient-identifier").setValue(targetPatientIdentifier); + } + if (resultPatient != null) { + inParams.addParameter().setName("result-patient").setResource(resultPatient); + } + if (preview != null) { + inParams.addParameter().setName("preview").setValue(new BooleanType(preview)); + } + if (deleteSource != null) { + inParams.addParameter().setName("delete-source").setValue(new BooleanType(deleteSource)); + } + return inParams; + } + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index ae171a36b799..7cad1590bb4e 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -1,44 +1,26 @@ package ca.uhn.fhir.jpa.provider.r4; -import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.jpa.dao.r4.replacereferences.ReplaceReferencesTestHelper; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.parser.StrictErrorHandler; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.EncodingEnum; -import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInput; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.rest.server.provider.ProviderConstants; -import com.google.common.base.Charsets; -import org.apache.commons.io.IOUtils; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; +import jakarta.annotation.Nonnull; import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; -import org.hl7.fhir.r4.model.CarePlan; import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.Encounter; -import org.hl7.fhir.r4.model.Encounter.EncounterStatus; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; -import org.hl7.fhir.r4.model.IntegerType; -import org.hl7.fhir.r4.model.Observation; -import org.hl7.fhir.r4.model.Observation.ObservationStatus; import org.hl7.fhir.r4.model.OperationOutcome; -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.Reference; import org.hl7.fhir.r4.model.Resource; -import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Task; -import org.hl7.fhir.r4.model.Type; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -50,13 +32,13 @@ import org.junit.jupiter.params.provider.ValueSource; import java.io.IOException; -import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; +import static ca.uhn.fhir.jpa.dao.r4.replacereferences.ReplaceReferencesTestHelper.EXPECTED_SMALL_BATCHES; import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER_RESPOND_ASYNC; @@ -66,7 +48,6 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_RESULT_PATIENT; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; import static org.assertj.core.api.Assertions.assertThat; @@ -79,32 +60,10 @@ public class PatientMergeR4Test extends BaseResourceProviderR4Test { static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PatientMergeR4Test.class); - static final FhirContext ourFhirContext = FhirContext.forR4Cached(); - - static final Identifier pat1IdentifierA = new Identifier().setSystem("SYS1A").setValue("VAL1A"); - static final Identifier pat1IdentifierB = new Identifier().setSystem("SYS1B").setValue("VAL1B"); - static final Identifier pat2IdentifierA = new Identifier().setSystem("SYS2A").setValue("VAL2A"); - static final Identifier pat2IdentifierB = new Identifier().setSystem("SYS2B").setValue("VAL2B"); - static final Identifier patBothIdentifierC = new Identifier().setSystem("SYSC").setValue("VALC"); - static final int TOTAL_EXPECTED_PATCHES = 23; - static final int SMALL_BATCH_SIZE = 5; - static final int EXPECTED_SMALL_BATCHES = (TOTAL_EXPECTED_PATCHES + SMALL_BATCH_SIZE - 1) / SMALL_BATCH_SIZE; - - - IIdType myOrgId; - IIdType mySourcePatId; - IIdType mySourceCarePlanId; - IIdType mySourceEncId1; - IIdType mySourceEncId2; - ArrayList mySourceObsIds; - IIdType myTargetPatId; - IIdType myTargetEnc1; - Patient myResultPatient; - @RegisterExtension - static MyExceptionHandler ourExceptionHandler = new MyExceptionHandler(); - - IGenericClient myFhirClient; + MyExceptionHandler ourExceptionHandler = new MyExceptionHandler(); + + ReplaceReferencesTestHelper myTestHelper; @Override @AfterEach @@ -112,7 +71,7 @@ public void after() throws Exception { super.after(); myStorageSettings.setReuseCachedSearchResultsForMillis(new JpaStorageSettings().getReuseCachedSearchResultsForMillis()); - } + } @Override @BeforeEach @@ -122,65 +81,8 @@ public void before() throws Exception { myStorageSettings.setAllowMultipleDelete(true); myFhirContext.setParserErrorHandler(new StrictErrorHandler()); - myFhirClient = myFhirContext.newRestfulGenericClient(myServerBase); - - Organization org = new Organization(); - org.setName("an org"); - myOrgId = myFhirClient.create().resource(org).execute().getId().toUnqualifiedVersionless(); - ourLog.info("OrgId: {}", myOrgId); - - Patient patient1 = new Patient(); - patient1.getManagingOrganization().setReferenceElement(myOrgId); - patient1.addIdentifier(pat1IdentifierA); - patient1.addIdentifier(pat1IdentifierB); - patient1.addIdentifier(patBothIdentifierC); - mySourcePatId = myFhirClient.create().resource(patient1).execute().getId().toUnqualifiedVersionless(); - - Patient patient2 = new Patient(); - patient2.addIdentifier(pat2IdentifierA); - patient2.addIdentifier(pat2IdentifierB); - patient2.addIdentifier(patBothIdentifierC); - patient2.getManagingOrganization().setReferenceElement(myOrgId); - myTargetPatId = myFhirClient.create().resource(patient2).execute().getId().toUnqualifiedVersionless(); - - Encounter enc1 = new Encounter(); - enc1.setStatus(EncounterStatus.CANCELLED); - enc1.getSubject().setReferenceElement(mySourcePatId); - enc1.getServiceProvider().setReferenceElement(myOrgId); - mySourceEncId1 = myFhirClient.create().resource(enc1).execute().getId().toUnqualifiedVersionless(); - - Encounter enc2 = new Encounter(); - enc2.setStatus(EncounterStatus.ARRIVED); - enc2.getSubject().setReferenceElement(mySourcePatId); - enc2.getServiceProvider().setReferenceElement(myOrgId); - mySourceEncId2 = myFhirClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless(); - - CarePlan carePlan = new CarePlan(); - carePlan.setStatus(CarePlan.CarePlanStatus.ACTIVE); - carePlan.getSubject().setReferenceElement(mySourcePatId); - mySourceCarePlanId = myFhirClient.create().resource(carePlan).execute().getId().toUnqualifiedVersionless(); - - Encounter targetEnc1 = new Encounter(); - targetEnc1.setStatus(EncounterStatus.ARRIVED); - targetEnc1.getSubject().setReferenceElement(myTargetPatId); - targetEnc1.getServiceProvider().setReferenceElement(myOrgId); - this.myTargetEnc1 = myFhirClient.create().resource(targetEnc1).execute().getId().toUnqualifiedVersionless(); - - mySourceObsIds = new ArrayList<>(); - for (int i = 0; i < 20; i++) { - Observation obs = new Observation(); - obs.getSubject().setReferenceElement(mySourcePatId); - obs.setStatus(ObservationStatus.FINAL); - IIdType obsId = myFhirClient.create().resource(obs).execute().getId().toUnqualifiedVersionless(); - mySourceObsIds.add(obsId); - } - - myResultPatient = new Patient(); - myResultPatient.setIdElement((IdType) myTargetPatId); - myResultPatient.addIdentifier(pat1IdentifierA); - Patient.PatientLinkComponent link = myResultPatient.addLink(); - link.setOther(new Reference(mySourcePatId)); - link.setType(Patient.LinkType.REPLACES); + myTestHelper = new ReplaceReferencesTestHelper(myFhirContext, myClient, myDaoRegistry); + myTestHelper.beforeEach(); } @ParameterizedTest @@ -207,12 +109,11 @@ public void before() throws Exception { public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPatient, boolean withPreview, boolean isAsync) throws Exception { // setup - PatientMergeInputParameters inParams = new PatientMergeInputParameters(); - inParams.sourcePatient = new Reference().setReferenceElement(mySourcePatId); - inParams.targetPatient = new Reference().setReferenceElement(myTargetPatId); + ReplaceReferencesTestHelper.PatientMergeInputParameters inParams = new ReplaceReferencesTestHelper.PatientMergeInputParameters(); + myTestHelper.setSourceAndTarget(inParams); inParams.deleteSource = withDelete; if (withInputResultPatient) { - inParams.resultPatient = myResultPatient; + myTestHelper.setResultPatient(inParams); } if (withPreview) { inParams.preview = true; @@ -231,8 +132,8 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa if (withInputResultPatient) { // if the following assert fails, check that these two patients are identical Patient p1 = (Patient) inParameters.getParameter(OPERATION_MERGE_RESULT_PATIENT).getResource(); Patient p2 = (Patient) input.getParameter(OPERATION_MERGE_RESULT_PATIENT).getResource(); - ourLog.info(ourFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(p1)); - ourLog.info(ourFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(p2)); + ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(p1)); + ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(p2)); } assertTrue(input.equalsDeep(inParameters)); @@ -245,7 +146,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa await().until(() -> taskCompleted(task.getIdElement())); Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); - ourLog.info("Complete Task: {}", ourFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); + ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); // FIXME KHS the rest of these asserts will likely need to be tweaked Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); @@ -266,7 +167,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa Reference outputRef = (Reference) taskOutput.getValue(); Bundle patchResultBundle = (Bundle) outputRef.getResource(); assertTrue(containedBundle.equalsDeep(patchResultBundle)); - validatePatchResultBundle(patchResultBundle, TOTAL_EXPECTED_PATCHES); + validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES); } else { // Synchronous case // Assert outcome OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); @@ -308,18 +209,18 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa } if (!withPreview && !withDelete) { // assert source has link to target - Patient source = myPatientDao.read(mySourcePatId, mySrd); + Patient source = myTestHelper.readSourcePatient(); assertThat(source.getLink()) .hasSize(1) .element(0) .extracting(link -> link.getOther().getReferenceElement()) - .isEqualTo(myTargetPatId); + .isEqualTo(myTestHelper.getTargetPatientId()); } } // Check that the linked resources were updated - Bundle bundle = fetchBundle(myServerBase + "/" + myTargetPatId + "/$everything?_format=json&_count=100"); + Bundle bundle = myTestHelper.getTargetEverythingBundle(); assertNull(bundle.getLink("next")); @@ -331,25 +232,9 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa ourLog.info("Found IDs: {}", actual); if (withPreview) { - assertThat(actual).doesNotContain(mySourcePatId); - assertThat(actual).doesNotContain(mySourceEncId1); - assertThat(actual).doesNotContain(mySourceEncId2); - assertThat(actual).contains(myOrgId); - assertThat(actual).doesNotContain(mySourceCarePlanId); - assertThat(actual).doesNotContainAnyElementsOf(mySourceObsIds); - assertThat(actual).contains(myTargetPatId); - assertThat(actual).contains(myTargetEnc1); + myTestHelper.assertNothingChanged(actual); } else { - if (withDelete) { - assertThat(actual).doesNotContain(mySourcePatId); - } - assertThat(actual).contains(mySourceEncId1); - assertThat(actual).contains(mySourceEncId2); - assertThat(actual).contains(myOrgId); - assertThat(actual).contains(mySourceCarePlanId); - assertThat(actual).containsAll(mySourceObsIds); - assertThat(actual).contains(myTargetPatId); - assertThat(actual).contains(myTargetEnc1); + myTestHelper.assertContainsAllResources(actual, withDelete); } } @@ -372,17 +257,7 @@ private Boolean taskCompleted(IdType theTaskId) { "false, false, false", }) public void testMultipleTargetMatchesFails(boolean withDelete, boolean withInputResultPatient, boolean withPreview) { - PatientMergeInputParameters inParams = new PatientMergeInputParameters(); - inParams.sourcePatient = new Reference().setReferenceElement(mySourcePatId); - inParams.targetPatientIdentifier = patBothIdentifierC; - inParams.deleteSource = withDelete; - if (withInputResultPatient) { - inParams.resultPatient = myResultPatient; - } - if (withPreview) { - inParams.preview = true; - } - + ReplaceReferencesTestHelper.PatientMergeInputParameters inParams = myTestHelper.buildMultipleTargetMatchParameters(withDelete, withInputResultPatient, withPreview); Parameters inParameters = inParams.asParametersResource(); @@ -403,16 +278,7 @@ public void testMultipleTargetMatchesFails(boolean withDelete, boolean withInput "false, false, false", }) public void testMultipleSourceMatchesFails(boolean withDelete, boolean withInputResultPatient, boolean withPreview) { - PatientMergeInputParameters inParams = new PatientMergeInputParameters(); - inParams.sourcePatientIdentifier = patBothIdentifierC; - inParams.targetPatient = new Reference().setReferenceElement(mySourcePatId); - inParams.deleteSource = withDelete; - if (withInputResultPatient) { - inParams.resultPatient = myResultPatient; - } - if (withPreview) { - inParams.preview = true; - } + ReplaceReferencesTestHelper.PatientMergeInputParameters inParams = myTestHelper.buildMultipleSourceMatchParameters(withDelete, withInputResultPatient, withPreview); Parameters inParameters = inParams.asParametersResource(); @@ -424,7 +290,7 @@ private void assertUnprocessibleEntityWithMessage(Parameters inParameters, Strin callMergeOperation(inParameters)) .isInstanceOf(UnprocessableEntityException.class) .extracting(UnprocessableEntityException.class::cast) - .extracting(PatientMergeR4Test::extractFailureMessage) + .extracting(this::extractFailureMessage) .isEqualTo(theExpectedMessage); } @@ -460,19 +326,7 @@ void test_MissingRequiredParameters_Returns400BadRequest() { @ValueSource(booleans = {false, true}) void testReplaceReferences(boolean isAsync) throws IOException { // exec - IOperationUntypedWithInput request = myClient.operation() - .onServer() - .named(OPERATION_REPLACE_REFERENCES) - .withParameter(Parameters.class, ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID, new StringType(mySourcePatId.getValue())) - .andParameter(ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID, new StringType(myTargetPatId.getValue())); - - if (isAsync) { - request.withAdditionalHeader(HEADER_PREFER, HEADER_PREFER_RESPOND_ASYNC); - } - - Parameters outParams = request - .returnResourceType(Parameters.class) - .execute(); + Parameters outParams = myTestHelper.callReplaceReferences(isAsync); assertThat(outParams.getParameter()).hasSize(1); @@ -484,7 +338,7 @@ void testReplaceReferences(boolean isAsync) throws IOException { await().until(() -> taskCompleted(task.getIdElement())); Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); - ourLog.info("Complete Task: {}", ourFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); + ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); @@ -509,7 +363,7 @@ void testReplaceReferences(boolean isAsync) throws IOException { } // validate - validatePatchResultBundle(patchResultBundle, TOTAL_EXPECTED_PATCHES); + validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES); // Check that the linked resources were updated @@ -520,20 +374,8 @@ void testReplaceReferences(boolean isAsync) throws IOException { @ValueSource(booleans = {false, true}) void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { // exec - IOperationUntypedWithInput request = myClient.operation() - .onServer() - .named(OPERATION_REPLACE_REFERENCES) - .withParameter(Parameters.class, ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID, new StringType(mySourcePatId.getValue())) - .andParameter(ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID, new StringType(myTargetPatId.getValue())) - .andParameter(ProviderConstants.OPERATION_REPLACE_REFERENCES_BATCH_SIZE, new IntegerType(SMALL_BATCH_SIZE)); - - if (isAsync) { - request.withAdditionalHeader(HEADER_PREFER, HEADER_PREFER_RESPOND_ASYNC); - } + Parameters outParams = myTestHelper.callReplaceReferencesWithBatchSize(isAsync, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); - Parameters outParams = request - .returnResourceType(Parameters.class) - .execute(); assertThat(outParams.getParameter()).hasSize(1); @@ -544,7 +386,7 @@ void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { await().until(() -> taskCompleted(task.getIdElement())); Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); - ourLog.info("Complete Task: {}", ourFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); + ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); assertThat(taskWithOutput.getOutput()).hasSize(EXPECTED_SMALL_BATCHES); List containedResources = taskWithOutput.getContained(); @@ -554,7 +396,7 @@ void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { .element(0) .isInstanceOf(Bundle.class); - int entriesLeft = TOTAL_EXPECTED_PATCHES; + int entriesLeft = ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES; for (int i = 1; i < EXPECTED_SMALL_BATCHES; i++) { Task.TaskOutputComponent taskOutput = taskWithOutput.getOutput().get(i); @@ -571,19 +413,18 @@ void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { assertTrue(containedBundle.equalsDeep(patchResultBundle)); // validate - entriesLeft -= SMALL_BATCH_SIZE; - int expectedNumberOfEntries = entriesLeft > SMALL_BATCH_SIZE ? SMALL_BATCH_SIZE : entriesLeft; + entriesLeft -= ReplaceReferencesTestHelper.SMALL_BATCH_SIZE; + int expectedNumberOfEntries = Math.min(entriesLeft, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); validatePatchResultBundle(patchResultBundle, expectedNumberOfEntries); } - // Check that the linked resources were updated validateLinksUsingEverything(); } private void validateLinksUsingEverything() throws IOException { - Bundle everythingBundle = fetchBundle(myServerBase + "/" + myTargetPatId + "/$everything?_format=json&_count=100"); + Bundle everythingBundle = myTestHelper.getTargetEverythingBundle(); assertNull(everythingBundle.getLink("next")); @@ -594,13 +435,7 @@ private void validateLinksUsingEverything() throws IOException { ourLog.info("Found IDs: {}", actual); - assertThat(actual).contains(mySourceEncId1); - assertThat(actual).contains(mySourceEncId2); - assertThat(actual).contains(myOrgId); - assertThat(actual).contains(mySourceCarePlanId); - assertThat(actual).containsAll(mySourceObsIds); - assertThat(actual).contains(myTargetPatId); - assertThat(actual).contains(myTargetEnc1); + myTestHelper.assertContainsAllResources(actual, false); } private static void validatePatchResultBundle(Bundle patchResultBundle, int theTotalExpectedPatches) { @@ -620,58 +455,7 @@ private static void validatePatchResultBundle(Bundle patchResultBundle, int theT // FIXME KHS look at PatientEverythingR4Test for ideas for other tests - private Bundle fetchBundle(String theUrl) throws IOException { - Bundle bundle; - HttpGet get = new HttpGet(theUrl); - CloseableHttpResponse resp = ourHttpClient.execute(get); - try { - assertEquals(EncodingEnum.JSON.getResourceContentTypeNonLegacy(), resp.getFirstHeader(Constants.HEADER_CONTENT_TYPE).getValue().replaceAll(";.*", "")); - bundle = EncodingEnum.JSON.newParser(myFhirContext).parseResource(Bundle.class, IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8)); - } finally { - IOUtils.closeQuietly(resp); - } - - return bundle; - } - - private static class PatientMergeInputParameters { - Type sourcePatient; - Type sourcePatientIdentifier; - Type targetPatient; - Type targetPatientIdentifier; - Patient resultPatient; - Boolean preview; - Boolean deleteSource; - - public Parameters asParametersResource() { - Parameters inParams = new Parameters(); - if (sourcePatient != null) { - inParams.addParameter().setName("source-patient").setValue(sourcePatient); - } - if (sourcePatientIdentifier != null) { - inParams.addParameter().setName("source-patient-identifier").setValue(sourcePatientIdentifier); - } - if (targetPatient != null) { - inParams.addParameter().setName("target-patient").setValue(targetPatient); - } - if (targetPatientIdentifier != null) { - inParams.addParameter().setName("target-patient-identifier").setValue(targetPatientIdentifier); - } - if (resultPatient != null) { - inParams.addParameter().setName("result-patient").setResource(resultPatient); - } - if (preview != null) { - inParams.addParameter().setName("preview").setValue(new BooleanType(preview)); - } - if (deleteSource != null) { - inParams.addParameter().setName("delete-source").setValue(new BooleanType(deleteSource)); - } - return inParams; - } - } - - - static class MyExceptionHandler implements TestExecutionExceptionHandler { + class MyExceptionHandler implements TestExecutionExceptionHandler { @Override public void handleTestExecutionException(ExtensionContext theExtensionContext, Throwable theThrowable) throws Throwable { if (theThrowable instanceof BaseServerResponseException) { @@ -683,9 +467,9 @@ public void handleTestExecutionException(ExtensionContext theExtensionContext, T } } - private static @NotNull String extractFailureMessage(BaseServerResponseException ex) { + private @Nonnull String extractFailureMessage(BaseServerResponseException ex) { String body = ex.getResponseBody(); - Parameters outParams = ourFhirContext.newJsonParser().parseResource(Parameters.class, body); + Parameters outParams = myFhirContext.newJsonParser().parseResource(Parameters.class, body); OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); return outcome.getIssue().stream() .map(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics) From 46a561152a1856e3f946a673c9b9136318777269 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 9 Dec 2024 20:08:47 -0500 Subject: [PATCH 056/148] move replace references tests out --- .../ReplaceReferencesTestHelper.java | 26 +++ .../jpa/provider/r4/PatientMergeR4Test.java | 150 +--------------- .../provider/r4/ReplaceReferencesR4Test.java | 160 ++++++++++++++++++ 3 files changed, 190 insertions(+), 146 deletions(-) create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesTestHelper.java index eeae80652548..f898393603b2 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesTestHelper.java @@ -16,11 +16,13 @@ import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.IntegerType; import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.OperationOutcome; 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.Reference; import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.Type; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -28,6 +30,7 @@ import java.util.ArrayList; import java.util.Set; +import java.util.regex.Pattern; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER_RESPOND_ASYNC; @@ -46,6 +49,7 @@ public class ReplaceReferencesTestHelper { public static final int SMALL_BATCH_SIZE = 5; public static final int EXPECTED_SMALL_BATCHES = (TOTAL_EXPECTED_PATCHES + SMALL_BATCH_SIZE - 1) / SMALL_BATCH_SIZE; private final IFhirResourceDao myPatientDao; + private final IFhirResourceDao myTaskDao; IIdType myOrgId; IIdType mySourcePatientId; @@ -65,6 +69,7 @@ public ReplaceReferencesTestHelper(FhirContext theFhirContext, IGenericClient th myFhirContext = theFhirContext; myFhirClient = theFhirClient; myPatientDao = theDaoRegistry.getResourceDao(Patient.class); + myTaskDao = theDaoRegistry.getResourceDao(Task.class); } public void beforeEach() throws Exception { @@ -155,6 +160,12 @@ public Bundle getTargetEverythingBundle() { .execute(); } + public Boolean taskCompleted(IdType theTaskId) { + Task updatedTask = myTaskDao.read(theTaskId, mySrd); + ourLog.info("Task {} status is {}", theTaskId, updatedTask.getStatus()); + return updatedTask.getStatus() == Task.TaskStatus.COMPLETED; + } + public Parameters callReplaceReferences(boolean theIsAsync) { return callReplaceReferencesWithBatchSize(theIsAsync, null); } @@ -266,4 +277,19 @@ public Parameters asParametersResource() { } } + public void validatePatchResultBundle(Bundle patchResultBundle, int theTotalExpectedPatches) { + Pattern expectedPatchIssuePattern = Pattern.compile("Successfully patched resource \"(Observation|Encounter|CarePlan)/\\d+/_history/\\d+\"."); + assertThat(patchResultBundle.getEntry()).hasSize(theTotalExpectedPatches) + .allSatisfy(entry -> + assertThat(entry.getResponse().getOutcome()) + .isInstanceOf(OperationOutcome.class) + .extracting(OperationOutcome.class::cast) + .extracting(OperationOutcome::getIssue) + .satisfies(issues -> + assertThat(issues).hasSize(1) + .element(0) + .extracting(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics) + .satisfies(diagnostics -> assertThat(diagnostics).matches(expectedPatchIssuePattern)))); + } + } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 7cad1590bb4e..b83b4bd87df0 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -13,7 +13,6 @@ import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Parameters; @@ -29,16 +28,12 @@ import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.ValueSource; -import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.regex.Pattern; import java.util.stream.Collectors; -import static ca.uhn.fhir.jpa.dao.r4.replacereferences.ReplaceReferencesTestHelper.EXPECTED_SMALL_BATCHES; import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER_RESPOND_ASYNC; @@ -48,8 +43,6 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_RESULT_PATIENT; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; @@ -106,7 +99,7 @@ public void before() throws Exception { "false, true, false, true", "false, false, false, true", }) - public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPatient, boolean withPreview, boolean isAsync) throws Exception { + public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPatient, boolean withPreview, boolean isAsync) { // setup ReplaceReferencesTestHelper.PatientMergeInputParameters inParams = new ReplaceReferencesTestHelper.PatientMergeInputParameters(); @@ -143,7 +136,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa Task task = (Task) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource(); assertNull(task.getIdElement().getVersionIdPart()); ourLog.info("Got task {}", task.getId()); - await().until(() -> taskCompleted(task.getIdElement())); + await().until(() -> myTestHelper.taskCompleted(task.getIdElement())); Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); @@ -167,7 +160,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa Reference outputRef = (Reference) taskOutput.getValue(); Bundle patchResultBundle = (Bundle) outputRef.getResource(); assertTrue(containedBundle.equalsDeep(patchResultBundle)); - validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES); + myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES); } else { // Synchronous case // Assert outcome OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); @@ -238,11 +231,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa } } - private Boolean taskCompleted(IdType theTaskId) { - Task updatedTask = myTaskDao.read(theTaskId, mySrd); - ourLog.info("Task {} status is {}", theTaskId, updatedTask.getStatus()); - return updatedTask.getStatus() == Task.TaskStatus.COMPLETED; - } + @ParameterizedTest @CsvSource({ @@ -322,137 +311,6 @@ void test_MissingRequiredParameters_Returns400BadRequest() { .isEqualTo(400); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testReplaceReferences(boolean isAsync) throws IOException { - // exec - Parameters outParams = myTestHelper.callReplaceReferences(isAsync); - - assertThat(outParams.getParameter()).hasSize(1); - - Bundle patchResultBundle; - if (isAsync) { - Task task = (Task) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).getResource(); - assertNull(task.getIdElement().getVersionIdPart()); - ourLog.info("Got task {}", task.getId()); - await().until(() -> taskCompleted(task.getIdElement())); - - Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); - ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); - - Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); - - // Assert on the output type - Coding taskType = taskOutput.getType().getCodingFirstRep(); - assertEquals(RESOURCE_TYPES_SYSTEM, taskType.getSystem()); - assertEquals("Bundle", taskType.getCode()); - - List containedResources = taskWithOutput.getContained(); - assertThat(containedResources) - .hasSize(1) - .element(0) - .isInstanceOf(Bundle.class); - - Bundle containedBundle = (Bundle) containedResources.get(0); - - Reference outputRef = (Reference) taskOutput.getValue(); - patchResultBundle = (Bundle) outputRef.getResource(); - assertTrue(containedBundle.equalsDeep(patchResultBundle)); - } else { - patchResultBundle = (Bundle) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME).getResource(); - } - - // validate - validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES); - - // Check that the linked resources were updated - - validateLinksUsingEverything(); - } - - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { - // exec - Parameters outParams = myTestHelper.callReplaceReferencesWithBatchSize(isAsync, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); - - - assertThat(outParams.getParameter()).hasSize(1); - - Bundle patchResultBundle; - Task task = (Task) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).getResource(); - assertNull(task.getIdElement().getVersionIdPart()); - ourLog.info("Got task {}", task.getId()); - await().until(() -> taskCompleted(task.getIdElement())); - - Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); - ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); - - assertThat(taskWithOutput.getOutput()).hasSize(EXPECTED_SMALL_BATCHES); - List containedResources = taskWithOutput.getContained(); - - assertThat(containedResources) - .hasSize(EXPECTED_SMALL_BATCHES) - .element(0) - .isInstanceOf(Bundle.class); - - int entriesLeft = ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES; - for (int i = 1; i < EXPECTED_SMALL_BATCHES; i++) { - - Task.TaskOutputComponent taskOutput = taskWithOutput.getOutput().get(i); - - // Assert on the output type - Coding taskType = taskOutput.getType().getCodingFirstRep(); - assertEquals(RESOURCE_TYPES_SYSTEM, taskType.getSystem()); - assertEquals("Bundle", taskType.getCode()); - - Bundle containedBundle = (Bundle) containedResources.get(i); - - Reference outputRef = (Reference) taskOutput.getValue(); - patchResultBundle = (Bundle) outputRef.getResource(); - assertTrue(containedBundle.equalsDeep(patchResultBundle)); - - // validate - entriesLeft -= ReplaceReferencesTestHelper.SMALL_BATCH_SIZE; - int expectedNumberOfEntries = Math.min(entriesLeft, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); - validatePatchResultBundle(patchResultBundle, expectedNumberOfEntries); - } - - // Check that the linked resources were updated - - validateLinksUsingEverything(); - } - - private void validateLinksUsingEverything() throws IOException { - Bundle everythingBundle = myTestHelper.getTargetEverythingBundle(); - - assertNull(everythingBundle.getLink("next")); - - Set actual = new HashSet<>(); - for (BundleEntryComponent nextEntry : everythingBundle.getEntry()) { - actual.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless()); - } - - ourLog.info("Found IDs: {}", actual); - - myTestHelper.assertContainsAllResources(actual, false); - } - - private static void validatePatchResultBundle(Bundle patchResultBundle, int theTotalExpectedPatches) { - Pattern expectedPatchIssuePattern = Pattern.compile("Successfully patched resource \"(Observation|Encounter|CarePlan)/\\d+/_history/\\d+\"."); - assertThat(patchResultBundle.getEntry()).hasSize(theTotalExpectedPatches) - .allSatisfy(entry -> - assertThat(entry.getResponse().getOutcome()) - .isInstanceOf(OperationOutcome.class) - .extracting(OperationOutcome.class::cast) - .extracting(OperationOutcome::getIssue) - .satisfies(issues -> - assertThat(issues).hasSize(1) - .element(0) - .extracting(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics) - .satisfies(diagnostics -> assertThat(diagnostics).matches(expectedPatchIssuePattern)))); - } - // FIXME KHS look at PatientEverythingR4Test for ideas for other tests class MyExceptionHandler implements TestExecutionExceptionHandler { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java new file mode 100644 index 000000000000..7db29601e97b --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -0,0 +1,160 @@ +package ca.uhn.fhir.jpa.provider.r4; + +import ca.uhn.fhir.jpa.dao.r4.replacereferences.ReplaceReferencesTestHelper; +import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; +import ca.uhn.fhir.parser.StrictErrorHandler; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.Task; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static ca.uhn.fhir.jpa.dao.r4.replacereferences.ReplaceReferencesTestHelper.EXPECTED_SMALL_BATCHES; +import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ReplaceReferencesR4Test extends BaseResourceProviderR4Test { + ReplaceReferencesTestHelper myTestHelper; + + @Override + @BeforeEach + public void before() throws Exception { + super.before(); + + myTestHelper = new ReplaceReferencesTestHelper(myFhirContext, myClient, myDaoRegistry); + myTestHelper.beforeEach(); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void testReplaceReferences(boolean isAsync) throws IOException { + // exec + Parameters outParams = myTestHelper.callReplaceReferences(isAsync); + + assertThat(outParams.getParameter()).hasSize(1); + + Bundle patchResultBundle; + if (isAsync) { + Task task = (Task) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).getResource(); + assertNull(task.getIdElement().getVersionIdPart()); + ourLog.info("Got task {}", task.getId()); + await().until(() -> myTestHelper.taskCompleted(task.getIdElement())); + + Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); + ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); + + Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); + + // Assert on the output type + Coding taskType = taskOutput.getType().getCodingFirstRep(); + assertEquals(RESOURCE_TYPES_SYSTEM, taskType.getSystem()); + assertEquals("Bundle", taskType.getCode()); + + List containedResources = taskWithOutput.getContained(); + assertThat(containedResources) + .hasSize(1) + .element(0) + .isInstanceOf(Bundle.class); + + Bundle containedBundle = (Bundle) containedResources.get(0); + + Reference outputRef = (Reference) taskOutput.getValue(); + patchResultBundle = (Bundle) outputRef.getResource(); + assertTrue(containedBundle.equalsDeep(patchResultBundle)); + } else { + patchResultBundle = (Bundle) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME).getResource(); + } + + // validate + myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES); + + // Check that the linked resources were updated + + validateLinksUsingEverything(); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { + // exec + Parameters outParams = myTestHelper.callReplaceReferencesWithBatchSize(isAsync, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); + + + assertThat(outParams.getParameter()).hasSize(1); + + Bundle patchResultBundle; + Task task = (Task) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).getResource(); + assertNull(task.getIdElement().getVersionIdPart()); + ourLog.info("Got task {}", task.getId()); + await().until(() -> myTestHelper.taskCompleted(task.getIdElement())); + + Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); + ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); + + assertThat(taskWithOutput.getOutput()).hasSize(EXPECTED_SMALL_BATCHES); + List containedResources = taskWithOutput.getContained(); + + assertThat(containedResources) + .hasSize(EXPECTED_SMALL_BATCHES) + .element(0) + .isInstanceOf(Bundle.class); + + int entriesLeft = ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES; + for (int i = 1; i < EXPECTED_SMALL_BATCHES; i++) { + + Task.TaskOutputComponent taskOutput = taskWithOutput.getOutput().get(i); + + // Assert on the output type + Coding taskType = taskOutput.getType().getCodingFirstRep(); + assertEquals(RESOURCE_TYPES_SYSTEM, taskType.getSystem()); + assertEquals("Bundle", taskType.getCode()); + + Bundle containedBundle = (Bundle) containedResources.get(i); + + Reference outputRef = (Reference) taskOutput.getValue(); + patchResultBundle = (Bundle) outputRef.getResource(); + assertTrue(containedBundle.equalsDeep(patchResultBundle)); + + // validate + entriesLeft -= ReplaceReferencesTestHelper.SMALL_BATCH_SIZE; + int expectedNumberOfEntries = Math.min(entriesLeft, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); + myTestHelper.validatePatchResultBundle(patchResultBundle, expectedNumberOfEntries); + } + + // Check that the linked resources were updated + + validateLinksUsingEverything(); + } + + + private void validateLinksUsingEverything() { + Bundle everythingBundle = myTestHelper.getTargetEverythingBundle(); + + assertNull(everythingBundle.getLink("next")); + + Set actual = new HashSet<>(); + for (Bundle.BundleEntryComponent nextEntry : everythingBundle.getEntry()) { + actual.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless()); + } + + ourLog.info("Found IDs: {}", actual); + + myTestHelper.assertContainsAllResources(actual, false); + } +} From f2a3cd272bff1cb81431d6c9838f67c8ce8d7477 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 9 Dec 2024 21:14:08 -0500 Subject: [PATCH 057/148] switch helper to use dao --- .../jpa/provider/r4/PatientMergeR4Test.java | 17 +---- .../provider/r4/ReplaceReferencesR4Test.java | 21 ++---- .../ReplaceReferencesBatchTest.java | 17 ++++- .../ReplaceReferencesTestHelper.java | 70 ++++++++++++------- 4 files changed, 68 insertions(+), 57 deletions(-) rename hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/{dao/r4 => }/replacereferences/ReplaceReferencesBatchTest.java (51%) rename hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/{dao/r4 => }/replacereferences/ReplaceReferencesTestHelper.java (79%) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index b83b4bd87df0..7b9678587459 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -1,8 +1,8 @@ package ca.uhn.fhir.jpa.provider.r4; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; -import ca.uhn.fhir.jpa.dao.r4.replacereferences.ReplaceReferencesTestHelper; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; +import ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper; import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInput; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; @@ -11,7 +11,6 @@ import jakarta.annotation.Nonnull; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.OperationOutcome; @@ -29,7 +28,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -74,7 +72,7 @@ public void before() throws Exception { myStorageSettings.setAllowMultipleDelete(true); myFhirContext.setParserErrorHandler(new StrictErrorHandler()); - myTestHelper = new ReplaceReferencesTestHelper(myFhirContext, myClient, myDaoRegistry); + myTestHelper = new ReplaceReferencesTestHelper(myFhirContext, myDaoRegistry); myTestHelper.beforeEach(); } @@ -213,14 +211,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa // Check that the linked resources were updated - Bundle bundle = myTestHelper.getTargetEverythingBundle(); - - assertNull(bundle.getLink("next")); - - Set actual = new HashSet<>(); - for (BundleEntryComponent nextEntry : bundle.getEntry()) { - actual.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless()); - } + Set actual = myTestHelper.getTargetEverythingResourceIds(); ourLog.info("Found IDs: {}", actual); @@ -231,8 +222,6 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa } } - - @ParameterizedTest @CsvSource({ // withDelete, withInputResultPatient, withPreview diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index 7db29601e97b..f9735ef7e84a 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -1,8 +1,7 @@ package ca.uhn.fhir.jpa.provider.r4; -import ca.uhn.fhir.jpa.dao.r4.replacereferences.ReplaceReferencesTestHelper; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; -import ca.uhn.fhir.parser.StrictErrorHandler; +import ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; @@ -15,12 +14,11 @@ import org.junit.jupiter.params.provider.ValueSource; import java.io.IOException; -import java.util.HashSet; import java.util.List; import java.util.Set; -import static ca.uhn.fhir.jpa.dao.r4.replacereferences.ReplaceReferencesTestHelper.EXPECTED_SMALL_BATCHES; import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; +import static ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper.EXPECTED_SMALL_BATCHES; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; import static org.assertj.core.api.Assertions.assertThat; @@ -37,7 +35,7 @@ public class ReplaceReferencesR4Test extends BaseResourceProviderR4Test { public void before() throws Exception { super.before(); - myTestHelper = new ReplaceReferencesTestHelper(myFhirContext, myClient, myDaoRegistry); + myTestHelper = new ReplaceReferencesTestHelper(myFhirContext, myDaoRegistry); myTestHelper.beforeEach(); } @@ -45,7 +43,7 @@ public void before() throws Exception { @ValueSource(booleans = {false, true}) void testReplaceReferences(boolean isAsync) throws IOException { // exec - Parameters outParams = myTestHelper.callReplaceReferences(isAsync); + Parameters outParams = myTestHelper.callReplaceReferences(myClient, isAsync); assertThat(outParams.getParameter()).hasSize(1); @@ -93,7 +91,7 @@ void testReplaceReferences(boolean isAsync) throws IOException { @ValueSource(booleans = {false, true}) void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { // exec - Parameters outParams = myTestHelper.callReplaceReferencesWithBatchSize(isAsync, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); + Parameters outParams = myTestHelper.callReplaceReferencesWithBatchSize(myClient, isAsync, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); assertThat(outParams.getParameter()).hasSize(1); @@ -144,14 +142,7 @@ void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { private void validateLinksUsingEverything() { - Bundle everythingBundle = myTestHelper.getTargetEverythingBundle(); - - assertNull(everythingBundle.getLink("next")); - - Set actual = new HashSet<>(); - for (Bundle.BundleEntryComponent nextEntry : everythingBundle.getEntry()) { - actual.add(nextEntry.getResource().getIdElement().toUnqualifiedVersionless()); - } + Set actual = myTestHelper.getTargetEverythingResourceIds(); ourLog.info("Found IDs: {}", actual); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java similarity index 51% rename from hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesBatchTest.java rename to hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java index 1d027e50a3a6..d012d904edc3 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java @@ -1,8 +1,10 @@ -package ca.uhn.fhir.jpa.dao.r4.replacereferences; +package ca.uhn.fhir.jpa.replacereferences; import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.api.IJobPersistence; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.test.BaseJpaR4Test; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -12,6 +14,19 @@ public class ReplaceReferencesBatchTest extends BaseJpaR4Test { private IJobCoordinator myJobCoordinator; @Autowired private IJobPersistence myJobPersistence; + @Autowired + private DaoRegistry myDaoRegistry; + + private ReplaceReferencesTestHelper myTestHelper; + + @Override + @BeforeEach + public void before() throws Exception { + super.before(); + + myTestHelper = new ReplaceReferencesTestHelper(myFhirContext, myDaoRegistry); + myTestHelper.beforeEach(); + } @Test public void testSimple() { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java similarity index 79% rename from hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesTestHelper.java rename to hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index f898393603b2..32e69285c840 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -1,12 +1,17 @@ -package ca.uhn.fhir.jpa.dao.r4.replacereferences; +package ca.uhn.fhir.jpa.replacereferences; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 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.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.server.provider.ProviderConstants; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Bundle; @@ -24,18 +29,19 @@ import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.Type; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Set; import java.util.regex.Pattern; +import java.util.stream.Collectors; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER_RESPOND_ASYNC; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNull; public class ReplaceReferencesTestHelper { private static final Logger ourLog = LoggerFactory.getLogger(ReplaceReferencesTestHelper.class); @@ -48,8 +54,12 @@ public class ReplaceReferencesTestHelper { public static final int TOTAL_EXPECTED_PATCHES = 23; public static final int SMALL_BATCH_SIZE = 5; public static final int EXPECTED_SMALL_BATCHES = (TOTAL_EXPECTED_PATCHES + SMALL_BATCH_SIZE - 1) / SMALL_BATCH_SIZE; - private final IFhirResourceDao myPatientDao; + private final IFhirResourceDaoPatient myPatientDao; private final IFhirResourceDao myTaskDao; + private final IFhirResourceDao myOrganizationDao; + private final IFhirResourceDao myEncounterDao; + private final IFhirResourceDao myCarePlanDao; + private final IFhirResourceDao myObservationDao; IIdType myOrgId; IIdType mySourcePatientId; @@ -62,21 +72,23 @@ public class ReplaceReferencesTestHelper { Patient myResultPatient; private final FhirContext myFhirContext; - private final IGenericClient myFhirClient; private final SystemRequestDetails mySrd = new SystemRequestDetails(); - public ReplaceReferencesTestHelper(FhirContext theFhirContext, IGenericClient theFhirClient, DaoRegistry theDaoRegistry) { + public ReplaceReferencesTestHelper(FhirContext theFhirContext, DaoRegistry theDaoRegistry) { myFhirContext = theFhirContext; - myFhirClient = theFhirClient; - myPatientDao = theDaoRegistry.getResourceDao(Patient.class); + myPatientDao = (IFhirResourceDaoPatient) theDaoRegistry.getResourceDao(Patient.class); myTaskDao = theDaoRegistry.getResourceDao(Task.class); + myOrganizationDao = theDaoRegistry.getResourceDao(Organization.class); + myEncounterDao = theDaoRegistry.getResourceDao(Encounter.class); + myCarePlanDao = theDaoRegistry.getResourceDao(CarePlan.class); + myObservationDao = theDaoRegistry.getResourceDao(Observation.class); } public void beforeEach() throws Exception { Organization org = new Organization(); org.setName("an org"); - myOrgId = myFhirClient.create().resource(org).execute().getId().toUnqualifiedVersionless(); + myOrgId = myOrganizationDao.create(org, mySrd).getId().toUnqualifiedVersionless(); ourLog.info("OrgId: {}", myOrgId); Patient patient1 = new Patient(); @@ -84,44 +96,44 @@ public void beforeEach() throws Exception { patient1.addIdentifier(pat1IdentifierA); patient1.addIdentifier(pat1IdentifierB); patient1.addIdentifier(patBothIdentifierC); - mySourcePatientId = myFhirClient.create().resource(patient1).execute().getId().toUnqualifiedVersionless(); + mySourcePatientId = myPatientDao.create(patient1, mySrd).getId().toUnqualifiedVersionless(); Patient patient2 = new Patient(); patient2.addIdentifier(pat2IdentifierA); patient2.addIdentifier(pat2IdentifierB); patient2.addIdentifier(patBothIdentifierC); patient2.getManagingOrganization().setReferenceElement(myOrgId); - myTargetPatientId = myFhirClient.create().resource(patient2).execute().getId().toUnqualifiedVersionless(); + myTargetPatientId = myPatientDao.create(patient2, mySrd).getId().toUnqualifiedVersionless(); Encounter enc1 = new Encounter(); enc1.setStatus(Encounter.EncounterStatus.CANCELLED); enc1.getSubject().setReferenceElement(mySourcePatientId); enc1.getServiceProvider().setReferenceElement(myOrgId); - mySourceEncId1 = myFhirClient.create().resource(enc1).execute().getId().toUnqualifiedVersionless(); + mySourceEncId1 = myEncounterDao.create(enc1, mySrd).getId().toUnqualifiedVersionless(); Encounter enc2 = new Encounter(); enc2.setStatus(Encounter.EncounterStatus.ARRIVED); enc2.getSubject().setReferenceElement(mySourcePatientId); enc2.getServiceProvider().setReferenceElement(myOrgId); - mySourceEncId2 = myFhirClient.create().resource(enc2).execute().getId().toUnqualifiedVersionless(); + mySourceEncId2 = myEncounterDao.create(enc2, mySrd).getId().toUnqualifiedVersionless(); CarePlan carePlan = new CarePlan(); carePlan.setStatus(CarePlan.CarePlanStatus.ACTIVE); carePlan.getSubject().setReferenceElement(mySourcePatientId); - mySourceCarePlanId = myFhirClient.create().resource(carePlan).execute().getId().toUnqualifiedVersionless(); + mySourceCarePlanId = myCarePlanDao.create(carePlan, mySrd).getId().toUnqualifiedVersionless(); Encounter targetEnc1 = new Encounter(); targetEnc1.setStatus(Encounter.EncounterStatus.ARRIVED); targetEnc1.getSubject().setReferenceElement(myTargetPatientId); targetEnc1.getServiceProvider().setReferenceElement(myOrgId); - this.myTargetEnc1 = myFhirClient.create().resource(targetEnc1).execute().getId().toUnqualifiedVersionless(); + this.myTargetEnc1 = myEncounterDao.create(targetEnc1, mySrd).getId().toUnqualifiedVersionless(); mySourceObsIds = new ArrayList<>(); for (int i = 0; i < 20; i++) { Observation obs = new Observation(); obs.getSubject().setReferenceElement(mySourcePatientId); obs.setStatus(Observation.ObservationStatus.FINAL); - IIdType obsId = myFhirClient.create().resource(obs).execute().getId().toUnqualifiedVersionless(); + IIdType obsId = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); mySourceObsIds.add(obsId); } @@ -150,14 +162,18 @@ public Object getTargetPatientId() { return myTargetPatientId; } - public Bundle getTargetEverythingBundle() { - return myFhirClient.operation() - .onInstance(myTargetPatientId) - .named("$everything") - .withParameter(Parameters.class, "_count", new IntegerType(100)) - .useHttpGet() - .returnResourceType(Bundle.class) - .execute(); + public Set getTargetEverythingResourceIds() { + PatientEverythingParameters everythingParams = new PatientEverythingParameters(); + everythingParams.setCount(new IntegerType(100)); + + IBundleProvider bundleProvider = myPatientDao.patientInstanceEverything(null, mySrd, everythingParams, myTargetPatientId); + + assertNull(bundleProvider.getNextPageId()); + + return bundleProvider.getAllResources().stream() + .map(IBaseResource::getIdElement) + .map(IIdType::toUnqualifiedVersionless) + .collect(Collectors.toSet()); } public Boolean taskCompleted(IdType theTaskId) { @@ -166,12 +182,12 @@ public Boolean taskCompleted(IdType theTaskId) { return updatedTask.getStatus() == Task.TaskStatus.COMPLETED; } - public Parameters callReplaceReferences(boolean theIsAsync) { - return callReplaceReferencesWithBatchSize(theIsAsync, null); + public Parameters callReplaceReferences(IGenericClient theFhirClient, boolean theIsAsync) { + return callReplaceReferencesWithBatchSize(theFhirClient, theIsAsync, null); } - public Parameters callReplaceReferencesWithBatchSize(boolean theIsAsync, Integer theBatchSize) { - IOperationUntypedWithInputAndPartialOutput request = myFhirClient.operation() + public Parameters callReplaceReferencesWithBatchSize(IGenericClient theFhirClient, boolean theIsAsync, Integer theBatchSize) { + IOperationUntypedWithInputAndPartialOutput request = theFhirClient.operation() .onServer() .named(OPERATION_REPLACE_REFERENCES) .withParameter(Parameters.class, ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID, new StringType(mySourcePatientId.getValue())) From daeee3970c0aaf04e9fdf86b9320d1ba05309c03 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Tue, 10 Dec 2024 10:30:50 -0500 Subject: [PATCH 058/148] incresed unit test coverage --- .../jpa/dao/merge/MergeOperationOutcome.java | 19 ++ .../jpa/dao/merge/ResourceMergeService.java | 7 +- .../dao/merge/ResourceMergeServiceTest.java | 258 ++++++++++++++---- 3 files changed, 225 insertions(+), 59 deletions(-) diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java index 53e1bcaa92d9..f043eeeae116 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR Storage api + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.jpa.dao.merge; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 205032e52eee..51dcde7024fc 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -278,8 +278,7 @@ private List getLinksToResource( Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { List links = getLinksOfType(theResource, theLinkType); return links.stream() - .filter(r -> r.getReference() != null - && r.getReference().equals(theResourceId.toVersionless().getValue())) + .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) .collect(Collectors.toList()); } @@ -380,7 +379,7 @@ private void prepareSourceResourceForUpdate(Patient theSourceResource, Patient t theSourceResource .addLink() .setType(Patient.LinkType.REPLACEDBY) - .setOther(new Reference(theTargetResource.getId())); + .setOther(new Reference(theTargetResource.getIdElement().toVersionless())); } private Patient prepareTargetPatientForUpdate( @@ -400,7 +399,7 @@ private Patient prepareTargetPatientForUpdate( theTargetResource .addLink() .setType(Patient.LinkType.REPLACES) - .setOther(new Reference(theSourceResource.getId())); + .setOther(new Reference(theSourceResource.getIdElement().toVersionless())); } // copy all identifiers from the source to the target diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index ee1e7facae0e..20095cbec27f 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -12,8 +12,10 @@ import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.CanonicalIdentifier; +import net.sourceforge.plantuml.bpm.Col; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; @@ -75,6 +77,10 @@ public class ResourceMergeServiceTest { private final FhirContext myFhirContext = FhirContext.forR4Cached(); + private Patient myCapturedSourcePatientForUpdate; + + private Patient myCapturedTargetPatientForUpdate; + @BeforeEach void setup() { when(myDaoMock.getContext()).thenReturn(myFhirContext); @@ -89,26 +95,58 @@ void testMerge_WithoutResultResource_Success() { mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + + //the identifiers should be copied from the source to the target, without creating duplicates on the target + sourcePatient.addIdentifier(new Identifier().setSystem("sysA").setValue("val1")); + sourcePatient.addIdentifier(new Identifier().setSystem("sysB").setValue("val2")); + sourcePatient.addIdentifier(new Identifier().setSystem("sysC").setValue("val3")); Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + targetPatient.addIdentifier(new Identifier().setSystem("sysC").setValue("val3")); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); setupDaoMockForSuccessfulSourcePatientUpdate(sourcePatient, new Patient()); Patient patientReturnedFromDaoAfterTargetUpdate = new Patient(); - setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientReturnedFromDaoAfterTargetUpdate, true); + setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientReturnedFromDaoAfterTargetUpdate); setupTransactionServiceMock(); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then - OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); - assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(200); - assertThat(mergeOutcome.getUpdatedTargetResource()).isEqualTo(patientReturnedFromDaoAfterTargetUpdate); - assertThat(operationOutcome.getIssue()).hasSize(1); - OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); - assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - assertThat(issue.getDetails().getText()).contains(SUCCESSFUL_MERGE_MSG); + verifySuccessfulOutcome(mergeOutcome, patientReturnedFromDaoAfterTargetUpdate); + verifyUpdatedSourcePatient(); + List expectedIdentifiers = List.of( + new Identifier().setSystem("sysC").setValue("val3"), + new Identifier().setSystem("sysA").setValue("val1"), + new Identifier().setSystem("sysB").setValue("val2")); + verifyUpdatedTargetPatient(true, expectedIdentifiers); + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_WithoutResultResource_TargetSetToActiveExplicitly_Success() { + // Given + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + targetPatient.setActive(true); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + setupDaoMockForSuccessfulSourcePatientUpdate(sourcePatient, new Patient()); + Patient patientReturnedFromDaoAfterTargetUpdate = new Patient(); + setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientReturnedFromDaoAfterTargetUpdate); + setupTransactionServiceMock(); + + // When + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + verifySuccessfulOutcome(mergeOutcome, patientReturnedFromDaoAfterTargetUpdate); + verifyUpdatedSourcePatient(); + verifyUpdatedTargetPatient(true, Collections.emptyList()); verifyNoMoreInteractions(myDaoMock); } @@ -129,20 +167,55 @@ void testMerge_WithResultResource_Success() { setupDaoMockForSuccessfulSourcePatientUpdate(sourcePatient, new Patient()); Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); - setupDaoMockForSuccessfulTargetPatientUpdate(resultPatient, patientToBeReturnedFromDaoAfterTargetUpdate, true); + setupDaoMockForSuccessfulTargetPatientUpdate(resultPatient, patientToBeReturnedFromDaoAfterTargetUpdate); setupTransactionServiceMock(); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then - OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); - assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(200); - assertThat(mergeOutcome.getUpdatedTargetResource()).isEqualTo(patientToBeReturnedFromDaoAfterTargetUpdate); - assertThat(operationOutcome.getIssue()).hasSize(1); - OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); - assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - assertThat(issue.getDetails().getText()).contains(SUCCESSFUL_MERGE_MSG); + verifySuccessfulOutcome(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); + verifyUpdatedSourcePatient(); + verifyUpdatedTargetPatient(true, Collections.emptyList()); + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_WithResultResource_ResultHasAllTargetIdentifiers_Success() { + // Given + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResourceIdentifiers(List.of( + new CanonicalIdentifier().setSystem("sys").setValue("val1"), + new CanonicalIdentifier().setSystem("sys").setValue("val2") + )); + Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); + resultPatient.addLink().setType(Patient.LinkType.REPLACES).setOther(new Reference(SOURCE_PATIENT_TEST_ID)); + resultPatient.addIdentifier().setSystem("sys").setValue("val1"); + resultPatient.addIdentifier().setSystem("sys").setValue("val2"); + mergeOperationParameters.setResultResource(resultPatient); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockSearchForIdentifiers(List.of(List.of(targetPatient))); + + setupDaoMockForSuccessfulSourcePatientUpdate(sourcePatient, new Patient()); + Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); + setupDaoMockForSuccessfulTargetPatientUpdate(resultPatient, patientToBeReturnedFromDaoAfterTargetUpdate); + setupTransactionServiceMock(); + + // When + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + verifySuccessfulOutcome(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); + verifyUpdatedSourcePatient(); + List expectedIdentifiers = List.of( + new Identifier().setSystem("sys").setValue("val1"), + new Identifier().setSystem("sys").setValue("val2") + ); + verifyUpdatedTargetPatient(true, expectedIdentifiers); verifyNoMoreInteractions(myDaoMock); } @@ -160,21 +233,44 @@ void testMerge_WithDeleteSourceTrue_Success() { when(myDaoMock.delete(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenReturn(new DaoMethodOutcome()); Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); - setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientToBeReturnedFromDaoAfterTargetUpdate, false); + setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientToBeReturnedFromDaoAfterTargetUpdate); setupTransactionServiceMock(); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then - OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); - assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(200); - assertThat(mergeOutcome.getUpdatedTargetResource()).isEqualTo(patientToBeReturnedFromDaoAfterTargetUpdate); - assertThat(operationOutcome.getIssue()).hasSize(1); - OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); - assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - assertThat(issue.getDetails().getText()).contains(SUCCESSFUL_MERGE_MSG); + verifySuccessfulOutcome(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); + verifyUpdatedTargetPatient(false, Collections.emptyList()); + verifyNoMoreInteractions(myDaoMock); + } + + @Test + void testMerge_WithDeleteSourceTrue_And_WithResultResource_Success() { + // Given + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setDeleteSource(true); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); + mergeOperationParameters.setResultResource(resultPatient); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + when(myDaoMock.delete(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenReturn(new DaoMethodOutcome()); + Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); + setupDaoMockForSuccessfulTargetPatientUpdate(resultPatient, patientToBeReturnedFromDaoAfterTargetUpdate); + setupTransactionServiceMock(); + + // When + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + verifySuccessfulOutcome(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); + verifyUpdatedTargetPatient(false, Collections.emptyList()); verifyNoMoreInteractions(myDaoMock); } @@ -219,20 +315,18 @@ void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersio Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_2); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); - when(myDaoMock.update(any(), eq(myRequestDetailsMock))).thenReturn(new DaoMethodOutcome()); + setupDaoMockForSuccessfulSourcePatientUpdate(sourcePatient, new Patient()); + Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); + setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientToBeReturnedFromDaoAfterTargetUpdate); + setupTransactionServiceMock(); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then - OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); - assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(200); - - assertThat(operationOutcome.getIssue()).hasSize(1); - OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); - assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - assertThat(issue.getDetails().getText()).contains(SUCCESSFUL_MERGE_MSG); - + verifySuccessfulOutcome(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); + verifyUpdatedSourcePatient(); + verifyUpdatedTargetPatient(true, Collections.emptyList()); verifyNoMoreInteractions(myDaoMock); } @@ -845,13 +939,14 @@ void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersPr MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of( - new CanonicalIdentifier().setSystem("sys").setValue("val1"), - new CanonicalIdentifier().setSystem("sys").setValue("val2") + new CanonicalIdentifier().setSystem("sysA").setValue("val1"), + new CanonicalIdentifier().setSystem("sysB").setValue("val2") )); // the result patient has only one of the identifiers that were provided in the target identifiers Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); - resultPatient.addIdentifier().setSystem("sys").setValue("val"); + resultPatient.addIdentifier().setSystem("sysA").setValue("val1"); + resultPatient.addIdentifier().setSystem("sysC").setValue("val2"); addReplacesLink(resultPatient, SOURCE_PATIENT_TEST_ID); mergeOperationParameters.setResultResource(resultPatient); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); @@ -876,7 +971,7 @@ void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersPr @Test - void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLink_ReturnsErrorWith400Status() { + void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLinkAtAll_ReturnsErrorWith400Status() { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); @@ -904,6 +999,36 @@ void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLink_ReturnsEr verifyNoMoreInteractions(myDaoMock); } + @Test + void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLinkToSource_ReturnsErrorWith400Status() { + // Given + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + + Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); + addReplacesLink(resultPatient, "Patient/not-the-source-id"); + + mergeOperationParameters.setResultResource(resultPatient); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_1); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + // When + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + // Then + OperationOutcome operationOutcome = (OperationOutcome) mergeOutcome.getOperationOutcome(); + assertThat(mergeOutcome.getHttpStatusCode()).isEqualTo(400); + + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); + assertThat(issue.getDiagnostics()).contains("'result-patient' must have a 'replaces' link to the source resource."); + + verifyNoMoreInteractions(myDaoMock); + } @Test void testMerge_ValidatesResultResource_ResultResourceHasReplacesLinkAndDeleteSourceIsTrue_ReturnsErrorWith400Status() { @@ -969,6 +1094,16 @@ void testMerge_ValidatesResultResource_ResultResourceHasRedundantReplacesLinksTo verifyNoMoreInteractions(myDaoMock); } + private void verifySuccessfulOutcome(MergeOperationOutcome theMergeOutcome, Patient theExpectedTargetResource) { + OperationOutcome operationOutcome = (OperationOutcome) theMergeOutcome.getOperationOutcome(); + assertThat(theMergeOutcome.getHttpStatusCode()).isEqualTo(200); + assertThat(theMergeOutcome.getUpdatedTargetResource()).isEqualTo(theExpectedTargetResource); + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(issue.getDetails().getText()).contains(SUCCESSFUL_MERGE_MSG); + } + private Patient createPatient(String theId) { Patient patient = new Patient(); patient.setId(theId); @@ -1022,6 +1157,11 @@ private void setupDaoMockSearchForIdentifiers(List> theMatch } } + private void verifyUpdatedSourcePatient() { + assertThat(myCapturedSourcePatientForUpdate.getLink()).hasSize(1); + assertThat(myCapturedSourcePatientForUpdate.getLinkFirstRep().getType()).isEqualTo(Patient.LinkType.REPLACEDBY); + assertThat(myCapturedSourcePatientForUpdate.getLinkFirstRep().getOther().getReference()).isEqualTo(TARGET_PATIENT_TEST_ID); + } private void setupDaoMockForSuccessfulSourcePatientUpdate(Patient thePatientExpectedAsInput, Patient thePatientToReturnInDaoOutcome) { @@ -1029,10 +1169,7 @@ private void setupDaoMockForSuccessfulSourcePatientUpdate(Patient thePatientExpe daoMethodOutcome.setResource(thePatientToReturnInDaoOutcome); when(myDaoMock.update(thePatientExpectedAsInput, myRequestDetailsMock)) .thenAnswer(t -> { - Patient capturedSourcePatient = t.getArgument(0); - assertThat(capturedSourcePatient.getLink()).hasSize(1); - assertThat(capturedSourcePatient.getLinkFirstRep().getType()).isEqualTo(Patient.LinkType.REPLACEDBY); - assertThat(capturedSourcePatient.getLinkFirstRep().getOther().getReference()).isEqualTo(TARGET_PATIENT_TEST_ID); + myCapturedSourcePatientForUpdate = t.getArgument(0); DaoMethodOutcome outcome = new DaoMethodOutcome(); outcome.setResource(thePatientToReturnInDaoOutcome); @@ -1040,26 +1177,37 @@ private void setupDaoMockForSuccessfulSourcePatientUpdate(Patient thePatientExpe }); } + private void verifyUpdatedTargetPatient(boolean theExpectLinkToSourcePatient, List theExpectedIdentifiers) { + if (theExpectLinkToSourcePatient) { + assertThat(myCapturedTargetPatientForUpdate.getLink()).hasSize(1); + assertThat(myCapturedTargetPatientForUpdate.getLinkFirstRep().getType()).isEqualTo(Patient.LinkType.REPLACES); + assertThat(myCapturedTargetPatientForUpdate.getLinkFirstRep().getOther().getReference()).isEqualTo(SOURCE_PATIENT_TEST_ID); + } + else { + assertThat(myCapturedTargetPatientForUpdate.getLink()).isEmpty(); + } + + + assertThat(myCapturedTargetPatientForUpdate.getIdentifier()).hasSize(theExpectedIdentifiers.size()); + for (int i = 0; i < theExpectedIdentifiers.size(); i++) { + Identifier expectedIdentifier = theExpectedIdentifiers.get(i); + Identifier actualIdentifier = myCapturedTargetPatientForUpdate.getIdentifier().get(i); + assertThat(expectedIdentifier.equalsDeep(actualIdentifier)).isTrue(); + } + + } + private void setupDaoMockForSuccessfulTargetPatientUpdate(Patient thePatientExpectedAsInput, - Patient thePatientToReturnInDaoOutcome, - boolean theExpectLinkToSourcePatient) { + Patient thePatientToReturnInDaoOutcome) { DaoMethodOutcome daoMethodOutcome = new DaoMethodOutcome(); daoMethodOutcome.setResource(thePatientToReturnInDaoOutcome); when(myDaoMock.update(thePatientExpectedAsInput, myRequestDetailsMock)) .thenAnswer(t -> { - Patient capturedTargetPatient = t.getArgument(0); - if (theExpectLinkToSourcePatient) { - assertThat(capturedTargetPatient.getLink()).hasSize(1); - assertThat(capturedTargetPatient.getLinkFirstRep().getType()).isEqualTo(Patient.LinkType.REPLACES); - assertThat(capturedTargetPatient.getLinkFirstRep().getOther().getReference()).isEqualTo(SOURCE_PATIENT_TEST_ID); - } - else { - assertThat(capturedTargetPatient.getLink()).isEmpty(); - } + myCapturedTargetPatientForUpdate = t.getArgument(0); DaoMethodOutcome outcome = new DaoMethodOutcome(); - outcome.setResource(thePatientToReturnInDaoOutcome); - return outcome; - }); + outcome.setResource(thePatientToReturnInDaoOutcome); + return outcome; + }); } private void verifySearchParametersOnDaoSearchInvocations(List> theExpectedIdentifierParams) { From ca97a8a0c9ce6b40b61d81b034a2b94ca68e37e9 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 10 Dec 2024 14:06:53 -0500 Subject: [PATCH 059/148] build out batch --- .../uhn/fhir/jpa/batch2/Batch2DaoSvcImpl.java | 21 +- .../fhir/jpa/config/Batch2SupportConfig.java | 3 +- .../fhir/jpa/dao/data/IResourceLinkDao.java | 4 +- .../BaseJpaResourceProviderPatient.java | 6 +- .../fhir/jpa/provider/JpaSystemProvider.java | 12 +- .../provider/ReplaceReferencesSvcImpl.java | 11 +- .../fhir/jpa/batch2/Batch2DaoSvcImplTest.java | 2 +- .../jpa/provider/r4/PatientMergeR4Test.java | 9 +- .../provider/r4/ReplaceReferencesR4Test.java | 15 +- .../ReplaceReferencesBatchTest.java | 33 +- .../ReplaceReferencesTestHelper.java | 52 +-- .../rest/api/server/SystemRequestDetails.java | 6 + .../ReplaceReferenceUpdateStep.java | 109 ++++++- .../ReplaceReferenceUpdateTaskStep.java | 4 +- .../ReplaceReferencesAppCtx.java | 18 +- .../ReplaceReferencesJobParameters.java | 67 +++- .../ReplaceReferencesQueryIdsStep.java | 49 ++- .../fhir/batch2/jobs/chunk/FhirIdJson.java | 94 ++++++ .../jobs/chunk/FhirIdListWorkChunkJson.java | 96 ++++++ .../jpa/api/config/JpaStorageSettings.java | 3 +- .../uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java | 5 + .../jpa/dao/merge/ResourceMergeService.java | 306 +++++++++--------- .../jpa/provider/ReplaceReferenceRequest.java | 6 +- .../dao/merge/ResourceMergeServiceTest.java | 8 +- 24 files changed, 711 insertions(+), 228 deletions(-) create mode 100644 hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdJson.java create mode 100644 hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdListWorkChunkJson.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/Batch2DaoSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/Batch2DaoSvcImpl.java index f6a67c305792..f3bb75dc410b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/Batch2DaoSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/Batch2DaoSvcImpl.java @@ -31,11 +31,13 @@ import ca.uhn.fhir.jpa.api.pid.TypedResourcePid; import ca.uhn.fhir.jpa.api.pid.TypedResourceStream; import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc; +import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.SortSpec; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; @@ -46,6 +48,7 @@ import jakarta.annotation.Nullable; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.IIdType; import java.util.Date; import java.util.function.Supplier; @@ -56,6 +59,8 @@ public class Batch2DaoSvcImpl implements IBatch2DaoSvc { private final IResourceTableDao myResourceTableDao; + private final IResourceLinkDao myResourceLinkDao; + private final MatchUrlService myMatchUrlService; private final DaoRegistry myDaoRegistry; @@ -70,12 +75,13 @@ public boolean isAllResourceTypeSupported() { } public Batch2DaoSvcImpl( - IResourceTableDao theResourceTableDao, - MatchUrlService theMatchUrlService, - DaoRegistry theDaoRegistry, - FhirContext theFhirContext, - IHapiTransactionService theTransactionService) { + IResourceTableDao theResourceTableDao, IResourceLinkDao theResourceLinkDao, + MatchUrlService theMatchUrlService, + DaoRegistry theDaoRegistry, + FhirContext theFhirContext, + IHapiTransactionService theTransactionService) { myResourceTableDao = theResourceTableDao; + myResourceLinkDao = theResourceLinkDao; myMatchUrlService = theMatchUrlService; myDaoRegistry = theDaoRegistry; myFhirContext = theFhirContext; @@ -95,6 +101,11 @@ public IResourcePidStream fetchResourceIdStream( } } + @Override + public Stream streamSourceIdsThatReferenceTargetId(IIdType theTargetId) { + return myResourceLinkDao.streamSourceIdsForTargetFhirId(theTargetId.getResourceType(), theTargetId.getIdPart()); + } + private Stream streamResourceIdsWithUrl( Date theStart, Date theEnd, String theUrl, RequestPartitionId theRequestPartitionId) { validateUrl(theUrl); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/Batch2SupportConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/Batch2SupportConfig.java index 83f308ba0db5..30ee9fe77853 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/Batch2SupportConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/Batch2SupportConfig.java @@ -43,12 +43,13 @@ public class Batch2SupportConfig { @Bean public IBatch2DaoSvc batch2DaoSvc( IResourceTableDao theResourceTableDao, + IResourceLinkDao theResourceLinkDao, MatchUrlService theMatchUrlService, DaoRegistry theDaoRegistry, FhirContext theFhirContext, IHapiTransactionService theTransactionService) { return new Batch2DaoSvcImpl( - theResourceTableDao, theMatchUrlService, theDaoRegistry, theFhirContext, theTransactionService); + theResourceTableDao, theResourceLinkDao, theMatchUrlService, theDaoRegistry, theFhirContext, theTransactionService); } @Bean diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java index fa9862a6f1af..809102e4ca94 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java @@ -50,13 +50,13 @@ public interface IResourceLinkDao extends JpaRepository, IHa @Query( "SELECT DISTINCT new ca.uhn.fhir.model.primitive.IdDt(t.mySourceResourceType, t.mySourceResource.myFhirId) FROM ResourceLink t WHERE t.myTargetResourceType = :resourceType AND t.myTargetResource.myFhirId = :resourceFhirId") - Stream streamSourceIdsForTargetPid( + Stream streamSourceIdsForTargetFhirId( @Param("resourceType") String theTargetResourceType, @Param("resourceFhirId") String theTargetResourceFhirId); @Query( "SELECT COUNT(DISTINCT t.mySourceResourcePid) FROM ResourceLink t WHERE t.myTargetResourceType = :resourceType AND t.myTargetResource.myFhirId = :resourceFhirId") - Integer countResourcesTargetingFhirTypeAndId( + Integer countResourcesTargetingFhirTypeAndFhirId( @Param("resourceType") String theTargetResourceType, @Param("resourceFhirId") String theTargetResourceFhirId); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 161cf1818617..b87747b905f8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -28,6 +28,7 @@ import ca.uhn.fhir.jpa.dao.merge.ResourceMergeService; import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.valueset.BundleTypeEnum; @@ -77,6 +78,9 @@ public abstract class BaseJpaResourceProviderPatient ex @Autowired private IHapiTransactionService myHapiTransactionService; + @Autowired + private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; + /** * Patient/123/$everything */ @@ -314,7 +318,7 @@ public IBaseParameters patientMerge( IFhirResourceDaoPatient dao = (IFhirResourceDaoPatient) getDao(); ResourceMergeService resourceMergeService = - new ResourceMergeService(dao, myReplaceReferencesSvc, myHapiTransactionService); + new ResourceMergeService(dao, myReplaceReferencesSvc, myHapiTransactionService, myRequestPartitionHelperSvc); FhirContext fhirContext = dao.getContext(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index 81af890b37a5..aeba4d5c7890 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -20,8 +20,11 @@ package ca.uhn.fhir.jpa.provider; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.annotation.Operation; @@ -36,6 +39,7 @@ import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.springframework.beans.factory.annotation.Autowired; import java.security.InvalidParameterException; import java.util.Collections; @@ -49,6 +53,8 @@ import static software.amazon.awssdk.utils.StringUtils.isBlank; public final class JpaSystemProvider extends BaseJpaSystemProvider { + @Autowired + private RequestPartitionHelperSvc myRequestPartitionHelperSvc; @Description( "Marks all currently existing resources of a given type, or all resources of all types, for reindexing.") @@ -167,8 +173,12 @@ public IBaseParameters replaceReferences( if (batchSize > myStorageSettings.getMaxTransactionEntriesForWrite()) { batchSize = myStorageSettings.getMaxTransactionEntriesForWrite(); } + + IdDt sourceId = new IdDt(theSourceId); + IdDt targetId = new IdDt(theTargetId); + RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, ReadPartitionIdRequestDetails.forRead(targetId)); ReplaceReferenceRequest replaceReferenceRequest = - new ReplaceReferenceRequest(new IdDt(theSourceId), new IdDt(theTargetId), batchSize); + new ReplaceReferenceRequest(sourceId, targetId, batchSize, partitionId); return getReplaceReferencesSvc().replaceReferences(replaceReferenceRequest, theRequestDetails); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index b58052a4c38d..4f5dfe90b67e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -110,7 +110,7 @@ public IBaseParameters replaceReferences( @Override public Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails) { return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { - return myResourceLinkDao.countResourcesTargetingFhirTypeAndId( + return myResourceLinkDao.countResourcesTargetingFhirTypeAndFhirId( theResourceId.getResourceType(), theResourceId.getIdPart()); }); } @@ -148,7 +148,7 @@ private void fakeBackgroundTaskUpdate( List pidList = myHapiTransactionService .withSystemRequestOnPartition(thePartitionId) .execute(() -> myResourceLinkDao - .streamSourceIdsForTargetPid( + .streamSourceIdsForTargetFhirId( theReplaceReferenceRequest.sourceId.getResourceType(), theReplaceReferenceRequest.sourceId.getIdPart()) .collect(Collectors.toUnmodifiableList())); @@ -211,6 +211,7 @@ private IBaseParameters replaceReferencesPreferSync( return retval; } + // FIXME KHS delete after convert to batch private Bundle patchReferencingResources( ReplaceReferenceRequest theReplaceReferenceRequest, List theFhirIdList, @@ -226,13 +227,14 @@ private Bundle patchReferencingResources( private @Nonnull StopLimitAccumulator getAllPidsWithLimit( ReplaceReferenceRequest theReplaceReferenceRequest) { - Stream idStream = myResourceLinkDao.streamSourceIdsForTargetPid( + Stream idStream = myResourceLinkDao.streamSourceIdsForTargetFhirId( theReplaceReferenceRequest.sourceId.getResourceType(), theReplaceReferenceRequest.sourceId.getIdPart()); StopLimitAccumulator accumulator = StopLimitAccumulator.fromStreamAndLimit(idStream, theReplaceReferenceRequest.batchSize); return accumulator; } + // FIXME KHS delete after convert to batch private Bundle buildPatchBundle( ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails, @@ -249,6 +251,7 @@ private Bundle buildPatchBundle( return bundleBuilder.getBundleTyped(); } + // FIXME KHS delete after convert to batch private @Nonnull Parameters buildPatchParams( ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { Parameters params = new Parameters(); @@ -264,6 +267,7 @@ private Bundle buildPatchBundle( return params; } + // FIXME KHS delete after convert to batch private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { return refInfo.getResourceReference() .getReferenceElement() @@ -272,6 +276,7 @@ private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceI .equals(theSourceId.getValueAsString()); } + // FIXME KHS delete after convert to batch @Nonnull private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( String thePath, Type theValue) { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/batch2/Batch2DaoSvcImplTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/batch2/Batch2DaoSvcImplTest.java index d34e1c030d48..93ef792ed469 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/batch2/Batch2DaoSvcImplTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/batch2/Batch2DaoSvcImplTest.java @@ -43,7 +43,7 @@ class Batch2DaoSvcImplTest extends BaseJpaR4Test { @BeforeEach void beforeEach() { - mySvc = new Batch2DaoSvcImpl(myResourceTableDao, myMatchUrlService, myDaoRegistry, myFhirContext, myIHapiTransactionService); + mySvc = new Batch2DaoSvcImpl(myResourceTableDao, myResourceLinkDao, myMatchUrlService, myDaoRegistry, myFhirContext, myIHapiTransactionService); } @Test diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 7b9678587459..5bc6e99cdfe2 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -9,7 +9,6 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import jakarta.annotation.Nonnull; -import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Identifier; @@ -29,7 +28,6 @@ import org.junit.jupiter.params.provider.CsvSource; import java.util.List; -import java.util.Set; import java.util.stream.Collectors; import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; @@ -211,14 +209,11 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa // Check that the linked resources were updated - Set actual = myTestHelper.getTargetEverythingResourceIds(); - - ourLog.info("Found IDs: {}", actual); if (withPreview) { - myTestHelper.assertNothingChanged(actual); + myTestHelper.assertNothingChanged(); } else { - myTestHelper.assertContainsAllResources(actual, withDelete); + myTestHelper.assertAllReferencesUpdated(withDelete); } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index f9735ef7e84a..dee13fcabd29 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -2,7 +2,6 @@ import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper; -import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Parameters; @@ -15,7 +14,6 @@ import java.io.IOException; import java.util.List; -import java.util.Set; import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; import static ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper.EXPECTED_SMALL_BATCHES; @@ -84,7 +82,7 @@ void testReplaceReferences(boolean isAsync) throws IOException { // Check that the linked resources were updated - validateLinksUsingEverything(); + myTestHelper.assertAllReferencesUpdated(); } @ParameterizedTest @@ -137,15 +135,6 @@ void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { // Check that the linked resources were updated - validateLinksUsingEverything(); - } - - - private void validateLinksUsingEverything() { - Set actual = myTestHelper.getTargetEverythingResourceIds(); - - ourLog.info("Found IDs: {}", actual); - - myTestHelper.assertContainsAllResources(actual, false); + myTestHelper.assertAllReferencesUpdated(); } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java index d012d904edc3..60e4584625cf 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java @@ -1,21 +1,32 @@ package ca.uhn.fhir.jpa.replacereferences; import ca.uhn.fhir.batch2.api.IJobCoordinator; -import ca.uhn.fhir.batch2.api.IJobPersistence; +import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; +import ca.uhn.fhir.batch2.model.JobInstance; +import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; import ca.uhn.fhir.jpa.test.BaseJpaR4Test; +import ca.uhn.fhir.jpa.test.Batch2JobHelper; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import static ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx.JOB_REPLACE_REFERENCES; + public class ReplaceReferencesBatchTest extends BaseJpaR4Test { @Autowired private IJobCoordinator myJobCoordinator; @Autowired - private IJobPersistence myJobPersistence; - @Autowired private DaoRegistry myDaoRegistry; + @Autowired + private Batch2JobHelper myBatch2JobHelper; + + SystemRequestDetails mySrd = new SystemRequestDetails(); private ReplaceReferencesTestHelper myTestHelper; @@ -26,10 +37,24 @@ public void before() throws Exception { myTestHelper = new ReplaceReferencesTestHelper(myFhirContext, myDaoRegistry); myTestHelper.beforeEach(); + + mySrd.setRequestPartitionId(RequestPartitionId.allPartitions()); } @Test - public void testSimple() { + public void testHappyPath() { + ReplaceReferencesJobParameters jobParams = new ReplaceReferencesJobParameters(); + jobParams.setSourceId(new FhirIdJson(myTestHelper.mySourcePatientId)); + jobParams.setTargetId(new FhirIdJson(myTestHelper.myTargetPatientId)); + + JobInstanceStartRequest request = new JobInstanceStartRequest(JOB_REPLACE_REFERENCES, jobParams); + Batch2JobStartResponse jobStartResponse = myJobCoordinator.startInstance(mySrd, request); + JobInstance jobInstance = myBatch2JobHelper.awaitJobCompletion(jobStartResponse); + + // FIXME KHS assert outcome + jobInstance.getReport(); + + myTestHelper.assertAllReferencesUpdated(); } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index 32e69285c840..69300a98e426 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -5,7 +5,6 @@ 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.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; @@ -162,7 +161,7 @@ public Object getTargetPatientId() { return myTargetPatientId; } - public Set getTargetEverythingResourceIds() { + private Set getTargetEverythingResourceIds() { PatientEverythingParameters everythingParams = new PatientEverythingParameters(); everythingParams.setCount(new IntegerType(100)); @@ -205,28 +204,41 @@ public Parameters callReplaceReferencesWithBatchSize(IGenericClient theFhirClien .execute(); } - public void assertContainsAllResources(Set theActual, boolean theWithDelete) { + public void assertAllReferencesUpdated() { + assertAllReferencesUpdated(false); + } + + public void assertAllReferencesUpdated(boolean theWithDelete) { + + Set actual = getTargetEverythingResourceIds(); + + ourLog.info("Found IDs: {}", actual); + if (theWithDelete) { - assertThat(theActual).doesNotContain(mySourcePatientId); + assertThat(actual).doesNotContain(mySourcePatientId); } - assertThat(theActual).contains(mySourceEncId1); - assertThat(theActual).contains(mySourceEncId2); - assertThat(theActual).contains(myOrgId); - assertThat(theActual).contains(mySourceCarePlanId); - assertThat(theActual).containsAll(mySourceObsIds); - assertThat(theActual).contains(myTargetPatientId); - assertThat(theActual).contains(myTargetEnc1); + assertThat(actual).contains(mySourceEncId1); + assertThat(actual).contains(mySourceEncId2); + assertThat(actual).contains(myOrgId); + assertThat(actual).contains(mySourceCarePlanId); + assertThat(actual).containsAll(mySourceObsIds); + assertThat(actual).contains(myTargetPatientId); + assertThat(actual).contains(myTargetEnc1); } - public void assertNothingChanged(Set theActual) { - assertThat(theActual).doesNotContain(mySourcePatientId); - assertThat(theActual).doesNotContain(mySourceEncId1); - assertThat(theActual).doesNotContain(mySourceEncId2); - assertThat(theActual).contains(myOrgId); - assertThat(theActual).doesNotContain(mySourceCarePlanId); - assertThat(theActual).doesNotContainAnyElementsOf(mySourceObsIds); - assertThat(theActual).contains(myTargetPatientId); - assertThat(theActual).contains(myTargetEnc1); + public void assertNothingChanged() { + Set actual = getTargetEverythingResourceIds(); + + ourLog.info("Found IDs: {}", actual); + + assertThat(actual).doesNotContain(mySourcePatientId); + assertThat(actual).doesNotContain(mySourceEncId1); + assertThat(actual).doesNotContain(mySourceEncId2); + assertThat(actual).contains(myOrgId); + assertThat(actual).doesNotContain(mySourceCarePlanId); + assertThat(actual).doesNotContainAnyElementsOf(mySourceObsIds); + assertThat(actual).contains(myTargetPatientId); + assertThat(actual).contains(myTargetEnc1); } public PatientMergeInputParameters buildMultipleTargetMatchParameters(boolean theWithDelete, boolean theWithInputResultPatient, boolean theWithPreview) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SystemRequestDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SystemRequestDetails.java index 46e14d53476a..12b2dd27448e 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SystemRequestDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SystemRequestDetails.java @@ -78,6 +78,12 @@ public SystemRequestDetails(RequestDetails theDetails) { } } + public static SystemRequestDetails forRequestPartitionId(RequestPartitionId thePartitionId) { + SystemRequestDetails retVal = new SystemRequestDetails(); + retVal.setRequestPartitionId(thePartitionId); + return retVal; + } + public RequestPartitionId getRequestPartitionId() { return myRequestPartitionId; } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java index fa29dae5b6a1..12f333f1f1b8 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java @@ -5,20 +5,121 @@ import ca.uhn.fhir.batch2.api.JobExecutionFailedException; import ca.uhn.fhir.batch2.api.RunOutcome; import ca.uhn.fhir.batch2.api.StepExecutionDetails; -import ca.uhn.fhir.batch2.jobs.chunk.ResourceIdListWorkChunkJson; +import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; +import ca.uhn.fhir.batch2.jobs.chunk.FhirIdListWorkChunkJson; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.util.BundleBuilder; +import ca.uhn.fhir.util.ResourceReferenceInfo; import jakarta.annotation.Nonnull; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.Meta; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Type; + +import java.util.UUID; + +import static ca.uhn.fhir.jpa.patch.FhirPatch.OPERATION_REPLACE; +import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_OPERATION; +import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_PATH; +import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_TYPE; +import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_VALUE; public class ReplaceReferenceUpdateStep implements IJobStepWorker< - ReplaceReferencesJobParameters, ResourceIdListWorkChunkJson, ReplaceReferenceResults> { + ReplaceReferencesJobParameters, FhirIdListWorkChunkJson, ReplaceReferenceResults> { + + private final FhirContext myFhirContext; + private final DaoRegistry myDaoRegistry; + + public ReplaceReferenceUpdateStep(FhirContext theFhirContext, DaoRegistry theDaoRegistry) { + myFhirContext = theFhirContext; + myDaoRegistry = theDaoRegistry; + } + @Nonnull @Override public RunOutcome run( @Nonnull - StepExecutionDetails + StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { - return null; + + ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); + FhirIdListWorkChunkJson theFhirIds = theStepExecutionDetails.getData(); + + SystemRequestDetails requestDetails = SystemRequestDetails.forRequestPartitionId(params.getPartitionId()); + Bundle patchBundle = buildPatchBundle(params, theFhirIds, requestDetails); + IFhirSystemDao systemDao = myDaoRegistry.getSystemDao(); + Bundle result = systemDao.transaction(requestDetails, patchBundle); + // TODO KHS shouldn't transaction response bundles have ids? + result.setId(UUID.randomUUID().toString()); + + RunOutcome retval = new RunOutcome(0); + return retval; } + + private Bundle buildPatchBundle( + ReplaceReferencesJobParameters theParams, + FhirIdListWorkChunkJson theFhirIds, RequestDetails theRequestDetails) { + BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); + + + theFhirIds.getFhirIds().stream() + .map(FhirIdJson::asIdDt) + .forEach(referencingResourceId -> { + IFhirResourceDao dao = myDaoRegistry.getResourceDao(referencingResourceId.getResourceType()); + IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); + Parameters patchParams = buildPatchParams(theParams, resource); + IIdType resourceId = resource.getIdElement(); + bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); + }); + return bundleBuilder.getBundleTyped(); + } + + private @Nonnull Parameters buildPatchParams( + ReplaceReferencesJobParameters theParams, IBaseResource referencingResource) { + Parameters params = new Parameters(); + + myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() + .filter(refInfo -> matches( + refInfo, + theParams.getSourceId().asIdDt())) // We only care about references to our source resource + .map(refInfo -> createReplaceReferencePatchOperation( + referencingResource.fhirType() + "." + refInfo.getName(), + new Reference(theParams.getTargetId().toString()))) + .forEach(params::addParameter); // Add each operation to parameters + return params; + } + + private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { + return refInfo.getResourceReference() + .getReferenceElement() + .toUnqualifiedVersionless() + .getValueAsString() + .equals(theSourceId.getValueAsString()); + } + + @Nonnull + private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( + String thePath, Type theValue) { + + Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); + operation.setName(PARAMETER_OPERATION); + operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE)); + operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath)); + operation.addPart().setName(PARAMETER_VALUE).setValue(theValue); + return operation; + } + } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java index 03164f73c3fc..de08cbaf096d 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java @@ -18,6 +18,8 @@ public RunOutcome run( theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { - return null; + // FIXME KHS + RunOutcome retval = new RunOutcome(0); + return retval; } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java index 23aee64477b9..89de6d0df998 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java @@ -1,13 +1,17 @@ package ca.uhn.fhir.batch2.jobs.replacereferences; -import ca.uhn.fhir.batch2.jobs.chunk.ResourceIdListWorkChunkJson; +import ca.uhn.fhir.batch2.jobs.chunk.FhirIdListWorkChunkJson; import ca.uhn.fhir.batch2.model.JobDefinition; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc; +import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ReplaceReferencesAppCtx { - private static final String JOB_REPLACE_REFERENCES = "REPLACE_REFERENCES"; + public static final String JOB_REPLACE_REFERENCES = "REPLACE_REFERENCES"; @Bean public JobDefinition bulkImport2JobDefinition( @@ -22,7 +26,7 @@ public JobDefinition bulkImport2JobDefinition( .addFirstStep( "query-ids", "Query IDs of resources that link to the source resource", - ResourceIdListWorkChunkJson.class, + FhirIdListWorkChunkJson.class, theReplaceReferencesQueryIds) .addIntermediateStep( "replace-references", @@ -37,13 +41,13 @@ public JobDefinition bulkImport2JobDefinition( } @Bean - public ReplaceReferencesQueryIdsStep replaceReferencesQueryIdsStep() { - return new ReplaceReferencesQueryIdsStep(); + public ReplaceReferencesQueryIdsStep replaceReferencesQueryIdsStep(HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { + return new ReplaceReferencesQueryIdsStep(theHapiTransactionService, theBatch2DaoSvc); } @Bean - public ReplaceReferenceUpdateStep replaceReferenceUpdateStep() { - return new ReplaceReferenceUpdateStep(); + public ReplaceReferenceUpdateStep replaceReferenceUpdateStep(FhirContext theFhirContext, DaoRegistry theDaoRegistry) { + return new ReplaceReferenceUpdateStep(theFhirContext, theDaoRegistry); } @Bean diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java index 374c756cc4d8..f21a6fab18b8 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java @@ -1,5 +1,68 @@ package ca.uhn.fhir.batch2.jobs.replacereferences; -import ca.uhn.fhir.model.api.IModelJson; +import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.provider.ReplaceReferenceRequest; +import ca.uhn.fhir.model.api.BaseBatchJobParameters; +import com.fasterxml.jackson.annotation.JsonProperty; + +import static ca.uhn.fhir.jpa.api.config.JpaStorageSettings.DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING; + +public class ReplaceReferencesJobParameters extends BaseBatchJobParameters { + @JsonProperty("sourceId") + private FhirIdJson mySourceId; + + @JsonProperty("targetId") + private FhirIdJson myTargetId; + + @JsonProperty( + value = "batchSize", + defaultValue = DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING, + required = false) + private int myBatchSize; + + @JsonProperty("partitionId") + private RequestPartitionId myPartitionId; + + public ReplaceReferencesJobParameters() {} + + public ReplaceReferencesJobParameters(ReplaceReferenceRequest theRequest) { + mySourceId = new FhirIdJson(theRequest.sourceId); + myTargetId = new FhirIdJson(theRequest.targetId); + myBatchSize = theRequest.batchSize; + myPartitionId = theRequest.partitionId; + } + + public FhirIdJson getSourceId() { + return mySourceId; + } + + public void setSourceId(FhirIdJson theSourceId) { + mySourceId = theSourceId; + } + + public FhirIdJson getTargetId() { + return myTargetId; + } + + public void setTargetId(FhirIdJson theTargetId) { + myTargetId = theTargetId; + } + + public int getBatchSize() { + return myBatchSize; + } + + public void setBatchSize(int theBatchSize) { + myBatchSize = theBatchSize; + } + + public RequestPartitionId getPartitionId() { + return myPartitionId; + } + + public void setPartitionId(RequestPartitionId thePartitionId) { + myPartitionId = thePartitionId; + } +} -public class ReplaceReferencesJobParameters implements IModelJson {} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java index af21bf9c00e9..5f9677c3a9e2 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java @@ -6,17 +6,58 @@ import ca.uhn.fhir.batch2.api.RunOutcome; import ca.uhn.fhir.batch2.api.StepExecutionDetails; import ca.uhn.fhir.batch2.api.VoidModel; -import ca.uhn.fhir.batch2.jobs.chunk.ResourceIdListWorkChunkJson; +import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; +import ca.uhn.fhir.batch2.jobs.chunk.FhirIdListWorkChunkJson; +import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc; +import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import jakarta.annotation.Nonnull; +import java.util.concurrent.atomic.AtomicInteger; + public class ReplaceReferencesQueryIdsStep - implements IJobStepWorker { + implements IJobStepWorker { + + private final HapiTransactionService myHapiTransactionService; + private final IBatch2DaoSvc myBatch2DaoSvc; + + public ReplaceReferencesQueryIdsStep(HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { + myHapiTransactionService = theHapiTransactionService; + myBatch2DaoSvc = theBatch2DaoSvc; + } + @Nonnull @Override public RunOutcome run( @Nonnull StepExecutionDetails theStepExecutionDetails, - @Nonnull IJobDataSink theDataSink) + @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { - return null; + ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); + + // Warning: It is a little confusing that source/target are reversed in the resource link table from the meaning in + // the replace references request + + FhirIdListWorkChunkJson chunk = new FhirIdListWorkChunkJson(params.getBatchSize(), params.getPartitionId()); + AtomicInteger totalCount = new AtomicInteger(); + myHapiTransactionService + .withSystemRequestOnPartition(params.getPartitionId()) + .execute(() -> myBatch2DaoSvc + .streamSourceIdsThatReferenceTargetId(params.getSourceId().asIdDt()) + .map(FhirIdJson::new) + .forEach(id -> { + chunk.add(id); + if (chunk.size() == params.getBatchSize()) { + totalCount.addAndGet(processChunk(theDataSink, chunk)); + chunk.clear(); + } + })); + if (!chunk.isEmpty()) { + totalCount.addAndGet(processChunk(theDataSink, chunk)); + } + return new RunOutcome(totalCount.get()); + } + + private int processChunk(IJobDataSink theDataSink, FhirIdListWorkChunkJson theChunk) { + theDataSink.accept(theChunk); + return theChunk.size(); } } diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdJson.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdJson.java new file mode 100644 index 000000000000..78a526783e3a --- /dev/null +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdJson.java @@ -0,0 +1,94 @@ +/*- + * #%L + * HAPI FHIR JPA Server - Batch2 Task Processor + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.batch2.jobs.chunk; + +import ca.uhn.fhir.model.api.IModelJson; +import ca.uhn.fhir.model.primitive.IdDt; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.hl7.fhir.instance.model.api.IIdType; + +public class FhirIdJson implements IModelJson { + + @JsonProperty("type") + private String myResourceType; + + @JsonProperty("id") + private String myFhirId; + + public FhirIdJson() {} + + public FhirIdJson(String theResourceType, String theFhirId) { + myResourceType = theResourceType; + myFhirId = theFhirId; + } + + public FhirIdJson(IIdType theFhirId) { + myResourceType = theFhirId.getResourceType(); + myFhirId = theFhirId.getIdPart(); + } + + @Override + public String toString() { + return myResourceType + "/" + myFhirId; + } + + public String getResourceType() { + return myResourceType; + } + + public FhirIdJson setResourceType(String theResourceType) { + myResourceType = theResourceType; + return this; + } + + public String getFhirId() { + return myFhirId; + } + + public FhirIdJson setFhirId(String theFhirId) { + myFhirId = theFhirId; + return this; + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + + if (theO == null || getClass() != theO.getClass()) return false; + + FhirIdJson id = (FhirIdJson) theO; + + return new EqualsBuilder() + .append(myResourceType, id.myResourceType) + .append(myFhirId, id.myFhirId) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(myResourceType).append(myFhirId).toHashCode(); + } + + public IdDt asIdDt() { + return new IdDt(myResourceType, myFhirId); + } +} diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdListWorkChunkJson.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdListWorkChunkJson.java new file mode 100644 index 000000000000..ab286fd7e75e --- /dev/null +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdListWorkChunkJson.java @@ -0,0 +1,96 @@ +/*- + * #%L + * HAPI FHIR JPA Server - Batch2 Task Processor + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.batch2.jobs.chunk; + +import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.model.api.IModelJson; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class FhirIdListWorkChunkJson implements IModelJson { + + @JsonProperty("requestPartitionId") + private RequestPartitionId myRequestPartitionId; + + @JsonProperty("fhirIds") + private List myFhirIds; + + /** + * Constructor + */ + public FhirIdListWorkChunkJson() { + super(); + } + + /** + * Constructor + */ + public FhirIdListWorkChunkJson( + Collection theFhirIds, RequestPartitionId theRequestPartitionId) { + this(); + getFhirIds().addAll(theFhirIds); + myRequestPartitionId = theRequestPartitionId; + } + + public FhirIdListWorkChunkJson(int theBatchSize, RequestPartitionId theRequestPartitionId) { + myFhirIds = new ArrayList<>(theBatchSize); + myRequestPartitionId = theRequestPartitionId; + } + + public RequestPartitionId getRequestPartitionId() { + return myRequestPartitionId; + } + + public List getFhirIds() { + if (myFhirIds == null) { + myFhirIds = new ArrayList<>(); + } + return myFhirIds; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("ids", myFhirIds) + .append("requestPartitionId", myRequestPartitionId) + .toString(); + } + + public int size() { + return getFhirIds().size(); + } + + public boolean isEmpty() { + return getFhirIds().isEmpty(); + } + + public void add(FhirIdJson theFhirId) { + getFhirIds().add(theFhirId); + } + + public void clear() { + getFhirIds().clear(); + } +} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java index 028e1472e0f0..9a499c439d37 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java @@ -117,7 +117,8 @@ public class JpaStorageSettings extends StorageSettings { private static final boolean DEFAULT_PREVENT_INVALIDATING_CONDITIONAL_MATCH_CRITERIA = false; private static final long DEFAULT_REST_DELETE_BY_URL_RESOURCE_ID_THRESHOLD = 10000; - public static final int DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE = 512; + public static final String DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING = "512"; + public static final int DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE = Integer.parseInt(DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING); /** * Do not change default of {@code 0}! diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java index cd303ca4fe54..62feeb8942f7 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java @@ -24,10 +24,13 @@ import ca.uhn.fhir.jpa.api.pid.IResourcePidList; import ca.uhn.fhir.jpa.api.pid.IResourcePidStream; import ca.uhn.fhir.jpa.api.pid.ListWrappingPidStream; +import ca.uhn.fhir.model.primitive.IdDt; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import org.hl7.fhir.instance.model.api.IIdType; import java.util.Date; +import java.util.stream.Stream; public interface IBatch2DaoSvc { @@ -76,4 +79,6 @@ default IResourcePidStream fetchResourceIdStream( return new ListWrappingPidStream(fetchResourceIdsPage( theStart, theEnd, 20000 /* ResourceIdListStep.DEFAULT_PAGE_SIZE */, theTargetPartitionId, theUrl)); } + + Stream streamSourceIdsThatReferenceTargetId(IIdType theTargetId); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 51dcde7024fc..233718fd60c2 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -20,9 +20,12 @@ package ca.uhn.fhir.jpa.dao.merge; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; import ca.uhn.fhir.jpa.provider.ReplaceReferenceRequest; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; @@ -63,13 +66,15 @@ public class ResourceMergeService { private final IReplaceReferencesSvc myReplaceReferencesSvc; private final IHapiTransactionService myHapiTransactionService; private final FhirContext myFhirContext; + private final IRequestPartitionHelperSvc myRequestPartitionHelperSvc; public ResourceMergeService( - IFhirResourceDaoPatient thePatientDao, - IReplaceReferencesSvc theReplaceReferencesSvc, - IHapiTransactionService theHapiTransactionService) { + IFhirResourceDaoPatient thePatientDao, + IReplaceReferencesSvc theReplaceReferencesSvc, + IHapiTransactionService theHapiTransactionService, IRequestPartitionHelperSvc theRequestPartitionHelperSvc) { myDao = thePatientDao; myReplaceReferencesSvc = theReplaceReferencesSvc; + myRequestPartitionHelperSvc = theRequestPartitionHelperSvc; myFhirContext = myDao.getContext(); myHapiTransactionService = theHapiTransactionService; } @@ -82,7 +87,7 @@ public ResourceMergeService( * @return the merge outcome containing OperationOutcome and HTTP status code */ public MergeOperationOutcome merge( - MergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + MergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); @@ -105,9 +110,9 @@ public MergeOperationOutcome merge( } private void validateAndMerge( - MergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { IBaseOperationOutcome operationOutcome = theMergeOutcome.getOperationOutcome(); @@ -118,7 +123,7 @@ private void validateAndMerge( // cast to Patient, since we only support merging Patient resources for now Patient sourceResource = - (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); + (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (sourceResource == null) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); @@ -127,7 +132,7 @@ private void validateAndMerge( // cast to Patient, since we only support merging Patient resources for now Patient targetResource = - (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); + (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (targetResource == null) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); @@ -140,19 +145,19 @@ private void validateAndMerge( } if (!validateResultResourceIfExists( - theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { + theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); return; } if (theMergeOperationParameters.getPreview()) { Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( - sourceResource.getIdElement(), theRequestDetails); + sourceResource.getIdElement(), theRequestDetails); // in preview mode, we should also return how the target would look like Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); Patient targetPatientAsIfUpdated = prepareTargetPatientForUpdate( - targetResource, sourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); + targetResource, sourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); // adding +2 because the source and the target resources themselved would be updated as well @@ -163,35 +168,38 @@ private void validateAndMerge( } mergeInTransaction( - theMergeOperationParameters, sourceResource, targetResource, theRequestDetails, theMergeOutcome); + theMergeOperationParameters, sourceResource, targetResource, theRequestDetails, theMergeOutcome); String detailsText = "Merge operation completed successfully."; addInfoToOperationOutcome(operationOutcome, null, detailsText); } private void mergeInTransaction( - MergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { // TODO: cannot do this in transaction yet, because systemDAO.transaction called by replaceReferences complains // that there is an active transaction already. + RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); + ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest( - theSourceResource.getIdElement(), - theTargetResource.getIdElement(), - theMergeOperationParameters.getBatchSize()); - // FIXME KHS check if it needs to go async + theSourceResource.getIdElement(), + theTargetResource.getIdElement(), + theMergeOperationParameters.getBatchSize(), + partitionId); + // FIXME KHS use the result of this method call to see if it went async myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); Patient patientToUpdate = prepareTargetPatientForUpdate( - theTargetResource, - theSourceResource, - theResultResource, - theMergeOperationParameters.getDeleteSource()); + theTargetResource, + theSourceResource, + theResultResource, + theMergeOperationParameters.getDeleteSource()); // update the target patient resource after the references are updated Patient targetPatientAfterUpdate = updateResource(patientToUpdate, theRequestDetails); theMergeOutcome.setUpdatedTargetResource(targetPatientAfterUpdate); @@ -206,10 +214,10 @@ private void mergeInTransaction( } private boolean validateResultResourceIfExists( - MergeOperationInputParameters theMergeOperationParameters, - Patient theResolvedTargetResource, - Patient theResolvedSourceResource, - IBaseOperationOutcome theOperationOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + Patient theResolvedTargetResource, + Patient theResolvedSourceResource, + IBaseOperationOutcome theOperationOutcome) { if (theMergeOperationParameters.getResultResource() == null) { // result resource is not provided, no further validation is needed @@ -223,21 +231,21 @@ private boolean validateResultResourceIfExists( // validate the result resource's id as same as the target resource if (!theResolvedTargetResource.getIdElement().toVersionless().equals(theResultResource.getIdElement())) { String msg = String.format( - "'%s' must have the same versionless id as the actual resolved target resource. " - + "The actual resolved target resource's id is: '%s'", - theMergeOperationParameters.getResultResourceParameterName(), - theResolvedTargetResource.getIdElement().toVersionless().getValue()); + "'%s' must have the same versionless id as the actual resolved target resource. " + + "The actual resolved target resource's id is: '%s'", + theMergeOperationParameters.getResultResourceParameterName(), + theResolvedTargetResource.getIdElement().toVersionless().getValue()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); isValid = false; } // validate the result resource contains the identifiers provided in the target identifiers param if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { + && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { String msg = String.format( - "'%s' must have all the identifiers provided in %s", - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "'%s' must have all the identifiers provided in %s", + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); isValid = false; } @@ -247,11 +255,11 @@ private boolean validateResultResourceIfExists( // if the source resource is being deleted, the result resource must not have a replaces link to the source // resource if (!validateResultResourceReplacesLinkToSourceResource( - theResultResource, - theResolvedSourceResource, - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getDeleteSource(), - theOperationOutcome)) { + theResultResource, + theResolvedSourceResource, + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getDeleteSource(), + theOperationOutcome)) { isValid = false; } @@ -263,9 +271,9 @@ private boolean hasAllIdentifiers(Patient theResource, List List identifiersInResource = theResource.getIdentifier(); for (CanonicalIdentifier identifier : theIdentifiers) { boolean identifierFound = identifiersInResource.stream() - .anyMatch(i -> i.getSystem() - .equals(identifier.getSystemElement().getValueAsString()) - && i.getValue().equals(identifier.getValueElement().getValueAsString())); + .anyMatch(i -> i.getSystem() + .equals(identifier.getSystemElement().getValueAsString()) + && i.getValue().equals(identifier.getValueElement().getValueAsString())); if (!identifierFound) { return false; @@ -275,45 +283,45 @@ private boolean hasAllIdentifiers(Patient theResource, List } private List getLinksToResource( - Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { + Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { List links = getLinksOfType(theResource, theLinkType); return links.stream() - .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) - .collect(Collectors.toList()); + .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) + .collect(Collectors.toList()); } private boolean validateResultResourceReplacesLinkToSourceResource( - Patient theResultResource, - Patient theResolvedSourceResource, - String theResultResourceParameterName, - boolean theDeleteSource, - IBaseOperationOutcome theOperationOutcome) { + Patient theResultResource, + Patient theResolvedSourceResource, + String theResultResourceParameterName, + boolean theDeleteSource, + IBaseOperationOutcome theOperationOutcome) { // the result resource must have the replaces link set to the source resource List replacesLinkToSourceResource = getLinksToResource( - theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); + theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); if (theDeleteSource) { if (!replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must not have a 'replaces' link to the source resource " - + "when the source resource will be deleted, as the link may prevent deleting the source " - + "resource.", - theResultResourceParameterName); + "'%s' must not have a 'replaces' link to the source resource " + + "when the source resource will be deleted, as the link may prevent deleting the source " + + "resource.", + theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } } else { if (replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); + "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } if (replacesLinkToSourceResource.size() > 1) { String msg = String.format( - "'%s' has multiple 'replaces' links to the source resource. There should be only one.", - theResultResourceParameterName); + "'%s' has multiple 'replaces' links to the source resource. There should be only one.", + theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } @@ -334,7 +342,7 @@ protected List getLinksOfType(Patient theResource, Patient.LinkType t } private boolean validateSourceAndTargetAreSuitableForMerge( - Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { + Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { if (theSourceResource.getId().equalsIgnoreCase(theTargetResource.getId())) { String msg = "Source and target resources are the same resource."; @@ -353,9 +361,9 @@ private boolean validateSourceAndTargetAreSuitableForMerge( if (!replacedByLinksInTarget.isEmpty()) { String ref = replacedByLinksInTarget.get(0).getReference(); String msg = String.format( - "Target resource was previously replaced by a resource with reference '%s', it " - + "is not a suitable target for merging.", - ref); + "Target resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable target for merging.", + ref); addErrorToOperationOutcome(outcome, msg, "invalid"); return false; } @@ -364,9 +372,9 @@ private boolean validateSourceAndTargetAreSuitableForMerge( if (!replacedByLinksInSource.isEmpty()) { String ref = replacedByLinksInSource.get(0).getReference(); String msg = String.format( - "Source resource was previously replaced by a resource with reference '%s', it " - + "is not a suitable source for merging.", - ref); + "Source resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable source for merging.", + ref); addErrorToOperationOutcome(outcome, msg, "invalid"); return false; } @@ -377,16 +385,16 @@ private boolean validateSourceAndTargetAreSuitableForMerge( private void prepareSourceResourceForUpdate(Patient theSourceResource, Patient theTargetResource) { theSourceResource.setActive(false); theSourceResource - .addLink() - .setType(Patient.LinkType.REPLACEDBY) - .setOther(new Reference(theTargetResource.getIdElement().toVersionless())); + .addLink() + .setType(Patient.LinkType.REPLACEDBY) + .setOther(new Reference(theTargetResource.getIdElement().toVersionless())); } private Patient prepareTargetPatientForUpdate( - Patient theTargetResource, - Patient theSourceResource, - @Nullable Patient theResultResource, - boolean theDeleteSource) { + Patient theTargetResource, + Patient theSourceResource, + @Nullable Patient theResultResource, + boolean theDeleteSource) { // if the client provided a result resource as input then use it to update the target resource if (theResultResource != null) { @@ -397,9 +405,9 @@ private Patient prepareTargetPatientForUpdate( // add the replaces link to the target resource, if the source resource is not to be deleted if (!theDeleteSource) { theTargetResource - .addLink() - .setType(Patient.LinkType.REPLACES) - .setOther(new Reference(theSourceResource.getIdElement().toVersionless())); + .addLink() + .setType(Patient.LinkType.REPLACES) + .setOther(new Reference(theSourceResource.getIdElement().toVersionless())); } // copy all identifiers from the source to the target @@ -410,8 +418,9 @@ private Patient prepareTargetPatientForUpdate( /** * Checks if theIdentifiers contains theIdentifier using equalsDeep + * * @param theIdentifiers the list of identifiers - * @param theIdentifier the identifier to check + * @param theIdentifier the identifier to check * @return true if theIdentifiers contains theIdentifier, false otherwise */ private boolean containsIdentifier(List theIdentifiers, Identifier theIdentifier) { @@ -426,6 +435,7 @@ private boolean containsIdentifier(List theIdentifiers, Identifier t /** * Copies each identifier from theSourceResource to theTargetResource, after checking that theTargetResource does * not already contain the source identifier + * * @param theSourceResource the source resource to copy identifiers from * @param theTargetResource the target resource to copy identifiers to */ @@ -458,59 +468,59 @@ private void deleteResource(Patient theResource, RequestDetails theRequestDetail * @return true if the parameters are valid, false otherwise */ private boolean validateMergeOperationParameters( - MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { List errorMessages = new ArrayList<>(); if (!theMergeOperationParameters.hasAtLeastOneSourceIdentifier() - && theMergeOperationParameters.getSourceResource() == null) { + && theMergeOperationParameters.getSourceResource() == null) { String msg = String.format( - "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", - theMergeOperationParameters.getSourceResourceParameterName(), - theMergeOperationParameters.getSourceIdentifiersParameterName()); + "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); errorMessages.add(msg); } // Spec has conflicting information about this case if (theMergeOperationParameters.hasAtLeastOneSourceIdentifier() - && theMergeOperationParameters.getSourceResource() != null) { + && theMergeOperationParameters.getSourceResource() != null) { String msg = String.format( - "Source resource must be provided either by '%s' or by '%s', not both.", - theMergeOperationParameters.getSourceResourceParameterName(), - theMergeOperationParameters.getSourceIdentifiersParameterName()); + "Source resource must be provided either by '%s' or by '%s', not both.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); errorMessages.add(msg); } if (!theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && theMergeOperationParameters.getTargetResource() == null) { + && theMergeOperationParameters.getTargetResource() == null) { String msg = String.format( - "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", - theMergeOperationParameters.getTargetResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); errorMessages.add(msg); } // Spec has conflicting information about this case if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && theMergeOperationParameters.getTargetResource() != null) { + && theMergeOperationParameters.getTargetResource() != null) { String msg = String.format( - "Target resource must be provided either by '%s' or by '%s', not both.", - theMergeOperationParameters.getTargetResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "Target resource must be provided either by '%s' or by '%s', not both.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); errorMessages.add(msg); } Reference sourceRef = (Reference) theMergeOperationParameters.getSourceResource(); if (sourceRef != null && !sourceRef.hasReference()) { String msg = String.format( - "Reference specified in '%s' parameter does not have a reference element.", - theMergeOperationParameters.getSourceResourceParameterName()); + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getSourceResourceParameterName()); errorMessages.add(msg); } Reference targetRef = (Reference) theMergeOperationParameters.getTargetResource(); if (targetRef != null && !targetRef.hasReference()) { String msg = String.format( - "Reference specified in '%s' parameter does not have a reference element.", - theMergeOperationParameters.getTargetResourceParameterName()); + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getTargetResourceParameterName()); errorMessages.add(msg); } @@ -527,43 +537,43 @@ private boolean validateMergeOperationParameters( } private IBaseResource resolveSourceResource( - MergeOperationInputParameters theOperationParameters, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { return resolveResource( - theOperationParameters.getSourceResource(), - theOperationParameters.getSourceIdentifiers(), - theRequestDetails, - theOutcome, - theOperationParameters.getSourceResourceParameterName(), - theOperationParameters.getSourceIdentifiersParameterName()); + theOperationParameters.getSourceResource(), + theOperationParameters.getSourceIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getSourceResourceParameterName(), + theOperationParameters.getSourceIdentifiersParameterName()); } private IBaseResource resolveTargetResource( - MergeOperationInputParameters theOperationParameters, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { return resolveResource( - theOperationParameters.getTargetResource(), - theOperationParameters.getTargetIdentifiers(), - theRequestDetails, - theOutcome, - theOperationParameters.getTargetResourceParameterName(), - theOperationParameters.getTargetIdentifiersParameterName()); + theOperationParameters.getTargetResource(), + theOperationParameters.getTargetIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getTargetResourceParameterName(), + theOperationParameters.getTargetIdentifiersParameterName()); } private IBaseResource resolveResourceByIdentifiers( - List theIdentifiers, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationParameterName) { + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { SearchParameterMap searchParameterMap = new SearchParameterMap(); TokenAndListParam tokenAndListParam = new TokenAndListParam(); for (CanonicalIdentifier identifier : theIdentifiers) { TokenParam tokenParam = new TokenParam( - identifier.getSystemElement().getValueAsString(), - identifier.getValueElement().getValueAsString()); + identifier.getSystemElement().getValueAsString(), + identifier.getValueElement().getValueAsString()); tokenAndListParam.addAnd(tokenParam); } searchParameterMap.add("identifier", tokenAndListParam); @@ -573,13 +583,13 @@ private IBaseResource resolveResourceByIdentifiers( List resources = bundle.getAllResources(); if (resources.isEmpty()) { String msg = String.format( - "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); + "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "not-found"); return null; } if (resources.size() > 1) { String msg = String.format( - "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); + "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "multiple-matches"); return null; } @@ -588,10 +598,10 @@ private IBaseResource resolveResourceByIdentifiers( } private IBaseResource resolveResourceByReference( - IBaseReference theReference, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationParameterName) { + IBaseReference theReference, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { // TODO Emre: why does IBaseReference not have getIdentifier or hasReference methods? // casting it to r4.Reference for now Reference r4ref = (Reference) theReference; @@ -602,19 +612,19 @@ private IBaseResource resolveResourceByReference( resource = myDao.read(theResourceId.toVersionless(), theRequestDetails); } catch (ResourceNotFoundException e) { String msg = String.format( - "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); + "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "not-found"); return null; } if (theResourceId.hasVersionIdPart() - && !theResourceId - .getVersionIdPart() - .equals(resource.getIdElement().getVersionIdPart())) { + && !theResourceId + .getVersionIdPart() + .equals(resource.getIdElement().getVersionIdPart())) { String msg = String.format( - "The reference in '%s' parameter has a version specified, " - + "but it is not the latest version of the resource", - theOperationParameterName); + "The reference in '%s' parameter has a version specified, " + + "but it is not the latest version of the resource", + theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "conflict"); return null; } @@ -623,25 +633,25 @@ private IBaseResource resolveResourceByReference( } private IBaseResource resolveResource( - IBaseReference theReference, - List theIdentifiers, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationReferenceParameterName, - String theOperationIdentifiersParameterName) { + IBaseReference theReference, + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationReferenceParameterName, + String theOperationIdentifiersParameterName) { if (theReference != null) { return resolveResourceByReference( - theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); + theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); } return resolveResourceByIdentifiers( - theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); + theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); } private void addInfoToOperationOutcome( - IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { + IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { IBase issue = - OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); OperationOutcomeUtil.addDetailsToIssue(myFhirContext, issue, null, null, theDetailsText); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java index 0140b5d3366e..361797ff69a4 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.provider; import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.rest.param.StringParam; @@ -23,10 +24,13 @@ public class ReplaceReferenceRequest { public final int batchSize; - public ReplaceReferenceRequest(@Nonnull IIdType theSourceId, @Nonnull IIdType theTargetId, int theBatchSize) { + public final RequestPartitionId partitionId; + + public ReplaceReferenceRequest(@Nonnull IIdType theSourceId, @Nonnull IIdType theTargetId, int theBatchSize, RequestPartitionId thePartitionId) { sourceId = theSourceId.toUnqualifiedVersionless(); targetId = theTargetId.toUnqualifiedVersionless(); batchSize = theBatchSize; + partitionId = thePartitionId; } public void validateOrThrowInvalidParameterException() { diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index 20095cbec27f..595a2bdb7fd9 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -12,7 +13,6 @@ import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.CanonicalIdentifier; -import net.sourceforge.plantuml.bpm.Col; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; @@ -73,6 +73,10 @@ public class ResourceMergeServiceTest { @Mock IHapiTransactionService myTransactionServiceMock; + @Mock + IRequestPartitionHelperSvc myRequestPartitionHelperSvcMock; + + private ResourceMergeService myResourceMergeService; private final FhirContext myFhirContext = FhirContext.forR4Cached(); @@ -84,7 +88,7 @@ public class ResourceMergeServiceTest { @BeforeEach void setup() { when(myDaoMock.getContext()).thenReturn(myFhirContext); - myResourceMergeService = new ResourceMergeService(myDaoMock, myReplaceReferencesSvcMock, myTransactionServiceMock); + myResourceMergeService = new ResourceMergeService(myDaoMock, myReplaceReferencesSvcMock, myTransactionServiceMock, myRequestPartitionHelperSvcMock); } // SUCCESS CASES From d6c1cc8506badad60094a3d04e11c04f41091b3d Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 10 Dec 2024 14:07:30 -0500 Subject: [PATCH 060/148] build out batch --- .../uhn/fhir/jpa/batch2/Batch2DaoSvcImpl.java | 11 +- .../fhir/jpa/config/Batch2SupportConfig.java | 7 +- .../BaseJpaResourceProviderPatient.java | 4 +- .../fhir/jpa/provider/JpaSystemProvider.java | 3 +- .../ReplaceReferenceUpdateStep.java | 40 ++- .../ReplaceReferencesAppCtx.java | 8 +- .../ReplaceReferencesJobParameters.java | 7 +- .../ReplaceReferencesQueryIdsStep.java | 29 +- .../fhir/batch2/jobs/chunk/FhirIdJson.java | 5 +- .../jobs/chunk/FhirIdListWorkChunkJson.java | 3 +- .../jpa/api/config/JpaStorageSettings.java | 3 +- .../jpa/dao/merge/ResourceMergeService.java | 298 +++++++++--------- .../jpa/provider/ReplaceReferenceRequest.java | 6 +- 13 files changed, 220 insertions(+), 204 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/Batch2DaoSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/Batch2DaoSvcImpl.java index f3bb75dc410b..c90dc5a8170d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/Batch2DaoSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch2/Batch2DaoSvcImpl.java @@ -75,11 +75,12 @@ public boolean isAllResourceTypeSupported() { } public Batch2DaoSvcImpl( - IResourceTableDao theResourceTableDao, IResourceLinkDao theResourceLinkDao, - MatchUrlService theMatchUrlService, - DaoRegistry theDaoRegistry, - FhirContext theFhirContext, - IHapiTransactionService theTransactionService) { + IResourceTableDao theResourceTableDao, + IResourceLinkDao theResourceLinkDao, + MatchUrlService theMatchUrlService, + DaoRegistry theDaoRegistry, + FhirContext theFhirContext, + IHapiTransactionService theTransactionService) { myResourceTableDao = theResourceTableDao; myResourceLinkDao = theResourceLinkDao; myMatchUrlService = theMatchUrlService; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/Batch2SupportConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/Batch2SupportConfig.java index 30ee9fe77853..b0b4f42258b5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/Batch2SupportConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/Batch2SupportConfig.java @@ -49,7 +49,12 @@ public IBatch2DaoSvc batch2DaoSvc( FhirContext theFhirContext, IHapiTransactionService theTransactionService) { return new Batch2DaoSvcImpl( - theResourceTableDao, theResourceLinkDao, theMatchUrlService, theDaoRegistry, theFhirContext, theTransactionService); + theResourceTableDao, + theResourceLinkDao, + theMatchUrlService, + theDaoRegistry, + theFhirContext, + theTransactionService); } @Bean diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index b87747b905f8..2d55232b77c6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -317,8 +317,8 @@ public IBaseParameters patientMerge( batchSize); IFhirResourceDaoPatient dao = (IFhirResourceDaoPatient) getDao(); - ResourceMergeService resourceMergeService = - new ResourceMergeService(dao, myReplaceReferencesSvc, myHapiTransactionService, myRequestPartitionHelperSvc); + ResourceMergeService resourceMergeService = new ResourceMergeService( + dao, myReplaceReferencesSvc, myHapiTransactionService, myRequestPartitionHelperSvc); FhirContext fhirContext = dao.getContext(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index aeba4d5c7890..cb4eec6886b4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -176,7 +176,8 @@ public IBaseParameters replaceReferences( IdDt sourceId = new IdDt(theSourceId); IdDt targetId = new IdDt(theTargetId); - RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, ReadPartitionIdRequestDetails.forRead(targetId)); + RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( + theRequestDetails, ReadPartitionIdRequestDetails.forRead(targetId)); ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest(sourceId, targetId, batchSize, partitionId); return getReplaceReferencesSvc().replaceReferences(replaceReferenceRequest, theRequestDetails); diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java index 12f333f1f1b8..23c79315eb86 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java @@ -35,8 +35,7 @@ import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_VALUE; public class ReplaceReferenceUpdateStep - implements IJobStepWorker< - ReplaceReferencesJobParameters, FhirIdListWorkChunkJson, ReplaceReferenceResults> { + implements IJobStepWorker { private final FhirContext myFhirContext; private final DaoRegistry myDaoRegistry; @@ -70,14 +69,12 @@ public RunOutcome run( } private Bundle buildPatchBundle( - ReplaceReferencesJobParameters theParams, - FhirIdListWorkChunkJson theFhirIds, RequestDetails theRequestDetails) { + ReplaceReferencesJobParameters theParams, + FhirIdListWorkChunkJson theFhirIds, + RequestDetails theRequestDetails) { BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); - - theFhirIds.getFhirIds().stream() - .map(FhirIdJson::asIdDt) - .forEach(referencingResourceId -> { + theFhirIds.getFhirIds().stream().map(FhirIdJson::asIdDt).forEach(referencingResourceId -> { IFhirResourceDao dao = myDaoRegistry.getResourceDao(referencingResourceId.getResourceType()); IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); Parameters patchParams = buildPatchParams(theParams, resource); @@ -88,31 +85,31 @@ private Bundle buildPatchBundle( } private @Nonnull Parameters buildPatchParams( - ReplaceReferencesJobParameters theParams, IBaseResource referencingResource) { + ReplaceReferencesJobParameters theParams, IBaseResource referencingResource) { Parameters params = new Parameters(); myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() - .filter(refInfo -> matches( - refInfo, - theParams.getSourceId().asIdDt())) // We only care about references to our source resource - .map(refInfo -> createReplaceReferencePatchOperation( - referencingResource.fhirType() + "." + refInfo.getName(), - new Reference(theParams.getTargetId().toString()))) - .forEach(params::addParameter); // Add each operation to parameters + .filter(refInfo -> matches( + refInfo, + theParams.getSourceId().asIdDt())) // We only care about references to our source resource + .map(refInfo -> createReplaceReferencePatchOperation( + referencingResource.fhirType() + "." + refInfo.getName(), + new Reference(theParams.getTargetId().toString()))) + .forEach(params::addParameter); // Add each operation to parameters return params; } private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { return refInfo.getResourceReference() - .getReferenceElement() - .toUnqualifiedVersionless() - .getValueAsString() - .equals(theSourceId.getValueAsString()); + .getReferenceElement() + .toUnqualifiedVersionless() + .getValueAsString() + .equals(theSourceId.getValueAsString()); } @Nonnull private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( - String thePath, Type theValue) { + String thePath, Type theValue) { Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); operation.setName(PARAMETER_OPERATION); @@ -121,5 +118,4 @@ private Parameters.ParametersParameterComponent createReplaceReferencePatchOpera operation.addPart().setName(PARAMETER_VALUE).setValue(theValue); return operation; } - } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java index 89de6d0df998..bab9ac6d7d50 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java @@ -26,7 +26,7 @@ public JobDefinition bulkImport2JobDefinition( .addFirstStep( "query-ids", "Query IDs of resources that link to the source resource", - FhirIdListWorkChunkJson.class, + FhirIdListWorkChunkJson.class, theReplaceReferencesQueryIds) .addIntermediateStep( "replace-references", @@ -41,12 +41,14 @@ public JobDefinition bulkImport2JobDefinition( } @Bean - public ReplaceReferencesQueryIdsStep replaceReferencesQueryIdsStep(HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { + public ReplaceReferencesQueryIdsStep replaceReferencesQueryIdsStep( + HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { return new ReplaceReferencesQueryIdsStep(theHapiTransactionService, theBatch2DaoSvc); } @Bean - public ReplaceReferenceUpdateStep replaceReferenceUpdateStep(FhirContext theFhirContext, DaoRegistry theDaoRegistry) { + public ReplaceReferenceUpdateStep replaceReferenceUpdateStep( + FhirContext theFhirContext, DaoRegistry theDaoRegistry) { return new ReplaceReferenceUpdateStep(theFhirContext, theDaoRegistry); } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java index f21a6fab18b8..466cc8035500 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java @@ -16,9 +16,9 @@ public class ReplaceReferencesJobParameters extends BaseBatchJobParameters { private FhirIdJson myTargetId; @JsonProperty( - value = "batchSize", - defaultValue = DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING, - required = false) + value = "batchSize", + defaultValue = DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING, + required = false) private int myBatchSize; @JsonProperty("partitionId") @@ -65,4 +65,3 @@ public void setPartitionId(RequestPartitionId thePartitionId) { myPartitionId = thePartitionId; } } - diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java index 5f9677c3a9e2..a58049ccd9d9 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java @@ -20,7 +20,8 @@ public class ReplaceReferencesQueryIdsStep private final HapiTransactionService myHapiTransactionService; private final IBatch2DaoSvc myBatch2DaoSvc; - public ReplaceReferencesQueryIdsStep(HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { + public ReplaceReferencesQueryIdsStep( + HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { myHapiTransactionService = theHapiTransactionService; myBatch2DaoSvc = theBatch2DaoSvc; } @@ -33,23 +34,25 @@ public RunOutcome run( throws JobExecutionFailedException { ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); - // Warning: It is a little confusing that source/target are reversed in the resource link table from the meaning in + // Warning: It is a little confusing that source/target are reversed in the resource link table from the meaning + // in // the replace references request FhirIdListWorkChunkJson chunk = new FhirIdListWorkChunkJson(params.getBatchSize(), params.getPartitionId()); AtomicInteger totalCount = new AtomicInteger(); myHapiTransactionService - .withSystemRequestOnPartition(params.getPartitionId()) - .execute(() -> myBatch2DaoSvc - .streamSourceIdsThatReferenceTargetId(params.getSourceId().asIdDt()) - .map(FhirIdJson::new) - .forEach(id -> { - chunk.add(id); - if (chunk.size() == params.getBatchSize()) { - totalCount.addAndGet(processChunk(theDataSink, chunk)); - chunk.clear(); - } - })); + .withSystemRequestOnPartition(params.getPartitionId()) + .execute(() -> myBatch2DaoSvc + .streamSourceIdsThatReferenceTargetId( + params.getSourceId().asIdDt()) + .map(FhirIdJson::new) + .forEach(id -> { + chunk.add(id); + if (chunk.size() == params.getBatchSize()) { + totalCount.addAndGet(processChunk(theDataSink, chunk)); + chunk.clear(); + } + })); if (!chunk.isEmpty()) { totalCount.addAndGet(processChunk(theDataSink, chunk)); } diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdJson.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdJson.java index 78a526783e3a..e608dc0ccefe 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdJson.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdJson.java @@ -85,7 +85,10 @@ public boolean equals(Object theO) { @Override public int hashCode() { - return new HashCodeBuilder(17, 37).append(myResourceType).append(myFhirId).toHashCode(); + return new HashCodeBuilder(17, 37) + .append(myResourceType) + .append(myFhirId) + .toHashCode(); } public IdDt asIdDt() { diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdListWorkChunkJson.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdListWorkChunkJson.java index ab286fd7e75e..77064f586991 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdListWorkChunkJson.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdListWorkChunkJson.java @@ -47,8 +47,7 @@ public FhirIdListWorkChunkJson() { /** * Constructor */ - public FhirIdListWorkChunkJson( - Collection theFhirIds, RequestPartitionId theRequestPartitionId) { + public FhirIdListWorkChunkJson(Collection theFhirIds, RequestPartitionId theRequestPartitionId) { this(); getFhirIds().addAll(theFhirIds); myRequestPartitionId = theRequestPartitionId; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java index 9a499c439d37..0e60aba4cbf0 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java @@ -118,7 +118,8 @@ public class JpaStorageSettings extends StorageSettings { private static final long DEFAULT_REST_DELETE_BY_URL_RESOURCE_ID_THRESHOLD = 10000; public static final String DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING = "512"; - public static final int DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE = Integer.parseInt(DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING); + public static final int DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE = + Integer.parseInt(DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING); /** * Do not change default of {@code 0}! diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 233718fd60c2..1a30c34891af 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -69,9 +69,10 @@ public class ResourceMergeService { private final IRequestPartitionHelperSvc myRequestPartitionHelperSvc; public ResourceMergeService( - IFhirResourceDaoPatient thePatientDao, - IReplaceReferencesSvc theReplaceReferencesSvc, - IHapiTransactionService theHapiTransactionService, IRequestPartitionHelperSvc theRequestPartitionHelperSvc) { + IFhirResourceDaoPatient thePatientDao, + IReplaceReferencesSvc theReplaceReferencesSvc, + IHapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc) { myDao = thePatientDao; myReplaceReferencesSvc = theReplaceReferencesSvc; myRequestPartitionHelperSvc = theRequestPartitionHelperSvc; @@ -87,7 +88,7 @@ public ResourceMergeService( * @return the merge outcome containing OperationOutcome and HTTP status code */ public MergeOperationOutcome merge( - MergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + MergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); @@ -110,9 +111,9 @@ public MergeOperationOutcome merge( } private void validateAndMerge( - MergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { IBaseOperationOutcome operationOutcome = theMergeOutcome.getOperationOutcome(); @@ -123,7 +124,7 @@ private void validateAndMerge( // cast to Patient, since we only support merging Patient resources for now Patient sourceResource = - (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); + (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (sourceResource == null) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); @@ -132,7 +133,7 @@ private void validateAndMerge( // cast to Patient, since we only support merging Patient resources for now Patient targetResource = - (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); + (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (targetResource == null) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); @@ -145,19 +146,19 @@ private void validateAndMerge( } if (!validateResultResourceIfExists( - theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { + theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); return; } if (theMergeOperationParameters.getPreview()) { Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( - sourceResource.getIdElement(), theRequestDetails); + sourceResource.getIdElement(), theRequestDetails); // in preview mode, we should also return how the target would look like Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); Patient targetPatientAsIfUpdated = prepareTargetPatientForUpdate( - targetResource, sourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); + targetResource, sourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); // adding +2 because the source and the target resources themselved would be updated as well @@ -168,38 +169,39 @@ private void validateAndMerge( } mergeInTransaction( - theMergeOperationParameters, sourceResource, targetResource, theRequestDetails, theMergeOutcome); + theMergeOperationParameters, sourceResource, targetResource, theRequestDetails, theMergeOutcome); String detailsText = "Merge operation completed successfully."; addInfoToOperationOutcome(operationOutcome, null, detailsText); } private void mergeInTransaction( - MergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { // TODO: cannot do this in transaction yet, because systemDAO.transaction called by replaceReferences complains // that there is an active transaction already. - RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest(theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); + RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( + theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest( - theSourceResource.getIdElement(), - theTargetResource.getIdElement(), - theMergeOperationParameters.getBatchSize(), - partitionId); + theSourceResource.getIdElement(), + theTargetResource.getIdElement(), + theMergeOperationParameters.getBatchSize(), + partitionId); // FIXME KHS use the result of this method call to see if it went async myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); Patient patientToUpdate = prepareTargetPatientForUpdate( - theTargetResource, - theSourceResource, - theResultResource, - theMergeOperationParameters.getDeleteSource()); + theTargetResource, + theSourceResource, + theResultResource, + theMergeOperationParameters.getDeleteSource()); // update the target patient resource after the references are updated Patient targetPatientAfterUpdate = updateResource(patientToUpdate, theRequestDetails); theMergeOutcome.setUpdatedTargetResource(targetPatientAfterUpdate); @@ -214,10 +216,10 @@ private void mergeInTransaction( } private boolean validateResultResourceIfExists( - MergeOperationInputParameters theMergeOperationParameters, - Patient theResolvedTargetResource, - Patient theResolvedSourceResource, - IBaseOperationOutcome theOperationOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + Patient theResolvedTargetResource, + Patient theResolvedSourceResource, + IBaseOperationOutcome theOperationOutcome) { if (theMergeOperationParameters.getResultResource() == null) { // result resource is not provided, no further validation is needed @@ -231,21 +233,21 @@ private boolean validateResultResourceIfExists( // validate the result resource's id as same as the target resource if (!theResolvedTargetResource.getIdElement().toVersionless().equals(theResultResource.getIdElement())) { String msg = String.format( - "'%s' must have the same versionless id as the actual resolved target resource. " - + "The actual resolved target resource's id is: '%s'", - theMergeOperationParameters.getResultResourceParameterName(), - theResolvedTargetResource.getIdElement().toVersionless().getValue()); + "'%s' must have the same versionless id as the actual resolved target resource. " + + "The actual resolved target resource's id is: '%s'", + theMergeOperationParameters.getResultResourceParameterName(), + theResolvedTargetResource.getIdElement().toVersionless().getValue()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); isValid = false; } // validate the result resource contains the identifiers provided in the target identifiers param if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { + && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { String msg = String.format( - "'%s' must have all the identifiers provided in %s", - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "'%s' must have all the identifiers provided in %s", + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); isValid = false; } @@ -255,11 +257,11 @@ private boolean validateResultResourceIfExists( // if the source resource is being deleted, the result resource must not have a replaces link to the source // resource if (!validateResultResourceReplacesLinkToSourceResource( - theResultResource, - theResolvedSourceResource, - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getDeleteSource(), - theOperationOutcome)) { + theResultResource, + theResolvedSourceResource, + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getDeleteSource(), + theOperationOutcome)) { isValid = false; } @@ -271,9 +273,9 @@ private boolean hasAllIdentifiers(Patient theResource, List List identifiersInResource = theResource.getIdentifier(); for (CanonicalIdentifier identifier : theIdentifiers) { boolean identifierFound = identifiersInResource.stream() - .anyMatch(i -> i.getSystem() - .equals(identifier.getSystemElement().getValueAsString()) - && i.getValue().equals(identifier.getValueElement().getValueAsString())); + .anyMatch(i -> i.getSystem() + .equals(identifier.getSystemElement().getValueAsString()) + && i.getValue().equals(identifier.getValueElement().getValueAsString())); if (!identifierFound) { return false; @@ -283,45 +285,45 @@ private boolean hasAllIdentifiers(Patient theResource, List } private List getLinksToResource( - Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { + Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { List links = getLinksOfType(theResource, theLinkType); return links.stream() - .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) - .collect(Collectors.toList()); + .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) + .collect(Collectors.toList()); } private boolean validateResultResourceReplacesLinkToSourceResource( - Patient theResultResource, - Patient theResolvedSourceResource, - String theResultResourceParameterName, - boolean theDeleteSource, - IBaseOperationOutcome theOperationOutcome) { + Patient theResultResource, + Patient theResolvedSourceResource, + String theResultResourceParameterName, + boolean theDeleteSource, + IBaseOperationOutcome theOperationOutcome) { // the result resource must have the replaces link set to the source resource List replacesLinkToSourceResource = getLinksToResource( - theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); + theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); if (theDeleteSource) { if (!replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must not have a 'replaces' link to the source resource " - + "when the source resource will be deleted, as the link may prevent deleting the source " - + "resource.", - theResultResourceParameterName); + "'%s' must not have a 'replaces' link to the source resource " + + "when the source resource will be deleted, as the link may prevent deleting the source " + + "resource.", + theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } } else { if (replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); + "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } if (replacesLinkToSourceResource.size() > 1) { String msg = String.format( - "'%s' has multiple 'replaces' links to the source resource. There should be only one.", - theResultResourceParameterName); + "'%s' has multiple 'replaces' links to the source resource. There should be only one.", + theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } @@ -342,7 +344,7 @@ protected List getLinksOfType(Patient theResource, Patient.LinkType t } private boolean validateSourceAndTargetAreSuitableForMerge( - Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { + Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { if (theSourceResource.getId().equalsIgnoreCase(theTargetResource.getId())) { String msg = "Source and target resources are the same resource."; @@ -361,9 +363,9 @@ private boolean validateSourceAndTargetAreSuitableForMerge( if (!replacedByLinksInTarget.isEmpty()) { String ref = replacedByLinksInTarget.get(0).getReference(); String msg = String.format( - "Target resource was previously replaced by a resource with reference '%s', it " - + "is not a suitable target for merging.", - ref); + "Target resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable target for merging.", + ref); addErrorToOperationOutcome(outcome, msg, "invalid"); return false; } @@ -372,9 +374,9 @@ private boolean validateSourceAndTargetAreSuitableForMerge( if (!replacedByLinksInSource.isEmpty()) { String ref = replacedByLinksInSource.get(0).getReference(); String msg = String.format( - "Source resource was previously replaced by a resource with reference '%s', it " - + "is not a suitable source for merging.", - ref); + "Source resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable source for merging.", + ref); addErrorToOperationOutcome(outcome, msg, "invalid"); return false; } @@ -385,16 +387,16 @@ private boolean validateSourceAndTargetAreSuitableForMerge( private void prepareSourceResourceForUpdate(Patient theSourceResource, Patient theTargetResource) { theSourceResource.setActive(false); theSourceResource - .addLink() - .setType(Patient.LinkType.REPLACEDBY) - .setOther(new Reference(theTargetResource.getIdElement().toVersionless())); + .addLink() + .setType(Patient.LinkType.REPLACEDBY) + .setOther(new Reference(theTargetResource.getIdElement().toVersionless())); } private Patient prepareTargetPatientForUpdate( - Patient theTargetResource, - Patient theSourceResource, - @Nullable Patient theResultResource, - boolean theDeleteSource) { + Patient theTargetResource, + Patient theSourceResource, + @Nullable Patient theResultResource, + boolean theDeleteSource) { // if the client provided a result resource as input then use it to update the target resource if (theResultResource != null) { @@ -405,9 +407,9 @@ private Patient prepareTargetPatientForUpdate( // add the replaces link to the target resource, if the source resource is not to be deleted if (!theDeleteSource) { theTargetResource - .addLink() - .setType(Patient.LinkType.REPLACES) - .setOther(new Reference(theSourceResource.getIdElement().toVersionless())); + .addLink() + .setType(Patient.LinkType.REPLACES) + .setOther(new Reference(theSourceResource.getIdElement().toVersionless())); } // copy all identifiers from the source to the target @@ -468,59 +470,59 @@ private void deleteResource(Patient theResource, RequestDetails theRequestDetail * @return true if the parameters are valid, false otherwise */ private boolean validateMergeOperationParameters( - MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { List errorMessages = new ArrayList<>(); if (!theMergeOperationParameters.hasAtLeastOneSourceIdentifier() - && theMergeOperationParameters.getSourceResource() == null) { + && theMergeOperationParameters.getSourceResource() == null) { String msg = String.format( - "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", - theMergeOperationParameters.getSourceResourceParameterName(), - theMergeOperationParameters.getSourceIdentifiersParameterName()); + "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); errorMessages.add(msg); } // Spec has conflicting information about this case if (theMergeOperationParameters.hasAtLeastOneSourceIdentifier() - && theMergeOperationParameters.getSourceResource() != null) { + && theMergeOperationParameters.getSourceResource() != null) { String msg = String.format( - "Source resource must be provided either by '%s' or by '%s', not both.", - theMergeOperationParameters.getSourceResourceParameterName(), - theMergeOperationParameters.getSourceIdentifiersParameterName()); + "Source resource must be provided either by '%s' or by '%s', not both.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); errorMessages.add(msg); } if (!theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && theMergeOperationParameters.getTargetResource() == null) { + && theMergeOperationParameters.getTargetResource() == null) { String msg = String.format( - "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", - theMergeOperationParameters.getTargetResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); errorMessages.add(msg); } // Spec has conflicting information about this case if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && theMergeOperationParameters.getTargetResource() != null) { + && theMergeOperationParameters.getTargetResource() != null) { String msg = String.format( - "Target resource must be provided either by '%s' or by '%s', not both.", - theMergeOperationParameters.getTargetResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "Target resource must be provided either by '%s' or by '%s', not both.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); errorMessages.add(msg); } Reference sourceRef = (Reference) theMergeOperationParameters.getSourceResource(); if (sourceRef != null && !sourceRef.hasReference()) { String msg = String.format( - "Reference specified in '%s' parameter does not have a reference element.", - theMergeOperationParameters.getSourceResourceParameterName()); + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getSourceResourceParameterName()); errorMessages.add(msg); } Reference targetRef = (Reference) theMergeOperationParameters.getTargetResource(); if (targetRef != null && !targetRef.hasReference()) { String msg = String.format( - "Reference specified in '%s' parameter does not have a reference element.", - theMergeOperationParameters.getTargetResourceParameterName()); + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getTargetResourceParameterName()); errorMessages.add(msg); } @@ -537,43 +539,43 @@ private boolean validateMergeOperationParameters( } private IBaseResource resolveSourceResource( - MergeOperationInputParameters theOperationParameters, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { return resolveResource( - theOperationParameters.getSourceResource(), - theOperationParameters.getSourceIdentifiers(), - theRequestDetails, - theOutcome, - theOperationParameters.getSourceResourceParameterName(), - theOperationParameters.getSourceIdentifiersParameterName()); + theOperationParameters.getSourceResource(), + theOperationParameters.getSourceIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getSourceResourceParameterName(), + theOperationParameters.getSourceIdentifiersParameterName()); } private IBaseResource resolveTargetResource( - MergeOperationInputParameters theOperationParameters, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { return resolveResource( - theOperationParameters.getTargetResource(), - theOperationParameters.getTargetIdentifiers(), - theRequestDetails, - theOutcome, - theOperationParameters.getTargetResourceParameterName(), - theOperationParameters.getTargetIdentifiersParameterName()); + theOperationParameters.getTargetResource(), + theOperationParameters.getTargetIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getTargetResourceParameterName(), + theOperationParameters.getTargetIdentifiersParameterName()); } private IBaseResource resolveResourceByIdentifiers( - List theIdentifiers, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationParameterName) { + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { SearchParameterMap searchParameterMap = new SearchParameterMap(); TokenAndListParam tokenAndListParam = new TokenAndListParam(); for (CanonicalIdentifier identifier : theIdentifiers) { TokenParam tokenParam = new TokenParam( - identifier.getSystemElement().getValueAsString(), - identifier.getValueElement().getValueAsString()); + identifier.getSystemElement().getValueAsString(), + identifier.getValueElement().getValueAsString()); tokenAndListParam.addAnd(tokenParam); } searchParameterMap.add("identifier", tokenAndListParam); @@ -583,13 +585,13 @@ private IBaseResource resolveResourceByIdentifiers( List resources = bundle.getAllResources(); if (resources.isEmpty()) { String msg = String.format( - "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); + "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "not-found"); return null; } if (resources.size() > 1) { String msg = String.format( - "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); + "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "multiple-matches"); return null; } @@ -598,10 +600,10 @@ private IBaseResource resolveResourceByIdentifiers( } private IBaseResource resolveResourceByReference( - IBaseReference theReference, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationParameterName) { + IBaseReference theReference, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { // TODO Emre: why does IBaseReference not have getIdentifier or hasReference methods? // casting it to r4.Reference for now Reference r4ref = (Reference) theReference; @@ -612,19 +614,19 @@ private IBaseResource resolveResourceByReference( resource = myDao.read(theResourceId.toVersionless(), theRequestDetails); } catch (ResourceNotFoundException e) { String msg = String.format( - "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); + "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "not-found"); return null; } if (theResourceId.hasVersionIdPart() - && !theResourceId - .getVersionIdPart() - .equals(resource.getIdElement().getVersionIdPart())) { + && !theResourceId + .getVersionIdPart() + .equals(resource.getIdElement().getVersionIdPart())) { String msg = String.format( - "The reference in '%s' parameter has a version specified, " - + "but it is not the latest version of the resource", - theOperationParameterName); + "The reference in '%s' parameter has a version specified, " + + "but it is not the latest version of the resource", + theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "conflict"); return null; } @@ -633,25 +635,25 @@ private IBaseResource resolveResourceByReference( } private IBaseResource resolveResource( - IBaseReference theReference, - List theIdentifiers, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationReferenceParameterName, - String theOperationIdentifiersParameterName) { + IBaseReference theReference, + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationReferenceParameterName, + String theOperationIdentifiersParameterName) { if (theReference != null) { return resolveResourceByReference( - theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); + theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); } return resolveResourceByIdentifiers( - theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); + theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); } private void addInfoToOperationOutcome( - IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { + IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { IBase issue = - OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); OperationOutcomeUtil.addDetailsToIssue(myFhirContext, issue, null, null, theDetailsText); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java index 361797ff69a4..1de8e442d8c4 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java @@ -26,7 +26,11 @@ public class ReplaceReferenceRequest { public final RequestPartitionId partitionId; - public ReplaceReferenceRequest(@Nonnull IIdType theSourceId, @Nonnull IIdType theTargetId, int theBatchSize, RequestPartitionId thePartitionId) { + public ReplaceReferenceRequest( + @Nonnull IIdType theSourceId, + @Nonnull IIdType theTargetId, + int theBatchSize, + RequestPartitionId thePartitionId) { sourceId = theSourceId.toUnqualifiedVersionless(); targetId = theTargetId.toUnqualifiedVersionless(); batchSize = theBatchSize; From 33c8c15e416b4130b58f54e957b9a16064ed6b94 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Tue, 10 Dec 2024 15:20:48 -0500 Subject: [PATCH 061/148] update test to add link to result-patient only if the source is not to be deleted --- .../fhir/jpa/provider/r4/PatientMergeR4Test.java | 2 +- .../ReplaceReferencesTestHelper.java | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 5bc6e99cdfe2..5e5f2c02dacd 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -102,7 +102,7 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa myTestHelper.setSourceAndTarget(inParams); inParams.deleteSource = withDelete; if (withInputResultPatient) { - myTestHelper.setResultPatient(inParams); + myTestHelper.setResultPatient(inParams, withDelete); } if (withPreview) { inParams.preview = true; diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index 69300a98e426..f0d6d211d8d1 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -139,9 +139,7 @@ public void beforeEach() throws Exception { myResultPatient = new Patient(); myResultPatient.setIdElement((IdType) myTargetPatientId); myResultPatient.addIdentifier(pat1IdentifierA); - Patient.PatientLinkComponent link = myResultPatient.addLink(); - link.setOther(new Reference(mySourcePatientId)); - link.setType(Patient.LinkType.REPLACES); + } public void setSourceAndTarget(PatientMergeInputParameters inParams) { @@ -149,7 +147,14 @@ public void setSourceAndTarget(PatientMergeInputParameters inParams) { inParams.targetPatient = new Reference().setReferenceElement(myTargetPatientId); } - public void setResultPatient(PatientMergeInputParameters theInParams) { + public void setResultPatient(PatientMergeInputParameters theInParams, boolean theWithDelete) { + if (!theWithDelete) + { + // add the link only if we are not deleting the source + Patient.PatientLinkComponent link = myResultPatient.addLink(); + link.setOther(new Reference(mySourcePatientId)); + link.setType(Patient.LinkType.REPLACES); + } theInParams.resultPatient = myResultPatient; } From 457b0b45e71f03609fff99615081004124c230a8 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 10 Dec 2024 15:58:00 -0500 Subject: [PATCH 062/148] batch passes test --- .../provider/r4/ReplaceReferencesR4Test.java | 23 +----- .../ReplaceReferencesBatchTest.java | 23 +++++- .../ReplaceReferencesTestHelper.java | 33 ++++++++ .../export/BulkExportCreateReportStep.java | 36 ++++----- .../ReplaceReferencePatchOutcomeJson.java | 24 ++++++ .../ReplaceReferenceResults.java | 5 -- .../ReplaceReferenceResultsJson.java | 20 +++++ .../ReplaceReferenceUpdateStep.java | 10 ++- ...ReplaceReferenceUpdateTaskReducerStep.java | 76 +++++++++++++++++++ .../ReplaceReferenceUpdateTaskStep.java | 25 ------ .../ReplaceReferencesAppCtx.java | 54 ++++++------- .../ReplaceReferencesJobParameters.java | 25 +++++- .../ReplaceReferencesQueryIdsStep.java | 46 +++++------ .../fhir/batch2/api/IReductionStepWorker.java | 3 +- 14 files changed, 275 insertions(+), 128 deletions(-) create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencePatchOutcomeJson.java delete mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResults.java create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResultsJson.java create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java delete mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index dee13fcabd29..d0837e3c407d 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -52,27 +52,7 @@ void testReplaceReferences(boolean isAsync) throws IOException { ourLog.info("Got task {}", task.getId()); await().until(() -> myTestHelper.taskCompleted(task.getIdElement())); - Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); - ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); - - Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); - - // Assert on the output type - Coding taskType = taskOutput.getType().getCodingFirstRep(); - assertEquals(RESOURCE_TYPES_SYSTEM, taskType.getSystem()); - assertEquals("Bundle", taskType.getCode()); - - List containedResources = taskWithOutput.getContained(); - assertThat(containedResources) - .hasSize(1) - .element(0) - .isInstanceOf(Bundle.class); - - Bundle containedBundle = (Bundle) containedResources.get(0); - - Reference outputRef = (Reference) taskOutput.getValue(); - patchResultBundle = (Bundle) outputRef.getResource(); - assertTrue(containedBundle.equalsDeep(patchResultBundle)); + patchResultBundle = myTestHelper.validateCompletedTask(task.getIdElement()); } else { patchResultBundle = (Bundle) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME).getResource(); } @@ -85,6 +65,7 @@ void testReplaceReferences(boolean isAsync) throws IOException { myTestHelper.assertAllReferencesUpdated(); } + @ParameterizedTest @ValueSource(booleans = {false, true}) void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java index 60e4584625cf..058ba01091d0 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java @@ -2,6 +2,7 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceResultsJson; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; @@ -10,12 +11,18 @@ import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; import ca.uhn.fhir.jpa.test.BaseJpaR4Test; import ca.uhn.fhir.jpa.test.Batch2JobHelper; +import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.util.JsonUtil; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Task; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import static ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx.JOB_REPLACE_REFERENCES; +import static org.junit.jupiter.api.Assertions.assertEquals; public class ReplaceReferencesBatchTest extends BaseJpaR4Test { @@ -43,18 +50,32 @@ public void before() throws Exception { @Test public void testHappyPath() { + IIdType taskId = createReplaceReferencesTask(); + ReplaceReferencesJobParameters jobParams = new ReplaceReferencesJobParameters(); jobParams.setSourceId(new FhirIdJson(myTestHelper.mySourcePatientId)); jobParams.setTargetId(new FhirIdJson(myTestHelper.myTargetPatientId)); + jobParams.setTaskId(taskId); JobInstanceStartRequest request = new JobInstanceStartRequest(JOB_REPLACE_REFERENCES, jobParams); Batch2JobStartResponse jobStartResponse = myJobCoordinator.startInstance(mySrd, request); JobInstance jobInstance = myBatch2JobHelper.awaitJobCompletion(jobStartResponse); // FIXME KHS assert outcome - jobInstance.getReport(); + String report = jobInstance.getReport(); + ReplaceReferenceResultsJson replaceReferenceResultsJson = JsonUtil.deserialize(report, ReplaceReferenceResultsJson.class); + IdDt resultTaskId = replaceReferenceResultsJson.getTaskId().asIdDt(); + assertEquals(taskId.getIdPart(), resultTaskId.getIdPart()); + Bundle patchResultBundle = myTestHelper.validateCompletedTask(taskId); + myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES); myTestHelper.assertAllReferencesUpdated(); } + + private IIdType createReplaceReferencesTask() { + Task task = new Task(); + task.setStatus(Task.TaskStatus.INPROGRESS); + return myTaskDao.create(task, mySrd).getId().toUnqualifiedVersionless(); + } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index 69300a98e426..58aa67c81c70 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -15,6 +15,7 @@ import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CarePlan; +import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; @@ -25,6 +26,7 @@ import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Task; import org.hl7.fhir.r4.model.Type; @@ -32,15 +34,19 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.List; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; +import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER_RESPOND_ASYNC; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public class ReplaceReferencesTestHelper { private static final Logger ourLog = LoggerFactory.getLogger(ReplaceReferencesTestHelper.class); @@ -320,4 +326,31 @@ public void validatePatchResultBundle(Bundle patchResultBundle, int theTotalExpe .satisfies(diagnostics -> assertThat(diagnostics).matches(expectedPatchIssuePattern)))); } + public Bundle validateCompletedTask(IIdType theTaskId) { + Bundle patchResultBundle; + Task taskWithOutput = myTaskDao.read(theTaskId, mySrd); + ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); + + Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); + + // Assert on the output type + Coding taskType = taskOutput.getType().getCodingFirstRep(); + assertEquals(RESOURCE_TYPES_SYSTEM, taskType.getSystem()); + assertEquals("Bundle", taskType.getCode()); + + List containedResources = taskWithOutput.getContained(); + assertThat(containedResources) + .hasSize(1) + .element(0) + .isInstanceOf(Bundle.class); + + Bundle containedBundle = (Bundle) containedResources.get(0); + + Reference outputRef = (Reference) taskOutput.getValue(); + patchResultBundle = (Bundle) outputRef.getResource(); + assertTrue(containedBundle.equalsDeep(patchResultBundle)); + return patchResultBundle; + } + + } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportCreateReportStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportCreateReportStep.java index 65d7d2af177f..102831f8c238 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportCreateReportStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportCreateReportStep.java @@ -47,6 +47,22 @@ public class BulkExportCreateReportStep private Map> myResourceToBinaryIds; + @Nonnull + @Override + public ChunkOutcome consume( + ChunkExecutionDetails theChunkDetails) { + BulkExportBinaryFileId fileId = theChunkDetails.getData(); + if (myResourceToBinaryIds == null) { + myResourceToBinaryIds = new HashMap<>(); + } + + myResourceToBinaryIds.putIfAbsent(fileId.getResourceType(), new ArrayList<>()); + + myResourceToBinaryIds.get(fileId.getResourceType()).add(fileId.getBinaryId()); + + return ChunkOutcome.SUCCESS(); + } + @Nonnull @Override public RunOutcome run( @@ -79,25 +95,9 @@ public RunOutcome run( return RunOutcome.SUCCESS; } - @Nonnull - @Override - public ChunkOutcome consume( - ChunkExecutionDetails theChunkDetails) { - BulkExportBinaryFileId fileId = theChunkDetails.getData(); - if (myResourceToBinaryIds == null) { - myResourceToBinaryIds = new HashMap<>(); - } - - myResourceToBinaryIds.putIfAbsent(fileId.getResourceType(), new ArrayList<>()); - - myResourceToBinaryIds.get(fileId.getResourceType()).add(fileId.getBinaryId()); - - return ChunkOutcome.SUCCESS(); - } - private static String getOriginatingRequestUrl( - @Nonnull StepExecutionDetails theStepExecutionDetails, - BulkExportJobResults results) { + @Nonnull StepExecutionDetails theStepExecutionDetails, + BulkExportJobResults results) { IJobInstance instance = theStepExecutionDetails.getInstance(); String url = ""; if (instance instanceof JobInstance) { diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencePatchOutcomeJson.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencePatchOutcomeJson.java new file mode 100644 index 000000000000..17f97e3daee1 --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencePatchOutcomeJson.java @@ -0,0 +1,24 @@ +package ca.uhn.fhir.batch2.jobs.replacereferences; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.IModelJson; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.hl7.fhir.r4.model.Bundle; + +public class ReplaceReferencePatchOutcomeJson implements IModelJson { + @JsonProperty("patchResponseBundle") + String myPatchResponseBundle; + + public ReplaceReferencePatchOutcomeJson() {} + + public ReplaceReferencePatchOutcomeJson(FhirContext theFhirContext, Bundle theResult) { + myPatchResponseBundle = theFhirContext.newJsonParser().encodeResourceToString(theResult); + } + public String getPatchResponseBundle() { + return myPatchResponseBundle; + } + + public void setPatchResponseBundle(String thePatchResponseBundle) { + myPatchResponseBundle = thePatchResponseBundle; + } +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResults.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResults.java deleted file mode 100644 index 22133a3910fc..000000000000 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResults.java +++ /dev/null @@ -1,5 +0,0 @@ -package ca.uhn.fhir.batch2.jobs.replacereferences; - -import ca.uhn.fhir.model.api.IModelJson; - -public class ReplaceReferenceResults implements IModelJson {} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResultsJson.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResultsJson.java new file mode 100644 index 000000000000..d3caf23c7c4c --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResultsJson.java @@ -0,0 +1,20 @@ +package ca.uhn.fhir.batch2.jobs.replacereferences; + +import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; +import ca.uhn.fhir.model.api.IModelJson; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ReplaceReferenceResultsJson implements IModelJson { + @JsonProperty("taskId") + private FhirIdJson myTaskId; + + public ReplaceReferenceResultsJson() {} + + public void setTaskId(FhirIdJson theTaskId) { + myTaskId = theTaskId; + } + + public FhirIdJson getTaskId() { + return myTaskId; + } +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java index 23c79315eb86..9c5337e6b97c 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java @@ -35,7 +35,7 @@ import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_VALUE; public class ReplaceReferenceUpdateStep - implements IJobStepWorker { + implements IJobStepWorker { private final FhirContext myFhirContext; private final DaoRegistry myDaoRegistry; @@ -51,7 +51,7 @@ public RunOutcome run( @Nonnull StepExecutionDetails theStepExecutionDetails, - @Nonnull IJobDataSink theDataSink) + @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); @@ -64,8 +64,10 @@ public RunOutcome run( // TODO KHS shouldn't transaction response bundles have ids? result.setId(UUID.randomUUID().toString()); - RunOutcome retval = new RunOutcome(0); - return retval; + ReplaceReferencePatchOutcomeJson data = new ReplaceReferencePatchOutcomeJson(myFhirContext, result); + theDataSink.accept(data); + + return new RunOutcome(result.getEntry().size()); } private Bundle buildPatchBundle( diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java new file mode 100644 index 000000000000..9f45af25ccdc --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java @@ -0,0 +1,76 @@ +package ca.uhn.fhir.batch2.jobs.replacereferences; + +import ca.uhn.fhir.batch2.api.ChunkExecutionDetails; +import ca.uhn.fhir.batch2.api.IJobDataSink; +import ca.uhn.fhir.batch2.api.IReductionStepWorker; +import ca.uhn.fhir.batch2.api.JobExecutionFailedException; +import ca.uhn.fhir.batch2.api.RunOutcome; +import ca.uhn.fhir.batch2.api.StepExecutionDetails; +import ca.uhn.fhir.batch2.model.ChunkOutcome; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Task; + +import java.util.ArrayList; +import java.util.List; + +public class ReplaceReferenceUpdateTaskReducerStep + implements IReductionStepWorker { + public static final String RESOURCE_TYPES_SYSTEM = "http://hl7.org/fhir/ValueSet/resource-types"; + + private final FhirContext myFhirContext; + private final DaoRegistry myDaoRegistry; + + private List myPatchOutputBundles = new ArrayList<>(); + + public ReplaceReferenceUpdateTaskReducerStep(FhirContext theFhirContext, DaoRegistry theDaoRegistry) { + myFhirContext = theFhirContext; + myDaoRegistry = theDaoRegistry; + } + + @Nonnull + @Override + public ChunkOutcome consume(ChunkExecutionDetails theChunkDetails) { + ReplaceReferencePatchOutcomeJson result = theChunkDetails.getData(); + Bundle patchOutputBundle = myFhirContext.newJsonParser().parseResource(Bundle.class, result.getPatchResponseBundle()); + myPatchOutputBundles.add(patchOutputBundle); + return ChunkOutcome.SUCCESS(); + } + + + @Nonnull + @Override + public RunOutcome run(@Nonnull StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { + + ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); + SystemRequestDetails requestDetails = SystemRequestDetails.forRequestPartitionId(params.getPartitionId()); + Task task = myDaoRegistry.getResourceDao(Task.class).read(params.getTaskId().asIdDt(), requestDetails); + + task.setStatus(Task.TaskStatus.COMPLETED); + myPatchOutputBundles.forEach(outputBundle -> { + Task.TaskOutputComponent output = task.addOutput(); + Coding coding = output.getType().getCodingFirstRep(); + coding.setSystem(RESOURCE_TYPES_SYSTEM); + coding.setCode("Bundle"); + Reference outputBundleReference = + new Reference("#" + outputBundle.getIdElement().getIdPart()); + output.setValue(outputBundleReference); + task.addContained(outputBundle); + }); + + myDaoRegistry.getResourceDao(Task.class).update(task, requestDetails); + + ReplaceReferenceResultsJson result = new ReplaceReferenceResultsJson(); + result.setTaskId(params.getTaskId()); + theDataSink.accept(result); + + return new RunOutcome(myPatchOutputBundles.size()); + } + + +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java deleted file mode 100644 index de08cbaf096d..000000000000 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskStep.java +++ /dev/null @@ -1,25 +0,0 @@ -package ca.uhn.fhir.batch2.jobs.replacereferences; - -import ca.uhn.fhir.batch2.api.IJobDataSink; -import ca.uhn.fhir.batch2.api.IJobStepWorker; -import ca.uhn.fhir.batch2.api.JobExecutionFailedException; -import ca.uhn.fhir.batch2.api.RunOutcome; -import ca.uhn.fhir.batch2.api.StepExecutionDetails; -import ca.uhn.fhir.batch2.api.VoidModel; -import jakarta.annotation.Nonnull; - -public class ReplaceReferenceUpdateTaskStep - implements IJobStepWorker { - @Nonnull - @Override - public RunOutcome run( - @Nonnull - StepExecutionDetails - theStepExecutionDetails, - @Nonnull IJobDataSink theDataSink) - throws JobExecutionFailedException { - // FIXME KHS - RunOutcome retval = new RunOutcome(0); - return retval; - } -} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java index bab9ac6d7d50..699a018e47c6 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java @@ -15,45 +15,47 @@ public class ReplaceReferencesAppCtx { @Bean public JobDefinition bulkImport2JobDefinition( - ReplaceReferencesQueryIdsStep theReplaceReferencesQueryIds, - ReplaceReferenceUpdateStep theReplaceReferenceUpdateStep, - ReplaceReferenceUpdateTaskStep theReplaceReferenceUpdateTaskStep) { + ReplaceReferencesQueryIdsStep theReplaceReferencesQueryIds, + ReplaceReferenceUpdateStep theReplaceReferenceUpdateStep, + ReplaceReferenceUpdateTaskReducerStep theReplaceReferenceUpdateTaskReducerStep) { return JobDefinition.newBuilder() - .setJobDefinitionId(JOB_REPLACE_REFERENCES) - .setJobDescription("Replace References") - .setJobDefinitionVersion(1) - .setParametersType(ReplaceReferencesJobParameters.class) - .addFirstStep( - "query-ids", - "Query IDs of resources that link to the source resource", - FhirIdListWorkChunkJson.class, - theReplaceReferencesQueryIds) - .addIntermediateStep( - "replace-references", - "Update all references from pointing to source to pointing to target", - ReplaceReferenceResults.class, - theReplaceReferenceUpdateStep) - .addLastStep( - "update-task", - "Waits for replace reference work to complete and updates Task.", - theReplaceReferenceUpdateTaskStep) - .build(); + .setJobDefinitionId(JOB_REPLACE_REFERENCES) + .setJobDescription("Replace References") + .setJobDefinitionVersion(1) + .gatedExecution() + .setParametersType(ReplaceReferencesJobParameters.class) + .addFirstStep( + "query-ids", + "Query IDs of resources that link to the source resource", + FhirIdListWorkChunkJson.class, + theReplaceReferencesQueryIds) + .addIntermediateStep( + "replace-references", + "Update all references from pointing to source to pointing to target", + ReplaceReferencePatchOutcomeJson.class, + theReplaceReferenceUpdateStep) + .addFinalReducerStep( + "update-task", + "Waits for replace reference work to complete and updates Task.", + ReplaceReferenceResultsJson.class, + theReplaceReferenceUpdateTaskReducerStep) + .build(); } @Bean public ReplaceReferencesQueryIdsStep replaceReferencesQueryIdsStep( - HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { + HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { return new ReplaceReferencesQueryIdsStep(theHapiTransactionService, theBatch2DaoSvc); } @Bean public ReplaceReferenceUpdateStep replaceReferenceUpdateStep( - FhirContext theFhirContext, DaoRegistry theDaoRegistry) { + FhirContext theFhirContext, DaoRegistry theDaoRegistry) { return new ReplaceReferenceUpdateStep(theFhirContext, theDaoRegistry); } @Bean - public ReplaceReferenceUpdateTaskStep replaceReferenceUpdateTaskStep() { - return new ReplaceReferenceUpdateTaskStep(); + public ReplaceReferenceUpdateTaskReducerStep replaceReferenceUpdateTaskStep(FhirContext theFhirContext, DaoRegistry theDaoRegistry) { + return new ReplaceReferenceUpdateTaskReducerStep(theFhirContext, theDaoRegistry); } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java index 466cc8035500..46b3048e92e3 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java @@ -5,10 +5,15 @@ import ca.uhn.fhir.jpa.provider.ReplaceReferenceRequest; import ca.uhn.fhir.model.api.BaseBatchJobParameters; import com.fasterxml.jackson.annotation.JsonProperty; +import org.hl7.fhir.instance.model.api.IIdType; +import static ca.uhn.fhir.jpa.api.config.JpaStorageSettings.DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE; import static ca.uhn.fhir.jpa.api.config.JpaStorageSettings.DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING; public class ReplaceReferencesJobParameters extends BaseBatchJobParameters { + @JsonProperty("taskId") + private FhirIdJson myTaskId; + @JsonProperty("sourceId") private FhirIdJson mySourceId; @@ -16,15 +21,16 @@ public class ReplaceReferencesJobParameters extends BaseBatchJobParameters { private FhirIdJson myTargetId; @JsonProperty( - value = "batchSize", - defaultValue = DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING, - required = false) + value = "batchSize", + defaultValue = DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING, + required = false) private int myBatchSize; @JsonProperty("partitionId") private RequestPartitionId myPartitionId; - public ReplaceReferencesJobParameters() {} + public ReplaceReferencesJobParameters() { + } public ReplaceReferencesJobParameters(ReplaceReferenceRequest theRequest) { mySourceId = new FhirIdJson(theRequest.sourceId); @@ -50,6 +56,9 @@ public void setTargetId(FhirIdJson theTargetId) { } public int getBatchSize() { + if (myBatchSize <= 0) { + myBatchSize = DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE; + } return myBatchSize; } @@ -64,4 +73,12 @@ public RequestPartitionId getPartitionId() { public void setPartitionId(RequestPartitionId thePartitionId) { myPartitionId = thePartitionId; } + + public void setTaskId(IIdType theTaskId) { + myTaskId = new FhirIdJson(theTaskId); + } + + public FhirIdJson getTaskId() { + return myTaskId; + } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java index a58049ccd9d9..a80aa22ae72c 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java @@ -8,20 +8,24 @@ import ca.uhn.fhir.batch2.api.VoidModel; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdListWorkChunkJson; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; +import ca.uhn.fhir.util.StreamUtil; import jakarta.annotation.Nonnull; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; public class ReplaceReferencesQueryIdsStep - implements IJobStepWorker { + implements IJobStepWorker { private final HapiTransactionService myHapiTransactionService; private final IBatch2DaoSvc myBatch2DaoSvc; public ReplaceReferencesQueryIdsStep( - HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { + HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { myHapiTransactionService = theHapiTransactionService; myBatch2DaoSvc = theBatch2DaoSvc; } @@ -29,38 +33,34 @@ public ReplaceReferencesQueryIdsStep( @Nonnull @Override public RunOutcome run( - @Nonnull StepExecutionDetails theStepExecutionDetails, - @Nonnull IJobDataSink theDataSink) - throws JobExecutionFailedException { + @Nonnull StepExecutionDetails theStepExecutionDetails, + @Nonnull IJobDataSink theDataSink) + throws JobExecutionFailedException { ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); // Warning: It is a little confusing that source/target are reversed in the resource link table from the meaning // in // the replace references request - FhirIdListWorkChunkJson chunk = new FhirIdListWorkChunkJson(params.getBatchSize(), params.getPartitionId()); AtomicInteger totalCount = new AtomicInteger(); myHapiTransactionService - .withSystemRequestOnPartition(params.getPartitionId()) - .execute(() -> myBatch2DaoSvc - .streamSourceIdsThatReferenceTargetId( - params.getSourceId().asIdDt()) - .map(FhirIdJson::new) - .forEach(id -> { - chunk.add(id); - if (chunk.size() == params.getBatchSize()) { - totalCount.addAndGet(processChunk(theDataSink, chunk)); - chunk.clear(); - } - })); - if (!chunk.isEmpty()) { - totalCount.addAndGet(processChunk(theDataSink, chunk)); - } + .withSystemRequestOnPartition(params.getPartitionId()) + .readOnly() + .execute(() -> { + Stream stream = myBatch2DaoSvc + .streamSourceIdsThatReferenceTargetId( + params.getSourceId().asIdDt()) + .map(FhirIdJson::new); + + StreamUtil.partition(stream, params.getBatchSize()).forEach(chunk -> totalCount.addAndGet(processChunk(theDataSink, chunk, params.getPartitionId()))); + }); + return new RunOutcome(totalCount.get()); } - private int processChunk(IJobDataSink theDataSink, FhirIdListWorkChunkJson theChunk) { - theDataSink.accept(theChunk); + private int processChunk(IJobDataSink theDataSink, List theChunk, RequestPartitionId theRequestPartitionId) { + FhirIdListWorkChunkJson data = new FhirIdListWorkChunkJson(theChunk, theRequestPartitionId); + theDataSink.accept(data); return theChunk.size(); } } diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/api/IReductionStepWorker.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/api/IReductionStepWorker.java index 9c82b3b38257..6c5a7d14f553 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/api/IReductionStepWorker.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/api/IReductionStepWorker.java @@ -24,7 +24,8 @@ import jakarta.annotation.Nonnull; /** - * Reduction step worker. + * Reduction step worker. Once all chunks from the previous step have completed, consume() will first be called on + * all chunks, and then funally run() will be called on this step. * @param Job Parameter Type * @param Input Parameter type (real input for step is ListResult of IT * @param Output Job Report Type From 9896b87862fad1b53be040b126bd8e04b34a8c49 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 10 Dec 2024 15:58:20 -0500 Subject: [PATCH 063/148] batch passes test --- .../export/BulkExportCreateReportStep.java | 6 +- .../ReplaceReferencePatchOutcomeJson.java | 1 + .../ReplaceReferenceUpdateStep.java | 3 +- ...ReplaceReferenceUpdateTaskReducerStep.java | 24 +++++--- .../ReplaceReferencesAppCtx.java | 55 ++++++++++--------- .../ReplaceReferencesJobParameters.java | 9 ++- .../ReplaceReferencesQueryIdsStep.java | 35 +++++++----- 7 files changed, 73 insertions(+), 60 deletions(-) diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportCreateReportStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportCreateReportStep.java index 102831f8c238..35b86c0130f0 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportCreateReportStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportCreateReportStep.java @@ -50,7 +50,7 @@ public class BulkExportCreateReportStep @Nonnull @Override public ChunkOutcome consume( - ChunkExecutionDetails theChunkDetails) { + ChunkExecutionDetails theChunkDetails) { BulkExportBinaryFileId fileId = theChunkDetails.getData(); if (myResourceToBinaryIds == null) { myResourceToBinaryIds = new HashMap<>(); @@ -96,8 +96,8 @@ public RunOutcome run( } private static String getOriginatingRequestUrl( - @Nonnull StepExecutionDetails theStepExecutionDetails, - BulkExportJobResults results) { + @Nonnull StepExecutionDetails theStepExecutionDetails, + BulkExportJobResults results) { IJobInstance instance = theStepExecutionDetails.getInstance(); String url = ""; if (instance instanceof JobInstance) { diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencePatchOutcomeJson.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencePatchOutcomeJson.java index 17f97e3daee1..8e6b4e867ab7 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencePatchOutcomeJson.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencePatchOutcomeJson.java @@ -14,6 +14,7 @@ public ReplaceReferencePatchOutcomeJson() {} public ReplaceReferencePatchOutcomeJson(FhirContext theFhirContext, Bundle theResult) { myPatchResponseBundle = theFhirContext.newJsonParser().encodeResourceToString(theResult); } + public String getPatchResponseBundle() { return myPatchResponseBundle; } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java index 9c5337e6b97c..903d153cf116 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java @@ -35,7 +35,8 @@ import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_VALUE; public class ReplaceReferenceUpdateStep - implements IJobStepWorker { + implements IJobStepWorker< + ReplaceReferencesJobParameters, FhirIdListWorkChunkJson, ReplaceReferencePatchOutcomeJson> { private final FhirContext myFhirContext; private final DaoRegistry myDaoRegistry; diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java index 9f45af25ccdc..e49c85bfeb0b 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java @@ -20,7 +20,8 @@ import java.util.List; public class ReplaceReferenceUpdateTaskReducerStep - implements IReductionStepWorker { + implements IReductionStepWorker< + ReplaceReferencesJobParameters, ReplaceReferencePatchOutcomeJson, ReplaceReferenceResultsJson> { public static final String RESOURCE_TYPES_SYSTEM = "http://hl7.org/fhir/ValueSet/resource-types"; private final FhirContext myFhirContext; @@ -35,21 +36,28 @@ public ReplaceReferenceUpdateTaskReducerStep(FhirContext theFhirContext, DaoRegi @Nonnull @Override - public ChunkOutcome consume(ChunkExecutionDetails theChunkDetails) { + public ChunkOutcome consume( + ChunkExecutionDetails theChunkDetails) { ReplaceReferencePatchOutcomeJson result = theChunkDetails.getData(); - Bundle patchOutputBundle = myFhirContext.newJsonParser().parseResource(Bundle.class, result.getPatchResponseBundle()); + Bundle patchOutputBundle = + myFhirContext.newJsonParser().parseResource(Bundle.class, result.getPatchResponseBundle()); myPatchOutputBundles.add(patchOutputBundle); return ChunkOutcome.SUCCESS(); } - @Nonnull @Override - public RunOutcome run(@Nonnull StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { + public RunOutcome run( + @Nonnull + StepExecutionDetails + theStepExecutionDetails, + @Nonnull IJobDataSink theDataSink) + throws JobExecutionFailedException { ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); SystemRequestDetails requestDetails = SystemRequestDetails.forRequestPartitionId(params.getPartitionId()); - Task task = myDaoRegistry.getResourceDao(Task.class).read(params.getTaskId().asIdDt(), requestDetails); + Task task = + myDaoRegistry.getResourceDao(Task.class).read(params.getTaskId().asIdDt(), requestDetails); task.setStatus(Task.TaskStatus.COMPLETED); myPatchOutputBundles.forEach(outputBundle -> { @@ -58,7 +66,7 @@ public RunOutcome run(@Nonnull StepExecutionDetails bulkImport2JobDefinition( - ReplaceReferencesQueryIdsStep theReplaceReferencesQueryIds, - ReplaceReferenceUpdateStep theReplaceReferenceUpdateStep, - ReplaceReferenceUpdateTaskReducerStep theReplaceReferenceUpdateTaskReducerStep) { + ReplaceReferencesQueryIdsStep theReplaceReferencesQueryIds, + ReplaceReferenceUpdateStep theReplaceReferenceUpdateStep, + ReplaceReferenceUpdateTaskReducerStep theReplaceReferenceUpdateTaskReducerStep) { return JobDefinition.newBuilder() - .setJobDefinitionId(JOB_REPLACE_REFERENCES) - .setJobDescription("Replace References") - .setJobDefinitionVersion(1) - .gatedExecution() - .setParametersType(ReplaceReferencesJobParameters.class) - .addFirstStep( - "query-ids", - "Query IDs of resources that link to the source resource", - FhirIdListWorkChunkJson.class, - theReplaceReferencesQueryIds) - .addIntermediateStep( - "replace-references", - "Update all references from pointing to source to pointing to target", - ReplaceReferencePatchOutcomeJson.class, - theReplaceReferenceUpdateStep) - .addFinalReducerStep( - "update-task", - "Waits for replace reference work to complete and updates Task.", - ReplaceReferenceResultsJson.class, - theReplaceReferenceUpdateTaskReducerStep) - .build(); + .setJobDefinitionId(JOB_REPLACE_REFERENCES) + .setJobDescription("Replace References") + .setJobDefinitionVersion(1) + .gatedExecution() + .setParametersType(ReplaceReferencesJobParameters.class) + .addFirstStep( + "query-ids", + "Query IDs of resources that link to the source resource", + FhirIdListWorkChunkJson.class, + theReplaceReferencesQueryIds) + .addIntermediateStep( + "replace-references", + "Update all references from pointing to source to pointing to target", + ReplaceReferencePatchOutcomeJson.class, + theReplaceReferenceUpdateStep) + .addFinalReducerStep( + "update-task", + "Waits for replace reference work to complete and updates Task.", + ReplaceReferenceResultsJson.class, + theReplaceReferenceUpdateTaskReducerStep) + .build(); } @Bean public ReplaceReferencesQueryIdsStep replaceReferencesQueryIdsStep( - HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { + HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { return new ReplaceReferencesQueryIdsStep(theHapiTransactionService, theBatch2DaoSvc); } @Bean public ReplaceReferenceUpdateStep replaceReferenceUpdateStep( - FhirContext theFhirContext, DaoRegistry theDaoRegistry) { + FhirContext theFhirContext, DaoRegistry theDaoRegistry) { return new ReplaceReferenceUpdateStep(theFhirContext, theDaoRegistry); } @Bean - public ReplaceReferenceUpdateTaskReducerStep replaceReferenceUpdateTaskStep(FhirContext theFhirContext, DaoRegistry theDaoRegistry) { + public ReplaceReferenceUpdateTaskReducerStep replaceReferenceUpdateTaskStep( + FhirContext theFhirContext, DaoRegistry theDaoRegistry) { return new ReplaceReferenceUpdateTaskReducerStep(theFhirContext, theDaoRegistry); } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java index 46b3048e92e3..ed70ccd3d84a 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java @@ -21,16 +21,15 @@ public class ReplaceReferencesJobParameters extends BaseBatchJobParameters { private FhirIdJson myTargetId; @JsonProperty( - value = "batchSize", - defaultValue = DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING, - required = false) + value = "batchSize", + defaultValue = DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING, + required = false) private int myBatchSize; @JsonProperty("partitionId") private RequestPartitionId myPartitionId; - public ReplaceReferencesJobParameters() { - } + public ReplaceReferencesJobParameters() {} public ReplaceReferencesJobParameters(ReplaceReferenceRequest theRequest) { mySourceId = new FhirIdJson(theRequest.sourceId); diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java index a80aa22ae72c..9124ba9a08ab 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java @@ -19,13 +19,13 @@ import java.util.stream.Stream; public class ReplaceReferencesQueryIdsStep - implements IJobStepWorker { + implements IJobStepWorker { private final HapiTransactionService myHapiTransactionService; private final IBatch2DaoSvc myBatch2DaoSvc; public ReplaceReferencesQueryIdsStep( - HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { + HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { myHapiTransactionService = theHapiTransactionService; myBatch2DaoSvc = theBatch2DaoSvc; } @@ -33,9 +33,9 @@ public ReplaceReferencesQueryIdsStep( @Nonnull @Override public RunOutcome run( - @Nonnull StepExecutionDetails theStepExecutionDetails, - @Nonnull IJobDataSink theDataSink) - throws JobExecutionFailedException { + @Nonnull StepExecutionDetails theStepExecutionDetails, + @Nonnull IJobDataSink theDataSink) + throws JobExecutionFailedException { ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); // Warning: It is a little confusing that source/target are reversed in the resource link table from the meaning @@ -44,21 +44,26 @@ public RunOutcome run( AtomicInteger totalCount = new AtomicInteger(); myHapiTransactionService - .withSystemRequestOnPartition(params.getPartitionId()) - .readOnly() - .execute(() -> { - Stream stream = myBatch2DaoSvc - .streamSourceIdsThatReferenceTargetId( - params.getSourceId().asIdDt()) - .map(FhirIdJson::new); + .withSystemRequestOnPartition(params.getPartitionId()) + .readOnly() + .execute(() -> { + Stream stream = myBatch2DaoSvc + .streamSourceIdsThatReferenceTargetId( + params.getSourceId().asIdDt()) + .map(FhirIdJson::new); - StreamUtil.partition(stream, params.getBatchSize()).forEach(chunk -> totalCount.addAndGet(processChunk(theDataSink, chunk, params.getPartitionId()))); - }); + StreamUtil.partition(stream, params.getBatchSize()) + .forEach(chunk -> + totalCount.addAndGet(processChunk(theDataSink, chunk, params.getPartitionId()))); + }); return new RunOutcome(totalCount.get()); } - private int processChunk(IJobDataSink theDataSink, List theChunk, RequestPartitionId theRequestPartitionId) { + private int processChunk( + IJobDataSink theDataSink, + List theChunk, + RequestPartitionId theRequestPartitionId) { FhirIdListWorkChunkJson data = new FhirIdListWorkChunkJson(theChunk, theRequestPartitionId); theDataSink.accept(data); return theChunk.size(); From d13729940f2ac6bc5519038a226ba3e7555fb3a4 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 10 Dec 2024 15:59:52 -0500 Subject: [PATCH 064/148] batch passes test --- .../replacereferences/ReplaceReferenceUpdateTaskReducerStep.java | 1 + 1 file changed, 1 insertion(+) diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java index e49c85bfeb0b..fadc794623c3 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java @@ -60,6 +60,7 @@ public RunOutcome run( myDaoRegistry.getResourceDao(Task.class).read(params.getTaskId().asIdDt(), requestDetails); task.setStatus(Task.TaskStatus.COMPLETED); + // TODO KHS this Task will probably be too large for large jobs. Revisit this model once we support Provenance resources. myPatchOutputBundles.forEach(outputBundle -> { Task.TaskOutputComponent output = task.addOutput(); Coding coding = output.getType().getCodingFirstRep(); From 9df9cf7427f0982a45921a9a5510609fdf09452b Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Wed, 11 Dec 2024 10:09:52 -0500 Subject: [PATCH 065/148] made patient-merge return the Task returned from replace references --- .../BaseJpaResourceProviderPatient.java | 8 +++++++ .../provider/ReplaceReferencesSvcImpl.java | 4 ++-- .../jpa/provider/r4/PatientMergeR4Test.java | 5 +++-- ...ReplaceReferenceUpdateTaskReducerStep.java | 3 ++- .../jpa/dao/merge/MergeOperationOutcome.java | 9 ++++++++ .../jpa/dao/merge/ResourceMergeService.java | 13 ++++++++++-- .../dao/merge/ResourceMergeServiceTest.java | 21 ++++++++++++++++++- 7 files changed, 55 insertions(+), 8 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 2d55232b77c6..df7649df194c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -352,6 +352,14 @@ private IBaseParameters buildMergeOperationOutputParameters( OPERATION_MERGE_OUTPUT_PARAM_RESULT, theMergeOutcome.getUpdatedTargetResource()); } + + if (theMergeOutcome.getTask() != null) { + ParametersUtil.addParameterToParameters( + theFhirContext, + retVal, + ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK, + theMergeOutcome.getTask()); + } return retVal; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 4f5dfe90b67e..38cb3d9a995f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -173,12 +173,12 @@ private void fakeBackgroundTaskUpdate( theTask.addContained(outputBundle); }); - myDaoRegistry.getResourceDao(Task.class).update(theTask, new SystemRequestDetails()); + myDaoRegistry.getResourceDao(Task.class).update(theTask, systemRequestDetails); ourLog.info("Updated task {} to COMPLETED.", theTask.getId()); } catch (Exception e) { ourLog.error("Patch failed", e); theTask.setStatus(Task.TaskStatus.FAILED); - myDaoRegistry.getResourceDao(Task.class).update(theTask, new SystemRequestDetails()); + myDaoRegistry.getResourceDao(Task.class).update(theTask, systemRequestDetails); ourLog.info("Updated task {} to FAILED.", theTask.getId()); } }); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 5e5f2c02dacd..bc32e53e54c3 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -60,7 +60,7 @@ public void after() throws Exception { super.after(); myStorageSettings.setReuseCachedSearchResultsForMillis(new JpaStorageSettings().getReuseCachedSearchResultsForMillis()); - } + } @Override @BeforeEach @@ -114,7 +114,8 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa Parameters outParams = callMergeOperation(inParameters, isAsync); // validate - assertThat(outParams.getParameter()).hasSize(3); + // in async mode, there will be an additional task in the output params + assertThat(outParams.getParameter()).hasSizeBetween(3, 4); // Assert input Parameters input = (Parameters) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_INPUT).getResource(); diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java index fadc794623c3..b6a6afe23d70 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java @@ -60,7 +60,8 @@ public RunOutcome run( myDaoRegistry.getResourceDao(Task.class).read(params.getTaskId().asIdDt(), requestDetails); task.setStatus(Task.TaskStatus.COMPLETED); - // TODO KHS this Task will probably be too large for large jobs. Revisit this model once we support Provenance resources. + // TODO KHS this Task will probably be too large for large jobs. Revisit this model once we support Provenance + // resources. myPatchOutputBundles.forEach(outputBundle -> { Task.TaskOutputComponent output = task.addOutput(); Coding coding = output.getType().getCodingFirstRep(); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java index f043eeeae116..a240521a9bfb 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java @@ -26,6 +26,7 @@ public class MergeOperationOutcome { private IBaseOperationOutcome myOperationOutcome; private int myHttpStatusCode; private IBaseResource myUpdatedTargetResource; + private IBaseResource myTask; public IBaseOperationOutcome getOperationOutcome() { return myOperationOutcome; @@ -50,4 +51,12 @@ public IBaseResource getUpdatedTargetResource() { public void setUpdatedTargetResource(IBaseResource theUpdatedTargetResource) { this.myUpdatedTargetResource = theUpdatedTargetResource; } + + public IBaseResource getTask() { + return myTask; + } + + public void setTask(IBaseResource theTask) { + this.myTask = theTask; + } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 1a30c34891af..1e7401deb200 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -45,6 +45,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; import org.slf4j.Logger; @@ -58,6 +59,7 @@ import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_400_BAD_REQUEST; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_500_INTERNAL_ERROR; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; public class ResourceMergeService { private static final Logger ourLog = LoggerFactory.getLogger(ResourceMergeService.class); @@ -192,8 +194,15 @@ private void mergeInTransaction( theTargetResource.getIdElement(), theMergeOperationParameters.getBatchSize(), partitionId); - // FIXME KHS use the result of this method call to see if it went async - myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); + + Parameters replaceRefsOutParams = + (Parameters) myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); + + Parameters.ParametersParameterComponent taskOutParam = + replaceRefsOutParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK); + if (taskOutParam != null) { + theMergeOutcome.setTask(taskOutParam.getResource()); + } myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index 595a2bdb7fd9..41123dc2a1d1 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; +import ca.uhn.fhir.jpa.provider.ReplaceReferenceRequest; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -17,6 +18,7 @@ import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; import org.junit.jupiter.api.BeforeEach; @@ -89,6 +91,7 @@ public class ResourceMergeServiceTest { void setup() { when(myDaoMock.getContext()).thenReturn(myFhirContext); myResourceMergeService = new ResourceMergeService(myDaoMock, myReplaceReferencesSvcMock, myTransactionServiceMock, myRequestPartitionHelperSvcMock); + } // SUCCESS CASES @@ -112,6 +115,7 @@ void testMerge_WithoutResultResource_Success() { Patient patientReturnedFromDaoAfterTargetUpdate = new Patient(); setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientReturnedFromDaoAfterTargetUpdate); setupTransactionServiceMock(); + setupReplaceReferencesForSuccessForSync(); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -143,6 +147,7 @@ void testMerge_WithoutResultResource_TargetSetToActiveExplicitly_Success() { Patient patientReturnedFromDaoAfterTargetUpdate = new Patient(); setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientReturnedFromDaoAfterTargetUpdate); setupTransactionServiceMock(); + setupReplaceReferencesForSuccessForSync(); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -173,6 +178,8 @@ void testMerge_WithResultResource_Success() { Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); setupDaoMockForSuccessfulTargetPatientUpdate(resultPatient, patientToBeReturnedFromDaoAfterTargetUpdate); setupTransactionServiceMock(); + setupReplaceReferencesForSuccessForSync(); + // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -208,6 +215,8 @@ void testMerge_WithResultResource_ResultHasAllTargetIdentifiers_Success() { Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); setupDaoMockForSuccessfulTargetPatientUpdate(resultPatient, patientToBeReturnedFromDaoAfterTargetUpdate); setupTransactionServiceMock(); + setupReplaceReferencesForSuccessForSync(); + // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -239,6 +248,8 @@ void testMerge_WithDeleteSourceTrue_Success() { Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientToBeReturnedFromDaoAfterTargetUpdate); setupTransactionServiceMock(); + setupReplaceReferencesForSuccessForSync(); + // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -268,6 +279,8 @@ void testMerge_WithDeleteSourceTrue_And_WithResultResource_Success() { Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); setupDaoMockForSuccessfulTargetPatientUpdate(resultPatient, patientToBeReturnedFromDaoAfterTargetUpdate); setupTransactionServiceMock(); + setupReplaceReferencesForSuccessForSync(); + // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -322,8 +335,9 @@ void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersio setupDaoMockForSuccessfulSourcePatientUpdate(sourcePatient, new Patient()); Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientToBeReturnedFromDaoAfterTargetUpdate); - setupTransactionServiceMock(); + setupReplaceReferencesForSuccessForSync(); + // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -1201,6 +1215,11 @@ private void verifyUpdatedTargetPatient(boolean theExpectLinkToSourcePatient, Li } + private void setupReplaceReferencesForSuccessForSync() { + when(myReplaceReferencesSvcMock.replaceReferences(isA(ReplaceReferenceRequest.class), + eq(myRequestDetailsMock))).thenReturn(new Parameters()); + } + private void setupDaoMockForSuccessfulTargetPatientUpdate(Patient thePatientExpectedAsInput, Patient thePatientToReturnInDaoOutcome) { DaoMethodOutcome daoMethodOutcome = new DaoMethodOutcome(); From 6685fb681380b36e311ec594fbdc69b26c2fffdb Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 11 Dec 2024 12:50:59 -0500 Subject: [PATCH 066/148] batch passes test --- .../ca/uhn/fhir/jpa/config/JpaConfig.java | 14 +++--- .../provider/ReplaceReferencesSvcImpl.java | 49 ++++++++++++------- .../provider/r4/ReplaceReferencesR4Test.java | 23 +++++++-- .../ReplaceReferencesTestHelper.java | 3 ++ .../server/provider/ProviderConstants.java | 2 + .../ReplaceReferenceUpdateStep.java | 4 +- ...ReplaceReferenceUpdateTaskReducerStep.java | 7 ++- .../ReplaceReferencesQueryIdsStep.java | 1 - 8 files changed, 72 insertions(+), 31 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index 7b49b430ce6d..b284d8e844e4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -19,6 +19,7 @@ */ package ca.uhn.fhir.jpa.config; +import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.api.IJobPersistence; import ca.uhn.fhir.batch2.jobs.export.BulkDataExportProvider; import ca.uhn.fhir.batch2.jobs.expunge.DeleteExpungeJobSubmitterImpl; @@ -932,12 +933,13 @@ public CacheTagDefinitionDao tagDefinitionDao( @Bean public IReplaceReferencesSvc replaceReferencesSvc( - FhirContext theFhirContext, - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IdHelperService theIdHelperService, - IResourceLinkDao theResourceLinkDao) { + FhirContext theFhirContext, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IdHelperService theIdHelperService, + IResourceLinkDao theResourceLinkDao, + IJobCoordinator theJobCoordinator) { return new ReplaceReferencesSvcImpl( - theFhirContext, theDaoRegistry, theHapiTransactionService, theIdHelperService, theResourceLinkDao); + theFhirContext, theDaoRegistry, theHapiTransactionService, theIdHelperService, theResourceLinkDao, theJobCoordinator); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 4f5dfe90b67e..c5e9689d053b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -19,11 +19,16 @@ */ package ca.uhn.fhir.jpa.provider; +import ca.uhn.fhir.batch2.api.IJobCoordinator; +import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; +import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; @@ -59,11 +64,13 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx.JOB_REPLACE_REFERENCES; import static ca.uhn.fhir.jpa.patch.FhirPatch.OPERATION_REPLACE; import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_OPERATION; import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_PATH; import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_TYPE; import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_VALUE; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.HAPI_BATCH_JOB_ID_SYSTEM; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; @@ -74,6 +81,7 @@ public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { private final DaoRegistry myDaoRegistry; private final HapiTransactionService myHapiTransactionService; private final IResourceLinkDao myResourceLinkDao; + private final IJobCoordinator myJobCoordinator; // FIXME remove private final ExecutorService myFakeExecutor = Executors.newSingleThreadExecutor(); @@ -84,15 +92,17 @@ public void preDestroy() { } public ReplaceReferencesSvcImpl( - FhirContext theFhirContext, - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IdHelperService theIdHelperService, - IResourceLinkDao theResourceLinkDao) { + FhirContext theFhirContext, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IdHelperService theIdHelperService, + IResourceLinkDao theResourceLinkDao, + IJobCoordinator theJobCoordinator) { myFhirContext = theFhirContext; myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; myResourceLinkDao = theResourceLinkDao; + myJobCoordinator = theJobCoordinator; } @Override @@ -117,23 +127,26 @@ public Integer countResourcesReferencingResource(IIdType theResourceId, RequestD private IBaseParameters replaceReferencesPreferAsync( ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { - // FIXME KHS actually start the job Task task = new Task(); task.setStatus(Task.TaskStatus.INPROGRESS); - myDaoRegistry.getResourceDao(Task.class).create(task, theRequestDetails); - // Make a copy so we can strip the version number so they don't accidentally keep polling for an - // out of date version - Task returnedTask = task.copy(); - returnedTask.setIdElement(task.getIdElement().toUnqualifiedVersionless()); - returnedTask.getMeta().setVersionId(null); + IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(Task.class); + resourceDao.create(task, theRequestDetails); + + ReplaceReferencesJobParameters jobParams = new ReplaceReferencesJobParameters(theReplaceReferenceRequest); + jobParams.setTaskId(task.getIdElement().toUnqualifiedVersionless()); + + JobInstanceStartRequest request = new JobInstanceStartRequest(JOB_REPLACE_REFERENCES, jobParams); + Batch2JobStartResponse jobStartResponse = myJobCoordinator.startInstance(theRequestDetails, request); + + task.addIdentifier().setSystem(HAPI_BATCH_JOB_ID_SYSTEM).setValue(jobStartResponse.getInstanceId()); + resourceDao.update(task, theRequestDetails); Parameters retval = new Parameters(); + task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); + task.getMeta().setVersionId(null); retval.addParameter() - .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) - .setResource(returnedTask); - - // FIXME KHS set partitions from request - fakeBackgroundTaskUpdate(theReplaceReferenceRequest, task, RequestPartitionId.allPartitions()); + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) + .setResource(task); return retval; } @@ -168,7 +181,7 @@ private void fakeBackgroundTaskUpdate( coding.setSystem(RESOURCE_TYPES_SYSTEM); coding.setCode("Bundle"); Reference outputBundleReference = - new Reference("#" + outputBundle.getIdElement().toUnqualifiedVersionless()); + new Reference(outputBundle.getIdElement().toUnqualifiedVersionless()); output.setValue(outputBundleReference); theTask.addContained(outputBundle); }); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index d0837e3c407d..62a452686bcc 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -1,9 +1,11 @@ package ca.uhn.fhir.jpa.provider.r4; +import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; @@ -17,6 +19,7 @@ import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; import static ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper.EXPECTED_SMALL_BATCHES; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.HAPI_BATCH_JOB_ID_SYSTEM; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; import static org.assertj.core.api.Assertions.assertThat; @@ -50,7 +53,10 @@ void testReplaceReferences(boolean isAsync) throws IOException { Task task = (Task) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).getResource(); assertNull(task.getIdElement().getVersionIdPart()); ourLog.info("Got task {}", task.getId()); - await().until(() -> myTestHelper.taskCompleted(task.getIdElement())); + + awaitJobCompletion(task); + +// FIXME KHS verify report patchResultBundle = myTestHelper.validateCompletedTask(task.getIdElement()); } else { @@ -65,6 +71,16 @@ void testReplaceReferences(boolean isAsync) throws IOException { myTestHelper.assertAllReferencesUpdated(); } + private void awaitJobCompletion(Task task) { + assertThat(task.getIdentifier()).hasSize(1) + .element(0) + .extracting(Identifier::getSystem) + .isEqualTo(HAPI_BATCH_JOB_ID_SYSTEM); + + String jobId = task.getIdentifierFirstRep().getValue(); + JobInstance jobInstance = myBatch2JobHelper.awaitJobCompletion(jobId); + } + @ParameterizedTest @ValueSource(booleans = {false, true}) @@ -79,12 +95,13 @@ void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { Task task = (Task) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).getResource(); assertNull(task.getIdElement().getVersionIdPart()); ourLog.info("Got task {}", task.getId()); - await().until(() -> myTestHelper.taskCompleted(task.getIdElement())); + + awaitJobCompletion(task); Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); - assertThat(taskWithOutput.getOutput()).hasSize(EXPECTED_SMALL_BATCHES); + assertThat(taskWithOutput.getOutput()).as("task " + task.getId() + " has size " + EXPECTED_SMALL_BATCHES).hasSize(EXPECTED_SMALL_BATCHES); List containedResources = taskWithOutput.getContained(); assertThat(containedResources) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index eb26b0280633..d9b5ea213b4b 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -334,6 +334,7 @@ public void validatePatchResultBundle(Bundle patchResultBundle, int theTotalExpe public Bundle validateCompletedTask(IIdType theTaskId) { Bundle patchResultBundle; Task taskWithOutput = myTaskDao.read(theTaskId, mySrd); + assertThat(taskWithOutput.getStatus()).isEqualTo(Task.TaskStatus.COMPLETED); ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); @@ -353,6 +354,8 @@ public Bundle validateCompletedTask(IIdType theTaskId) { Reference outputRef = (Reference) taskOutput.getValue(); patchResultBundle = (Bundle) outputRef.getResource(); +// ourLog.info("containedBundle: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(containedBundle)); +// ourLog.info("patchResultBundle: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(patchResultBundle)); assertTrue(containedBundle.equalsDeep(patchResultBundle)); return patchResultBundle; } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index c99eb22de950..77306dab8970 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -294,4 +294,6 @@ public class ProviderConstants { public static final String OPERATION_MERGE_OUTPUT_PARAM_OUTCOME = OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; public static final String OPERATION_MERGE_OUTPUT_PARAM_RESULT = "result"; public static final String OPERATION_MERGE_OUTPUT_PARAM_TASK = OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; + + public static final String HAPI_BATCH_JOB_ID_SYSTEM = "http://hapifhir.io/batch/jobId"; } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java index 903d153cf116..f77b76810502 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java @@ -38,8 +38,8 @@ public class ReplaceReferenceUpdateStep implements IJobStepWorker< ReplaceReferencesJobParameters, FhirIdListWorkChunkJson, ReplaceReferencePatchOutcomeJson> { - private final FhirContext myFhirContext; - private final DaoRegistry myDaoRegistry; + private FhirContext myFhirContext; + private DaoRegistry myDaoRegistry; public ReplaceReferenceUpdateStep(FhirContext theFhirContext, DaoRegistry theDaoRegistry) { myFhirContext = theFhirContext; diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java index fadc794623c3..4e7e5314141e 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java @@ -60,7 +60,8 @@ public RunOutcome run( myDaoRegistry.getResourceDao(Task.class).read(params.getTaskId().asIdDt(), requestDetails); task.setStatus(Task.TaskStatus.COMPLETED); - // TODO KHS this Task will probably be too large for large jobs. Revisit this model once we support Provenance resources. + // TODO KHS this Task will probably be too large for large jobs. Revisit this model once we support Provenance + // resources. myPatchOutputBundles.forEach(outputBundle -> { Task.TaskOutputComponent output = task.addOutput(); Coding coding = output.getType().getCodingFirstRep(); @@ -78,6 +79,10 @@ public RunOutcome run( result.setTaskId(params.getTaskId()); theDataSink.accept(result); + // Reusing the same reducer for all jobs feels confusing and dangerous to me. We need to fix this. + // See https://github.com/hapifhir/hapi-fhir/pull/6551 + myPatchOutputBundles.clear(); + return new RunOutcome(myPatchOutputBundles.size()); } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java index 9124ba9a08ab..37056ebd9106 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java @@ -45,7 +45,6 @@ public RunOutcome run( AtomicInteger totalCount = new AtomicInteger(); myHapiTransactionService .withSystemRequestOnPartition(params.getPartitionId()) - .readOnly() .execute(() -> { Stream stream = myBatch2DaoSvc .streamSourceIdsThatReferenceTargetId( From 709513d76a7142fdc5c1ab1ff0d460722a57e53f Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 11 Dec 2024 12:51:45 -0500 Subject: [PATCH 067/148] batch passes test --- .../ca/uhn/fhir/jpa/config/JpaConfig.java | 19 ++++++++++++------- .../provider/ReplaceReferencesSvcImpl.java | 17 ++++++++--------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index b284d8e844e4..894b2deff37b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -933,13 +933,18 @@ public CacheTagDefinitionDao tagDefinitionDao( @Bean public IReplaceReferencesSvc replaceReferencesSvc( - FhirContext theFhirContext, - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IdHelperService theIdHelperService, - IResourceLinkDao theResourceLinkDao, - IJobCoordinator theJobCoordinator) { + FhirContext theFhirContext, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IdHelperService theIdHelperService, + IResourceLinkDao theResourceLinkDao, + IJobCoordinator theJobCoordinator) { return new ReplaceReferencesSvcImpl( - theFhirContext, theDaoRegistry, theHapiTransactionService, theIdHelperService, theResourceLinkDao, theJobCoordinator); + theFhirContext, + theDaoRegistry, + theHapiTransactionService, + theIdHelperService, + theResourceLinkDao, + theJobCoordinator); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 24cb32451f79..101125c4b09b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -20,7 +20,6 @@ package ca.uhn.fhir.jpa.provider; import ca.uhn.fhir.batch2.api.IJobCoordinator; -import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; import ca.uhn.fhir.context.FhirContext; @@ -92,12 +91,12 @@ public void preDestroy() { } public ReplaceReferencesSvcImpl( - FhirContext theFhirContext, - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IdHelperService theIdHelperService, - IResourceLinkDao theResourceLinkDao, - IJobCoordinator theJobCoordinator) { + FhirContext theFhirContext, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IdHelperService theIdHelperService, + IResourceLinkDao theResourceLinkDao, + IJobCoordinator theJobCoordinator) { myFhirContext = theFhirContext; myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; @@ -145,8 +144,8 @@ private IBaseParameters replaceReferencesPreferAsync( task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); task.getMeta().setVersionId(null); retval.addParameter() - .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) - .setResource(task); + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) + .setResource(task); return retval; } From bb0ed333cbd93f4fbcdc80496ac69b81f53b7493 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 11 Dec 2024 13:34:08 -0500 Subject: [PATCH 068/148] consolidate async and sync supporting methods into a single storage service --- .../ca/uhn/fhir/jpa/config/JpaConfig.java | 166 +++++++------- .../fhir/jpa/provider/JpaSystemProvider.java | 1 + .../provider/ReplaceReferencesSvcImpl.java | 202 +++--------------- .../batch2/jobs/export/BulkExportAppCtx.java | 1 - .../ReplaceReferenceUpdateStep.java | 94 ++------ .../ReplaceReferencesAppCtx.java | 5 +- .../ReplaceReferencesJobParameters.java | 6 +- .../ca/uhn/fhir/jpa/api/dao/DaoRegistry.java | 20 +- .../export/svc/BulkExportHelperService.java | 2 + .../jpa/dao/merge/ResourceMergeService.java | 2 +- .../jpa/provider/IReplaceReferencesSvc.java | 3 +- .../ReplaceReferenceRequest.java | 2 +- .../ReplaceReferencesPatchBundleSvc.java | 101 +++++++++ .../dao/merge/ResourceMergeServiceTest.java | 2 +- 14 files changed, 253 insertions(+), 354 deletions(-) rename hapi-fhir-storage/src/main/java/ca/uhn/fhir/{jpa/provider => replacereferences}/ReplaceReferenceRequest.java (98%) create mode 100644 hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index e2d2d87cb25d..9c5113c602fd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -177,6 +177,7 @@ import ca.uhn.fhir.jpa.validation.ResourceLoaderImpl; import ca.uhn.fhir.jpa.validation.ValidationSettings; import ca.uhn.fhir.model.api.IPrimitiveDatatype; +import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.storage.IDeleteExpungeJobSubmitter; import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; @@ -213,8 +214,8 @@ @Configuration // repositoryFactoryBeanClass: EnversRevisionRepositoryFactoryBean is needed primarily for unit testing @EnableJpaRepositories( - basePackages = "ca.uhn.fhir.jpa.dao.data", - repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class) + basePackages = "ca.uhn.fhir.jpa.dao.data", + repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class) @Import({ BeanPostProcessorConfig.class, TermCodeSystemConfig.class, @@ -235,7 +236,7 @@ public class JpaConfig { public static final String PERSISTED_JPA_BUNDLE_PROVIDER = "PersistedJpaBundleProvider"; public static final String PERSISTED_JPA_BUNDLE_PROVIDER_BY_SEARCH = "PersistedJpaBundleProvider_BySearch"; public static final String PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER = - "PersistedJpaSearchFirstPageBundleProvider"; + "PersistedJpaSearchFirstPageBundleProvider"; public static final String HISTORY_BUILDER = "HistoryBuilder"; public static final String DEFAULT_PROFILE_VALIDATION_SUPPORT = "myDefaultProfileValidationSupport"; private static final String HAPI_DEFAULT_SCHEDULER_GROUP = "HAPI"; @@ -265,12 +266,12 @@ public DaoRegistry daoRegistry() { @Lazy @Bean public CascadingDeleteInterceptor cascadingDeleteInterceptor( - FhirContext theFhirContext, - DaoRegistry theDaoRegistry, - IInterceptorBroadcaster theInterceptorBroadcaster, - ThreadSafeResourceDeleterSvc threadSafeResourceDeleterSvc) { + FhirContext theFhirContext, + DaoRegistry theDaoRegistry, + IInterceptorBroadcaster theInterceptorBroadcaster, + ThreadSafeResourceDeleterSvc threadSafeResourceDeleterSvc) { return new CascadingDeleteInterceptor( - theFhirContext, theDaoRegistry, theInterceptorBroadcaster, threadSafeResourceDeleterSvc); + theFhirContext, theDaoRegistry, theInterceptorBroadcaster, threadSafeResourceDeleterSvc); } @Bean @@ -281,24 +282,24 @@ public ExternallyStoredResourceServiceRegistry ExternallyStoredResourceServiceRe @Lazy @Bean public ThreadSafeResourceDeleterSvc safeDeleter( - DaoRegistry theDaoRegistry, - IInterceptorBroadcaster theInterceptorBroadcaster, - HapiTransactionService hapiTransactionService) { + DaoRegistry theDaoRegistry, + IInterceptorBroadcaster theInterceptorBroadcaster, + HapiTransactionService hapiTransactionService) { return new ThreadSafeResourceDeleterSvc(theDaoRegistry, theInterceptorBroadcaster, hapiTransactionService); } @Lazy @Bean public ResponseTerminologyTranslationInterceptor responseTerminologyTranslationInterceptor( - IValidationSupport theValidationSupport, - ResponseTerminologyTranslationSvc theResponseTerminologyTranslationSvc) { + IValidationSupport theValidationSupport, + ResponseTerminologyTranslationSvc theResponseTerminologyTranslationSvc) { return new ResponseTerminologyTranslationInterceptor( - theValidationSupport, theResponseTerminologyTranslationSvc); + theValidationSupport, theResponseTerminologyTranslationSvc); } @Bean public ResponseTerminologyTranslationSvc responseTerminologyTranslationSvc( - IValidationSupport theValidationSupport) { + IValidationSupport theValidationSupport) { return new ResponseTerminologyTranslationSvc(theValidationSupport); } @@ -350,9 +351,9 @@ public BinaryAccessProvider binaryAccessProvider() { @Bean(name = "myBinaryStorageInterceptor") @Lazy public BinaryStorageInterceptor> binaryStorageInterceptor( - JpaStorageSettings theStorageSettings, FhirContext theCtx) { + JpaStorageSettings theStorageSettings, FhirContext theCtx) { BinaryStorageInterceptor> interceptor = - new BinaryStorageInterceptor<>(theCtx); + new BinaryStorageInterceptor<>(theCtx); interceptor.setAllowAutoInflateBinaries(theStorageSettings.isAllowAutoInflateBinaries()); interceptor.setAutoInflateBinariesMaximumSize(theStorageSettings.getAutoInflateBinariesMaximumBytes()); return interceptor; @@ -402,16 +403,16 @@ public ITermConceptMappingSvc termConceptMappingSvc() { @Bean public TaskScheduler taskScheduler() { ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler( - scheduledExecutorService().getObject(), - scheduledExecutorService().getObject()); + scheduledExecutorService().getObject(), + scheduledExecutorService().getObject()); return retVal; } @Bean(name = TASK_EXECUTOR_NAME) public AsyncTaskExecutor taskExecutor() { ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler( - scheduledExecutorService().getObject(), - scheduledExecutorService().getObject()); + scheduledExecutorService().getObject(), + scheduledExecutorService().getObject()); return retVal; } @@ -445,7 +446,7 @@ public HapiFhirHibernateJpaDialect hibernateJpaDialect(FhirContext theFhirContex @Bean @Lazy public OverridePathBasedReferentialIntegrityForDeletesInterceptor - overridePathBasedReferentialIntegrityForDeletesInterceptor() { + overridePathBasedReferentialIntegrityForDeletesInterceptor() { return new OverridePathBasedReferentialIntegrityForDeletesInterceptor(); } @@ -522,13 +523,13 @@ public AutowiringSpringBeanJobFactory schedulerJobFactory() { @Bean public IBulkDataExportJobSchedulingHelper bulkDataExportJobSchedulingHelper( - DaoRegistry theDaoRegistry, - PlatformTransactionManager theTxManager, - JpaStorageSettings theStorageSettings, - BulkExportHelperService theBulkExportHelperSvc, - IJobPersistence theJpaJobPersistence) { + DaoRegistry theDaoRegistry, + PlatformTransactionManager theTxManager, + JpaStorageSettings theStorageSettings, + BulkExportHelperService theBulkExportHelperSvc, + IJobPersistence theJpaJobPersistence) { return new BulkDataExportJobSchedulingHelperImpl( - theDaoRegistry, theTxManager, theStorageSettings, theBulkExportHelperSvc, theJpaJobPersistence, null); + theDaoRegistry, theTxManager, theStorageSettings, theBulkExportHelperSvc, theJpaJobPersistence, null); } @Bean @@ -592,19 +593,19 @@ public PersistedJpaBundleProvider newPersistedJpaBundleProvider(RequestDetails t @Bean(name = PERSISTED_JPA_BUNDLE_PROVIDER_BY_SEARCH) @Scope("prototype") public PersistedJpaBundleProvider newPersistedJpaBundleProviderBySearch( - RequestDetails theRequest, Search theSearch) { + RequestDetails theRequest, Search theSearch) { return new PersistedJpaBundleProvider(theRequest, theSearch); } @Bean(name = PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER) @Scope("prototype") public PersistedJpaSearchFirstPageBundleProvider newPersistedJpaSearchFirstPageBundleProvider( - RequestDetails theRequest, - SearchTask theSearchTask, - ISearchBuilder theSearchBuilder, - RequestPartitionId theRequestPartitionId) { + RequestDetails theRequest, + SearchTask theSearchTask, + ISearchBuilder theSearchBuilder, + RequestPartitionId theRequestPartitionId) { return new PersistedJpaSearchFirstPageBundleProvider( - theSearchTask, theSearchBuilder, theRequest, theRequestPartitionId); + theSearchTask, theSearchBuilder, theRequest, theRequestPartitionId); } @Bean(name = RepositoryValidatingRuleBuilder.REPOSITORY_VALIDATING_RULE_BUILDER) @@ -616,14 +617,14 @@ public RepositoryValidatingRuleBuilder repositoryValidatingRuleBuilder(IValidati @Bean @Scope("prototype") public ComboUniqueSearchParameterPredicateBuilder newComboUniqueSearchParameterPredicateBuilder( - SearchQueryBuilder theSearchSqlBuilder) { + SearchQueryBuilder theSearchSqlBuilder) { return new ComboUniqueSearchParameterPredicateBuilder(theSearchSqlBuilder); } @Bean @Scope("prototype") public ComboNonUniqueSearchParameterPredicateBuilder newComboNonUniqueSearchParameterPredicateBuilder( - SearchQueryBuilder theSearchSqlBuilder) { + SearchQueryBuilder theSearchSqlBuilder) { return new ComboNonUniqueSearchParameterPredicateBuilder(theSearchSqlBuilder); } @@ -654,14 +655,14 @@ public QuantityPredicateBuilder newQuantityPredicateBuilder(SearchQueryBuilder t @Bean @Scope("prototype") public QuantityNormalizedPredicateBuilder newQuantityNormalizedPredicateBuilder( - SearchQueryBuilder theSearchBuilder) { + SearchQueryBuilder theSearchBuilder) { return new QuantityNormalizedPredicateBuilder(theSearchBuilder); } @Bean @Scope("prototype") public ResourceLinkPredicateBuilder newResourceLinkPredicateBuilder( - QueryStack theQueryStack, SearchQueryBuilder theSearchBuilder, boolean theReversed) { + QueryStack theQueryStack, SearchQueryBuilder theSearchBuilder, boolean theReversed) { return new ResourceLinkPredicateBuilder(theQueryStack, theSearchBuilder, theReversed); } @@ -686,7 +687,7 @@ public ResourceIdPredicateBuilder newResourceIdPredicateBuilder(SearchQueryBuild @Bean @Scope("prototype") public SearchParamPresentPredicateBuilder newSearchParamPresentPredicateBuilder( - SearchQueryBuilder theSearchBuilder) { + SearchQueryBuilder theSearchBuilder) { return new SearchParamPresentPredicateBuilder(theSearchBuilder); } @@ -711,7 +712,7 @@ public ResourceHistoryPredicateBuilder newResourceHistoryPredicateBuilder(Search @Bean @Scope("prototype") public ResourceHistoryProvenancePredicateBuilder newResourceHistoryProvenancePredicateBuilder( - SearchQueryBuilder theSearchBuilder) { + SearchQueryBuilder theSearchBuilder) { return new ResourceHistoryProvenancePredicateBuilder(theSearchBuilder); } @@ -730,10 +731,10 @@ public SearchQueryExecutor newSearchQueryExecutor(GeneratedSql theGeneratedSql, @Bean(name = HISTORY_BUILDER) @Scope("prototype") public HistoryBuilder newHistoryBuilder( - @Nullable String theResourceType, - @Nullable JpaPid theResourceId, - @Nullable Date theRangeStartInclusive, - @Nullable Date theRangeEndInclusive) { + @Nullable String theResourceType, + @Nullable JpaPid theResourceId, + @Nullable Date theRangeStartInclusive, + @Nullable Date theRangeEndInclusive) { return new HistoryBuilder(theResourceType, theResourceId, theRangeStartInclusive, theRangeEndInclusive); } @@ -771,10 +772,10 @@ public ExpungeService expungeService() { @Bean @Scope("prototype") public ExpungeOperation expungeOperation( - String theResourceName, - IResourcePersistentId theResourceId, - ExpungeOptions theExpungeOptions, - RequestDetails theRequestDetails) { + String theResourceName, + IResourcePersistentId theResourceId, + ExpungeOptions theExpungeOptions, + RequestDetails theRequestDetails) { return new ExpungeOperation(theResourceName, theResourceId, theExpungeOptions, theRequestDetails); } @@ -830,7 +831,7 @@ public ResourceLoaderImpl jpaResourceLoader() { @Bean public UnknownCodeSystemWarningValidationSupport unknownCodeSystemWarningValidationSupport( - FhirContext theFhirContext) { + FhirContext theFhirContext) { return new UnknownCodeSystemWarningValidationSupport(theFhirContext); } @@ -846,9 +847,9 @@ public VersionCanonicalizer versionCanonicalizer(FhirContext theFhirContext) { @Bean public SearchParameterDaoValidator searchParameterDaoValidator( - FhirContext theFhirContext, - JpaStorageSettings theStorageSettings, - ISearchParamRegistry theSearchParamRegistry) { + FhirContext theFhirContext, + JpaStorageSettings theStorageSettings, + ISearchParamRegistry theSearchParamRegistry) { return new SearchParameterDaoValidator(theFhirContext, theStorageSettings, theSearchParamRegistry); } @@ -886,17 +887,17 @@ public PersistenceContextProvider persistenceContextProvider() { @Bean public ResourceSearchUrlSvc resourceSearchUrlSvc( - PersistenceContextProvider thePersistenceContextProvider, - IResourceSearchUrlDao theResourceSearchUrlDao, - MatchUrlService theMatchUrlService, - FhirContext theFhirContext, - PartitionSettings thePartitionSettings) { + PersistenceContextProvider thePersistenceContextProvider, + IResourceSearchUrlDao theResourceSearchUrlDao, + MatchUrlService theMatchUrlService, + FhirContext theFhirContext, + PartitionSettings thePartitionSettings) { return new ResourceSearchUrlSvc( - thePersistenceContextProvider.getEntityManager(), - theResourceSearchUrlDao, - theMatchUrlService, - theFhirContext, - thePartitionSettings); + thePersistenceContextProvider.getEntityManager(), + theResourceSearchUrlDao, + theMatchUrlService, + theFhirContext, + thePartitionSettings); } @Bean @@ -906,12 +907,12 @@ public ISearchUrlJobMaintenanceSvc searchUrlJobMaintenanceSvc(ResourceSearchUrlS @Bean public IResourceModifiedMessagePersistenceSvc subscriptionMessagePersistence( - FhirContext theFhirContext, - IResourceModifiedDao theIResourceModifiedDao, - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService) { + FhirContext theFhirContext, + IResourceModifiedDao theIResourceModifiedDao, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService) { return new ResourceModifiedMessagePersistenceSvcImpl( - theFhirContext, theIResourceModifiedDao, theDaoRegistry, theHapiTransactionService); + theFhirContext, theIResourceModifiedDao, theDaoRegistry, theHapiTransactionService); } @Bean @@ -921,30 +922,33 @@ public IMetaTagSorter metaTagSorter() { @Bean public ResourceHistoryCalculator resourceHistoryCalculator( - FhirContext theFhirContext, HibernatePropertiesProvider theHibernatePropertiesProvider) { + FhirContext theFhirContext, HibernatePropertiesProvider theHibernatePropertiesProvider) { return new ResourceHistoryCalculator(theFhirContext, theHibernatePropertiesProvider.isOracleDialect()); } @Bean public CacheTagDefinitionDao tagDefinitionDao( - ITagDefinitionDao tagDefinitionDao, MemoryCacheService memoryCacheService) { + ITagDefinitionDao tagDefinitionDao, MemoryCacheService memoryCacheService) { return new CacheTagDefinitionDao(tagDefinitionDao, memoryCacheService); } @Bean public IReplaceReferencesSvc replaceReferencesSvc( - FhirContext theFhirContext, - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IdHelperService theIdHelperService, - IResourceLinkDao theResourceLinkDao, - IJobCoordinator theJobCoordinator) { + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IResourceLinkDao theResourceLinkDao, + IJobCoordinator theJobCoordinator, + ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundle) { return new ReplaceReferencesSvcImpl( - theFhirContext, - theDaoRegistry, - theHapiTransactionService, - theIdHelperService, - theResourceLinkDao, - theJobCoordinator); + theDaoRegistry, + theHapiTransactionService, + theResourceLinkDao, + theJobCoordinator, + theReplaceReferencesPatchBundle); + } + + @Bean + public ReplaceReferencesPatchBundleSvc replaceReferencesPatchBundleSvc(DaoRegistry theDaoRegistry) { + return new ReplaceReferencesPatchBundleSvc(theDaoRegistry); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index cb4eec6886b4..faf90ec266c8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -27,6 +27,7 @@ import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.Transaction; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 101125c4b09b..c728cfaf94e9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -22,53 +22,28 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; -import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; +import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.api.server.SystemRequestDetails; -import ca.uhn.fhir.util.BundleBuilder; -import ca.uhn.fhir.util.ResourceReferenceInfo; import ca.uhn.fhir.util.StopLimitAccumulator; -import com.google.common.collect.Lists; import jakarta.annotation.Nonnull; -import jakarta.annotation.PreDestroy; import org.hl7.fhir.instance.model.api.IBaseParameters; -import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.CodeType; -import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.Meta; import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Task; -import org.hl7.fhir.r4.model.Type; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.stream.Collectors; import java.util.stream.Stream; import static ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx.JOB_REPLACE_REFERENCES; -import static ca.uhn.fhir.jpa.patch.FhirPatch.OPERATION_REPLACE; -import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_OPERATION; -import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_PATH; -import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_TYPE; -import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_VALUE; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.HAPI_BATCH_JOB_ID_SYSTEM; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; @@ -76,37 +51,28 @@ public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { private static final Logger ourLog = LoggerFactory.getLogger(ReplaceReferencesSvcImpl.class); public static final String RESOURCE_TYPES_SYSTEM = "http://hl7.org/fhir/ValueSet/resource-types"; - private final FhirContext myFhirContext; private final DaoRegistry myDaoRegistry; private final HapiTransactionService myHapiTransactionService; private final IResourceLinkDao myResourceLinkDao; private final IJobCoordinator myJobCoordinator; - // FIXME remove - private final ExecutorService myFakeExecutor = Executors.newSingleThreadExecutor(); - - // FIXME remove - @PreDestroy - public void preDestroy() { - myFakeExecutor.shutdown(); - } + private final ReplaceReferencesPatchBundleSvc myReplaceReferencesPatchBundleSvc; public ReplaceReferencesSvcImpl( - FhirContext theFhirContext, - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IdHelperService theIdHelperService, - IResourceLinkDao theResourceLinkDao, - IJobCoordinator theJobCoordinator) { - myFhirContext = theFhirContext; + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IResourceLinkDao theResourceLinkDao, + IJobCoordinator theJobCoordinator, + ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc) { myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; myResourceLinkDao = theResourceLinkDao; myJobCoordinator = theJobCoordinator; + myReplaceReferencesPatchBundleSvc = theReplaceReferencesPatchBundleSvc; } @Override public IBaseParameters replaceReferences( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { theReplaceReferenceRequest.validateOrThrowInvalidParameterException(); if (theRequestDetails.isPreferAsync()) { @@ -120,12 +86,12 @@ public IBaseParameters replaceReferences( public Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails) { return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { return myResourceLinkDao.countResourcesTargetingFhirTypeAndFhirId( - theResourceId.getResourceType(), theResourceId.getIdPart()); + theResourceId.getResourceType(), theResourceId.getIdPart()); }); } private IBaseParameters replaceReferencesPreferAsync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { Task task = new Task(); task.setStatus(Task.TaskStatus.INPROGRESS); IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(Task.class); @@ -144,164 +110,46 @@ private IBaseParameters replaceReferencesPreferAsync( task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); task.getMeta().setVersionId(null); retval.addParameter() - .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) - .setResource(task); + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) + .setResource(task); return retval; } - // FIXME KHS replace this with a proper batch job - private void fakeBackgroundTaskUpdate( - ReplaceReferenceRequest theReplaceReferenceRequest, Task theTask, RequestPartitionId thePartitionId) { - SystemRequestDetails systemRequestDetails = new SystemRequestDetails(); - systemRequestDetails.setRequestPartitionId(thePartitionId); - myFakeExecutor.submit(() -> { - try { - - List pidList = myHapiTransactionService - .withSystemRequestOnPartition(thePartitionId) - .execute(() -> myResourceLinkDao - .streamSourceIdsForTargetFhirId( - theReplaceReferenceRequest.sourceId.getResourceType(), - theReplaceReferenceRequest.sourceId.getIdPart()) - .collect(Collectors.toUnmodifiableList())); - - List> chunks = Lists.partition(pidList, theReplaceReferenceRequest.batchSize); - List outputBundles = new ArrayList<>(); - - chunks.forEach(chunk -> { - Bundle result = patchReferencingResources(theReplaceReferenceRequest, chunk, systemRequestDetails); - outputBundles.add(result); - }); - - theTask.setStatus(Task.TaskStatus.COMPLETED); - outputBundles.forEach(outputBundle -> { - Task.TaskOutputComponent output = theTask.addOutput(); - Coding coding = output.getType().getCodingFirstRep(); - coding.setSystem(RESOURCE_TYPES_SYSTEM); - coding.setCode("Bundle"); - Reference outputBundleReference = - new Reference(outputBundle.getIdElement().toUnqualifiedVersionless()); - output.setValue(outputBundleReference); - theTask.addContained(outputBundle); - }); - - myDaoRegistry.getResourceDao(Task.class).update(theTask, systemRequestDetails); - ourLog.info("Updated task {} to COMPLETED.", theTask.getId()); - } catch (Exception e) { - ourLog.error("Patch failed", e); - theTask.setStatus(Task.TaskStatus.FAILED); - myDaoRegistry.getResourceDao(Task.class).update(theTask, systemRequestDetails); - ourLog.info("Updated task {} to FAILED.", theTask.getId()); - } - }); - } - /** * Try to perform the operation synchronously. However if there is more than a page of results, fall back to asynchronous operation */ @Nonnull private IBaseParameters replaceReferencesPreferSync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // TODO KHS get partition from request StopLimitAccumulator accumulator = myHapiTransactionService - .withRequest(theRequestDetails) - .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); + .withRequest(theRequestDetails) + .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); if (accumulator.isTruncated()) { ourLog.warn("Too many results. Switching to asynchronous reference replacement."); return replaceReferencesPreferAsync(theReplaceReferenceRequest, theRequestDetails); } - Bundle result = - patchReferencingResources(theReplaceReferenceRequest, accumulator.getItemList(), theRequestDetails); + + Bundle result = myReplaceReferencesPatchBundleSvc. + patchReferencingResources(theReplaceReferenceRequest, accumulator.getItemList(), theRequestDetails); Parameters retval = new Parameters(); retval.addParameter() - .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME) - .setResource(result); + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME) + .setResource(result); return retval; } - // FIXME KHS delete after convert to batch - private Bundle patchReferencingResources( - ReplaceReferenceRequest theReplaceReferenceRequest, - List theFhirIdList, - RequestDetails theRequestDetails) { - Bundle patchBundle = buildPatchBundle(theReplaceReferenceRequest, theRequestDetails, theFhirIdList); - IFhirSystemDao systemDao = myDaoRegistry.getSystemDao(); - Bundle result = systemDao.transaction(theRequestDetails, patchBundle); - // TODO KHS shouldn't transaction response bundles have ids? - result.setId(UUID.randomUUID().toString()); - return result; - } - private @Nonnull StopLimitAccumulator getAllPidsWithLimit( - ReplaceReferenceRequest theReplaceReferenceRequest) { + ReplaceReferenceRequest theReplaceReferenceRequest) { Stream idStream = myResourceLinkDao.streamSourceIdsForTargetFhirId( - theReplaceReferenceRequest.sourceId.getResourceType(), theReplaceReferenceRequest.sourceId.getIdPart()); + theReplaceReferenceRequest.sourceId.getResourceType(), theReplaceReferenceRequest.sourceId.getIdPart()); StopLimitAccumulator accumulator = - StopLimitAccumulator.fromStreamAndLimit(idStream, theReplaceReferenceRequest.batchSize); + StopLimitAccumulator.fromStreamAndLimit(idStream, theReplaceReferenceRequest.batchSize); return accumulator; } - - // FIXME KHS delete after convert to batch - private Bundle buildPatchBundle( - ReplaceReferenceRequest theReplaceReferenceRequest, - RequestDetails theRequestDetails, - List theFhirIdList) { - BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); - - theFhirIdList.forEach(referencingResourceId -> { - IFhirResourceDao dao = getDao(referencingResourceId.getResourceType()); - IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); - Parameters patchParams = buildPatchParams(theReplaceReferenceRequest, resource); - IIdType resourceId = resource.getIdElement(); - bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); - }); - return bundleBuilder.getBundleTyped(); - } - - // FIXME KHS delete after convert to batch - private @Nonnull Parameters buildPatchParams( - ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { - Parameters params = new Parameters(); - - myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() - .filter(refInfo -> matches( - refInfo, - theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource - .map(refInfo -> createReplaceReferencePatchOperation( - referencingResource.fhirType() + "." + refInfo.getName(), - new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) - .forEach(params::addParameter); // Add each operation to parameters - return params; - } - - // FIXME KHS delete after convert to batch - private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { - return refInfo.getResourceReference() - .getReferenceElement() - .toUnqualifiedVersionless() - .getValueAsString() - .equals(theSourceId.getValueAsString()); - } - - // FIXME KHS delete after convert to batch - @Nonnull - private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( - String thePath, Type theValue) { - - Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); - operation.setName(PARAMETER_OPERATION); - operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE)); - operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath)); - operation.addPart().setName(PARAMETER_VALUE).setValue(theValue); - return operation; - } - - private IFhirResourceDao getDao(String theResourceName) { - return myDaoRegistry.getResourceDao(theResourceName); - } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportAppCtx.java index 56de694bc493..9d9e90822c61 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportAppCtx.java @@ -145,7 +145,6 @@ public ExpandResourceAndWriteBinaryStep expandResourceAndWriteBinaryStep() { } @Bean - @Scope("prototype") public BulkExportCreateReportStep createReportStep() { return new BulkExportCreateReportStep(); } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java index f77b76810502..7abcbee46ebf 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java @@ -8,42 +8,26 @@ import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdListWorkChunkJson; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.api.dao.DaoRegistry; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; -import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; +import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; -import ca.uhn.fhir.util.BundleBuilder; -import ca.uhn.fhir.util.ResourceReferenceInfo; import jakarta.annotation.Nonnull; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.CodeType; -import org.hl7.fhir.r4.model.Meta; -import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.Reference; -import org.hl7.fhir.r4.model.StringType; -import org.hl7.fhir.r4.model.Type; -import java.util.UUID; - -import static ca.uhn.fhir.jpa.patch.FhirPatch.OPERATION_REPLACE; -import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_OPERATION; -import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_PATH; -import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_TYPE; -import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_VALUE; +import java.util.List; +import java.util.stream.Collectors; public class ReplaceReferenceUpdateStep implements IJobStepWorker< ReplaceReferencesJobParameters, FhirIdListWorkChunkJson, ReplaceReferencePatchOutcomeJson> { - private FhirContext myFhirContext; - private DaoRegistry myDaoRegistry; + private final FhirContext myFhirContext; + private final ReplaceReferencesPatchBundleSvc myReplaceReferencesPatchBundleSvc; - public ReplaceReferenceUpdateStep(FhirContext theFhirContext, DaoRegistry theDaoRegistry) { + public ReplaceReferenceUpdateStep(FhirContext theFhirContext, ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc) { myFhirContext = theFhirContext; - myDaoRegistry = theDaoRegistry; + myReplaceReferencesPatchBundleSvc = theReplaceReferencesPatchBundleSvc; } @Nonnull @@ -56,14 +40,12 @@ public RunOutcome run( throws JobExecutionFailedException { ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); - FhirIdListWorkChunkJson theFhirIds = theStepExecutionDetails.getData(); + ReplaceReferenceRequest replaceReferencesRequest = params.asReplaceReferencesRequest(); + List fhirIds = theStepExecutionDetails.getData().getFhirIds().stream().map(FhirIdJson::asIdDt).collect(Collectors.toList()); SystemRequestDetails requestDetails = SystemRequestDetails.forRequestPartitionId(params.getPartitionId()); - Bundle patchBundle = buildPatchBundle(params, theFhirIds, requestDetails); - IFhirSystemDao systemDao = myDaoRegistry.getSystemDao(); - Bundle result = systemDao.transaction(requestDetails, patchBundle); - // TODO KHS shouldn't transaction response bundles have ids? - result.setId(UUID.randomUUID().toString()); + + Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources(replaceReferencesRequest, fhirIds, requestDetails); ReplaceReferencePatchOutcomeJson data = new ReplaceReferencePatchOutcomeJson(myFhirContext, result); theDataSink.accept(data); @@ -71,54 +53,4 @@ public RunOutcome run( return new RunOutcome(result.getEntry().size()); } - private Bundle buildPatchBundle( - ReplaceReferencesJobParameters theParams, - FhirIdListWorkChunkJson theFhirIds, - RequestDetails theRequestDetails) { - BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); - - theFhirIds.getFhirIds().stream().map(FhirIdJson::asIdDt).forEach(referencingResourceId -> { - IFhirResourceDao dao = myDaoRegistry.getResourceDao(referencingResourceId.getResourceType()); - IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); - Parameters patchParams = buildPatchParams(theParams, resource); - IIdType resourceId = resource.getIdElement(); - bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); - }); - return bundleBuilder.getBundleTyped(); - } - - private @Nonnull Parameters buildPatchParams( - ReplaceReferencesJobParameters theParams, IBaseResource referencingResource) { - Parameters params = new Parameters(); - - myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() - .filter(refInfo -> matches( - refInfo, - theParams.getSourceId().asIdDt())) // We only care about references to our source resource - .map(refInfo -> createReplaceReferencePatchOperation( - referencingResource.fhirType() + "." + refInfo.getName(), - new Reference(theParams.getTargetId().toString()))) - .forEach(params::addParameter); // Add each operation to parameters - return params; - } - - private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { - return refInfo.getResourceReference() - .getReferenceElement() - .toUnqualifiedVersionless() - .getValueAsString() - .equals(theSourceId.getValueAsString()); - } - - @Nonnull - private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( - String thePath, Type theValue) { - - Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); - operation.setName(PARAMETER_OPERATION); - operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE)); - operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath)); - operation.addPart().setName(PARAMETER_VALUE).setValue(theValue); - return operation; - } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java index d549b8e9bc2c..603e9a6a37b9 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; +import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -50,8 +51,8 @@ public ReplaceReferencesQueryIdsStep replaceReferencesQueryIdsStep( @Bean public ReplaceReferenceUpdateStep replaceReferenceUpdateStep( - FhirContext theFhirContext, DaoRegistry theDaoRegistry) { - return new ReplaceReferenceUpdateStep(theFhirContext, theDaoRegistry); + FhirContext theFhirContext, ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc) { + return new ReplaceReferenceUpdateStep(theFhirContext, theReplaceReferencesPatchBundleSvc); } @Bean diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java index ed70ccd3d84a..1dd3ed52b997 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java @@ -2,7 +2,7 @@ import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; import ca.uhn.fhir.interceptor.model.RequestPartitionId; -import ca.uhn.fhir.jpa.provider.ReplaceReferenceRequest; +import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; import ca.uhn.fhir.model.api.BaseBatchJobParameters; import com.fasterxml.jackson.annotation.JsonProperty; import org.hl7.fhir.instance.model.api.IIdType; @@ -80,4 +80,8 @@ public void setTaskId(IIdType theTaskId) { public FhirIdJson getTaskId() { return myTaskId; } + + public ReplaceReferenceRequest asReplaceReferencesRequest() { + return new ReplaceReferenceRequest(mySourceId.asIdDt(), myTargetId.asIdDt(), myBatchSize, myPartitionId); + } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/dao/DaoRegistry.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/dao/DaoRegistry.java index a49e1eb4a00e..06629ec11f0d 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/dao/DaoRegistry.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/dao/DaoRegistry.java @@ -47,7 +47,7 @@ public class DaoRegistry implements ApplicationContextAware, IDaoRegistry { private ApplicationContext myAppCtx; @Autowired - private FhirContext myContext; + private FhirContext myFhirContext; private volatile Map> myResourceNameToResourceDao; private volatile IFhirSystemDao mySystemDao; @@ -65,7 +65,7 @@ public DaoRegistry() { */ public DaoRegistry(FhirContext theFhirContext) { super(); - myContext = theFhirContext; + myFhirContext = theFhirContext; } public void setSupportedResourceTypes(Collection theSupportedResourceTypes) { @@ -128,7 +128,7 @@ public IFhirResourceDao getResourceDaoIfExists(Clas @Nullable public IFhirResourceDao getResourceDaoOrNull(Class theResourceType) { - String resourceName = myContext.getResourceType(theResourceType); + String resourceName = myFhirContext.getResourceType(theResourceType); try { return (IFhirResourceDao) getResourceDao(resourceName); } catch (InvalidRequestException e) { @@ -175,7 +175,7 @@ private void initializeMaps(Collection theResourceDaos) { for (IFhirResourceDao nextResourceDao : theResourceDaos) { Class resourceType = nextResourceDao.getResourceType(); assert resourceType != null; - RuntimeResourceDefinition nextResourceDef = myContext.getResourceDefinition(resourceType); + RuntimeResourceDefinition nextResourceDef = myFhirContext.getResourceDefinition(resourceType); if (mySupportedResourceTypes == null || mySupportedResourceTypes.contains(nextResourceDef.getName())) { myResourceNameToResourceDao.put(nextResourceDef.getName(), nextResourceDao); } @@ -183,7 +183,7 @@ private void initializeMaps(Collection theResourceDaos) { } public void register(IFhirResourceDao theResourceDao) { - RuntimeResourceDefinition resourceDef = myContext.getResourceDefinition(theResourceDao.getResourceType()); + RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceDao.getResourceType()); String resourceName = resourceDef.getName(); myResourceNameToResourceDao.put(resourceName, theResourceDao); } @@ -192,12 +192,12 @@ public IFhirResourceDao getDaoOrThrowException(Class th IFhirResourceDao retVal = getResourceDao(theClass); if (retVal == null) { List supportedResourceNames = myResourceNameToResourceDao.keySet().stream() - .map(t -> myContext.getResourceType(t)) + .map(t -> myFhirContext.getResourceType(t)) .sorted() .collect(Collectors.toList()); throw new InvalidRequestException(Msg.code(573) + "Unable to process request, this server does not know how to handle resources of type " - + myContext.getResourceType(theClass) + " - Can handle: " + supportedResourceNames); + + myFhirContext.getResourceType(theClass) + " - Can handle: " + supportedResourceNames); } return retVal; } @@ -225,4 +225,10 @@ private List toCollection(String[] theResourceTypes) { public Set getRegisteredDaoTypes() { return Collections.unmodifiableSet(myResourceNameToResourceDao.keySet()); } + + // TODO KHS find all the places where FhirContext and DaoRegistry are both passed into constructors and + // remove the FhirContext parameter and pull it from the DaoRegistry parameter + public FhirContext getFhirContext() { + return myFhirContext; + } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkExportHelperService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkExportHelperService.java index e905275b7561..35780820ecfb 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkExportHelperService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkExportHelperService.java @@ -45,6 +45,8 @@ public class BulkExportHelperService { @Autowired private FhirContext myContext; + public BulkExportHelperService() {} + /** * Given the parameters, create the search parameter map based on type filters and the _since parameter. * diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 1e7401deb200..701a30087cdd 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -27,7 +27,7 @@ import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; -import ca.uhn.fhir.jpa.provider.ReplaceReferenceRequest; +import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java index ff6b9751587e..4a00b5b121ed 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java @@ -19,6 +19,7 @@ */ package ca.uhn.fhir.jpa.provider; +import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; import ca.uhn.fhir.rest.api.server.RequestDetails; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IIdType; @@ -29,7 +30,7 @@ public interface IReplaceReferencesSvc { IBaseParameters replaceReferences( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails); + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails); Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java similarity index 98% rename from hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java rename to hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java index 1de8e442d8c4..4d97ea0dfc46 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferenceRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.provider; +package ca.uhn.fhir.replacereferences; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.model.RequestPartitionId; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java new file mode 100644 index 000000000000..d3526ece8c35 --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java @@ -0,0 +1,101 @@ +package ca.uhn.fhir.replacereferences; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.util.BundleBuilder; +import ca.uhn.fhir.util.ResourceReferenceInfo; +import jakarta.annotation.Nonnull; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CodeType; +import org.hl7.fhir.r4.model.Meta; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Type; + +import java.util.List; +import java.util.UUID; + +import static ca.uhn.fhir.jpa.patch.FhirPatch.OPERATION_REPLACE; +import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_OPERATION; +import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_PATH; +import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_TYPE; +import static ca.uhn.fhir.jpa.patch.FhirPatch.PARAMETER_VALUE; + +public class ReplaceReferencesPatchBundleSvc { + + private final FhirContext myFhirContext; + private final DaoRegistry myDaoRegistry; + + public ReplaceReferencesPatchBundleSvc(DaoRegistry theDaoRegistry) { + myDaoRegistry = theDaoRegistry; + myFhirContext = theDaoRegistry.getFhirContext(); + } + + public Bundle patchReferencingResources(ReplaceReferenceRequest theReplaceReferenceRequest, List theResourceIds, RequestDetails theRequestDetails) { + Bundle patchBundle = buildPatchBundle(theReplaceReferenceRequest, theResourceIds, theRequestDetails); + IFhirSystemDao systemDao = myDaoRegistry.getSystemDao(); + Bundle result = systemDao.transaction(theRequestDetails, patchBundle); + // TODO KHS shouldn't transaction response bundles already have ids? + result.setId(UUID.randomUUID().toString()); + return result; + } + + private Bundle buildPatchBundle( + ReplaceReferenceRequest theReplaceReferenceRequest, + List theResourceIds, + RequestDetails theRequestDetails) { + BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); + + theResourceIds.forEach(referencingResourceId -> { + IFhirResourceDao dao = myDaoRegistry.getResourceDao(referencingResourceId.getResourceType()); + IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); + Parameters patchParams = buildPatchParams(theReplaceReferenceRequest, resource); + IIdType resourceId = resource.getIdElement(); + bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); + }); + return bundleBuilder.getBundleTyped(); + } + + private @Nonnull Parameters buildPatchParams( + ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { + Parameters params = new Parameters(); + + myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() + .filter(refInfo -> matches( + refInfo, + theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource + .map(refInfo -> createReplaceReferencePatchOperation( + referencingResource.fhirType() + "." + refInfo.getName(), + new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) + .forEach(params::addParameter); // Add each operation to parameters + return params; + } + + private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { + return refInfo.getResourceReference() + .getReferenceElement() + .toUnqualifiedVersionless() + .getValueAsString() + .equals(theSourceId.getValueAsString()); + } + + @Nonnull + private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( + String thePath, Type theValue) { + + Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); + operation.setName(PARAMETER_OPERATION); + operation.addPart().setName(PARAMETER_TYPE).setValue(new CodeType(OPERATION_REPLACE)); + operation.addPart().setName(PARAMETER_PATH).setValue(new StringType(thePath)); + operation.addPart().setName(PARAMETER_VALUE).setValue(theValue); + return operation; + } +} diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index 41123dc2a1d1..6d2efba1853a 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -6,7 +6,7 @@ import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; -import ca.uhn.fhir.jpa.provider.ReplaceReferenceRequest; +import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.api.server.IBundleProvider; From dae5b5582f4688aa88cbb65f827efc68a4b9f14b Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 11 Dec 2024 13:34:33 -0500 Subject: [PATCH 069/148] consolidate async and sync supporting methods into a single storage service --- .../ca/uhn/fhir/jpa/config/JpaConfig.java | 158 +++++++++--------- .../provider/ReplaceReferencesSvcImpl.java | 41 +++-- .../batch2/jobs/export/BulkExportAppCtx.java | 1 - .../ReplaceReferenceUpdateStep.java | 11 +- .../ReplaceReferencesJobParameters.java | 2 +- .../jpa/dao/merge/ResourceMergeService.java | 2 +- .../jpa/provider/IReplaceReferencesSvc.java | 2 +- .../ReplaceReferencesPatchBundleSvc.java | 38 +++-- 8 files changed, 129 insertions(+), 126 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index 9c5113c602fd..eed372a13c1b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -214,8 +214,8 @@ @Configuration // repositoryFactoryBeanClass: EnversRevisionRepositoryFactoryBean is needed primarily for unit testing @EnableJpaRepositories( - basePackages = "ca.uhn.fhir.jpa.dao.data", - repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class) + basePackages = "ca.uhn.fhir.jpa.dao.data", + repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class) @Import({ BeanPostProcessorConfig.class, TermCodeSystemConfig.class, @@ -236,7 +236,7 @@ public class JpaConfig { public static final String PERSISTED_JPA_BUNDLE_PROVIDER = "PersistedJpaBundleProvider"; public static final String PERSISTED_JPA_BUNDLE_PROVIDER_BY_SEARCH = "PersistedJpaBundleProvider_BySearch"; public static final String PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER = - "PersistedJpaSearchFirstPageBundleProvider"; + "PersistedJpaSearchFirstPageBundleProvider"; public static final String HISTORY_BUILDER = "HistoryBuilder"; public static final String DEFAULT_PROFILE_VALIDATION_SUPPORT = "myDefaultProfileValidationSupport"; private static final String HAPI_DEFAULT_SCHEDULER_GROUP = "HAPI"; @@ -266,12 +266,12 @@ public DaoRegistry daoRegistry() { @Lazy @Bean public CascadingDeleteInterceptor cascadingDeleteInterceptor( - FhirContext theFhirContext, - DaoRegistry theDaoRegistry, - IInterceptorBroadcaster theInterceptorBroadcaster, - ThreadSafeResourceDeleterSvc threadSafeResourceDeleterSvc) { + FhirContext theFhirContext, + DaoRegistry theDaoRegistry, + IInterceptorBroadcaster theInterceptorBroadcaster, + ThreadSafeResourceDeleterSvc threadSafeResourceDeleterSvc) { return new CascadingDeleteInterceptor( - theFhirContext, theDaoRegistry, theInterceptorBroadcaster, threadSafeResourceDeleterSvc); + theFhirContext, theDaoRegistry, theInterceptorBroadcaster, threadSafeResourceDeleterSvc); } @Bean @@ -282,24 +282,24 @@ public ExternallyStoredResourceServiceRegistry ExternallyStoredResourceServiceRe @Lazy @Bean public ThreadSafeResourceDeleterSvc safeDeleter( - DaoRegistry theDaoRegistry, - IInterceptorBroadcaster theInterceptorBroadcaster, - HapiTransactionService hapiTransactionService) { + DaoRegistry theDaoRegistry, + IInterceptorBroadcaster theInterceptorBroadcaster, + HapiTransactionService hapiTransactionService) { return new ThreadSafeResourceDeleterSvc(theDaoRegistry, theInterceptorBroadcaster, hapiTransactionService); } @Lazy @Bean public ResponseTerminologyTranslationInterceptor responseTerminologyTranslationInterceptor( - IValidationSupport theValidationSupport, - ResponseTerminologyTranslationSvc theResponseTerminologyTranslationSvc) { + IValidationSupport theValidationSupport, + ResponseTerminologyTranslationSvc theResponseTerminologyTranslationSvc) { return new ResponseTerminologyTranslationInterceptor( - theValidationSupport, theResponseTerminologyTranslationSvc); + theValidationSupport, theResponseTerminologyTranslationSvc); } @Bean public ResponseTerminologyTranslationSvc responseTerminologyTranslationSvc( - IValidationSupport theValidationSupport) { + IValidationSupport theValidationSupport) { return new ResponseTerminologyTranslationSvc(theValidationSupport); } @@ -351,9 +351,9 @@ public BinaryAccessProvider binaryAccessProvider() { @Bean(name = "myBinaryStorageInterceptor") @Lazy public BinaryStorageInterceptor> binaryStorageInterceptor( - JpaStorageSettings theStorageSettings, FhirContext theCtx) { + JpaStorageSettings theStorageSettings, FhirContext theCtx) { BinaryStorageInterceptor> interceptor = - new BinaryStorageInterceptor<>(theCtx); + new BinaryStorageInterceptor<>(theCtx); interceptor.setAllowAutoInflateBinaries(theStorageSettings.isAllowAutoInflateBinaries()); interceptor.setAutoInflateBinariesMaximumSize(theStorageSettings.getAutoInflateBinariesMaximumBytes()); return interceptor; @@ -403,16 +403,16 @@ public ITermConceptMappingSvc termConceptMappingSvc() { @Bean public TaskScheduler taskScheduler() { ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler( - scheduledExecutorService().getObject(), - scheduledExecutorService().getObject()); + scheduledExecutorService().getObject(), + scheduledExecutorService().getObject()); return retVal; } @Bean(name = TASK_EXECUTOR_NAME) public AsyncTaskExecutor taskExecutor() { ConcurrentTaskScheduler retVal = new ConcurrentTaskScheduler( - scheduledExecutorService().getObject(), - scheduledExecutorService().getObject()); + scheduledExecutorService().getObject(), + scheduledExecutorService().getObject()); return retVal; } @@ -446,7 +446,7 @@ public HapiFhirHibernateJpaDialect hibernateJpaDialect(FhirContext theFhirContex @Bean @Lazy public OverridePathBasedReferentialIntegrityForDeletesInterceptor - overridePathBasedReferentialIntegrityForDeletesInterceptor() { + overridePathBasedReferentialIntegrityForDeletesInterceptor() { return new OverridePathBasedReferentialIntegrityForDeletesInterceptor(); } @@ -523,13 +523,13 @@ public AutowiringSpringBeanJobFactory schedulerJobFactory() { @Bean public IBulkDataExportJobSchedulingHelper bulkDataExportJobSchedulingHelper( - DaoRegistry theDaoRegistry, - PlatformTransactionManager theTxManager, - JpaStorageSettings theStorageSettings, - BulkExportHelperService theBulkExportHelperSvc, - IJobPersistence theJpaJobPersistence) { + DaoRegistry theDaoRegistry, + PlatformTransactionManager theTxManager, + JpaStorageSettings theStorageSettings, + BulkExportHelperService theBulkExportHelperSvc, + IJobPersistence theJpaJobPersistence) { return new BulkDataExportJobSchedulingHelperImpl( - theDaoRegistry, theTxManager, theStorageSettings, theBulkExportHelperSvc, theJpaJobPersistence, null); + theDaoRegistry, theTxManager, theStorageSettings, theBulkExportHelperSvc, theJpaJobPersistence, null); } @Bean @@ -593,19 +593,19 @@ public PersistedJpaBundleProvider newPersistedJpaBundleProvider(RequestDetails t @Bean(name = PERSISTED_JPA_BUNDLE_PROVIDER_BY_SEARCH) @Scope("prototype") public PersistedJpaBundleProvider newPersistedJpaBundleProviderBySearch( - RequestDetails theRequest, Search theSearch) { + RequestDetails theRequest, Search theSearch) { return new PersistedJpaBundleProvider(theRequest, theSearch); } @Bean(name = PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER) @Scope("prototype") public PersistedJpaSearchFirstPageBundleProvider newPersistedJpaSearchFirstPageBundleProvider( - RequestDetails theRequest, - SearchTask theSearchTask, - ISearchBuilder theSearchBuilder, - RequestPartitionId theRequestPartitionId) { + RequestDetails theRequest, + SearchTask theSearchTask, + ISearchBuilder theSearchBuilder, + RequestPartitionId theRequestPartitionId) { return new PersistedJpaSearchFirstPageBundleProvider( - theSearchTask, theSearchBuilder, theRequest, theRequestPartitionId); + theSearchTask, theSearchBuilder, theRequest, theRequestPartitionId); } @Bean(name = RepositoryValidatingRuleBuilder.REPOSITORY_VALIDATING_RULE_BUILDER) @@ -617,14 +617,14 @@ public RepositoryValidatingRuleBuilder repositoryValidatingRuleBuilder(IValidati @Bean @Scope("prototype") public ComboUniqueSearchParameterPredicateBuilder newComboUniqueSearchParameterPredicateBuilder( - SearchQueryBuilder theSearchSqlBuilder) { + SearchQueryBuilder theSearchSqlBuilder) { return new ComboUniqueSearchParameterPredicateBuilder(theSearchSqlBuilder); } @Bean @Scope("prototype") public ComboNonUniqueSearchParameterPredicateBuilder newComboNonUniqueSearchParameterPredicateBuilder( - SearchQueryBuilder theSearchSqlBuilder) { + SearchQueryBuilder theSearchSqlBuilder) { return new ComboNonUniqueSearchParameterPredicateBuilder(theSearchSqlBuilder); } @@ -655,14 +655,14 @@ public QuantityPredicateBuilder newQuantityPredicateBuilder(SearchQueryBuilder t @Bean @Scope("prototype") public QuantityNormalizedPredicateBuilder newQuantityNormalizedPredicateBuilder( - SearchQueryBuilder theSearchBuilder) { + SearchQueryBuilder theSearchBuilder) { return new QuantityNormalizedPredicateBuilder(theSearchBuilder); } @Bean @Scope("prototype") public ResourceLinkPredicateBuilder newResourceLinkPredicateBuilder( - QueryStack theQueryStack, SearchQueryBuilder theSearchBuilder, boolean theReversed) { + QueryStack theQueryStack, SearchQueryBuilder theSearchBuilder, boolean theReversed) { return new ResourceLinkPredicateBuilder(theQueryStack, theSearchBuilder, theReversed); } @@ -687,7 +687,7 @@ public ResourceIdPredicateBuilder newResourceIdPredicateBuilder(SearchQueryBuild @Bean @Scope("prototype") public SearchParamPresentPredicateBuilder newSearchParamPresentPredicateBuilder( - SearchQueryBuilder theSearchBuilder) { + SearchQueryBuilder theSearchBuilder) { return new SearchParamPresentPredicateBuilder(theSearchBuilder); } @@ -712,7 +712,7 @@ public ResourceHistoryPredicateBuilder newResourceHistoryPredicateBuilder(Search @Bean @Scope("prototype") public ResourceHistoryProvenancePredicateBuilder newResourceHistoryProvenancePredicateBuilder( - SearchQueryBuilder theSearchBuilder) { + SearchQueryBuilder theSearchBuilder) { return new ResourceHistoryProvenancePredicateBuilder(theSearchBuilder); } @@ -731,10 +731,10 @@ public SearchQueryExecutor newSearchQueryExecutor(GeneratedSql theGeneratedSql, @Bean(name = HISTORY_BUILDER) @Scope("prototype") public HistoryBuilder newHistoryBuilder( - @Nullable String theResourceType, - @Nullable JpaPid theResourceId, - @Nullable Date theRangeStartInclusive, - @Nullable Date theRangeEndInclusive) { + @Nullable String theResourceType, + @Nullable JpaPid theResourceId, + @Nullable Date theRangeStartInclusive, + @Nullable Date theRangeEndInclusive) { return new HistoryBuilder(theResourceType, theResourceId, theRangeStartInclusive, theRangeEndInclusive); } @@ -772,10 +772,10 @@ public ExpungeService expungeService() { @Bean @Scope("prototype") public ExpungeOperation expungeOperation( - String theResourceName, - IResourcePersistentId theResourceId, - ExpungeOptions theExpungeOptions, - RequestDetails theRequestDetails) { + String theResourceName, + IResourcePersistentId theResourceId, + ExpungeOptions theExpungeOptions, + RequestDetails theRequestDetails) { return new ExpungeOperation(theResourceName, theResourceId, theExpungeOptions, theRequestDetails); } @@ -831,7 +831,7 @@ public ResourceLoaderImpl jpaResourceLoader() { @Bean public UnknownCodeSystemWarningValidationSupport unknownCodeSystemWarningValidationSupport( - FhirContext theFhirContext) { + FhirContext theFhirContext) { return new UnknownCodeSystemWarningValidationSupport(theFhirContext); } @@ -847,9 +847,9 @@ public VersionCanonicalizer versionCanonicalizer(FhirContext theFhirContext) { @Bean public SearchParameterDaoValidator searchParameterDaoValidator( - FhirContext theFhirContext, - JpaStorageSettings theStorageSettings, - ISearchParamRegistry theSearchParamRegistry) { + FhirContext theFhirContext, + JpaStorageSettings theStorageSettings, + ISearchParamRegistry theSearchParamRegistry) { return new SearchParameterDaoValidator(theFhirContext, theStorageSettings, theSearchParamRegistry); } @@ -887,17 +887,17 @@ public PersistenceContextProvider persistenceContextProvider() { @Bean public ResourceSearchUrlSvc resourceSearchUrlSvc( - PersistenceContextProvider thePersistenceContextProvider, - IResourceSearchUrlDao theResourceSearchUrlDao, - MatchUrlService theMatchUrlService, - FhirContext theFhirContext, - PartitionSettings thePartitionSettings) { + PersistenceContextProvider thePersistenceContextProvider, + IResourceSearchUrlDao theResourceSearchUrlDao, + MatchUrlService theMatchUrlService, + FhirContext theFhirContext, + PartitionSettings thePartitionSettings) { return new ResourceSearchUrlSvc( - thePersistenceContextProvider.getEntityManager(), - theResourceSearchUrlDao, - theMatchUrlService, - theFhirContext, - thePartitionSettings); + thePersistenceContextProvider.getEntityManager(), + theResourceSearchUrlDao, + theMatchUrlService, + theFhirContext, + thePartitionSettings); } @Bean @@ -907,12 +907,12 @@ public ISearchUrlJobMaintenanceSvc searchUrlJobMaintenanceSvc(ResourceSearchUrlS @Bean public IResourceModifiedMessagePersistenceSvc subscriptionMessagePersistence( - FhirContext theFhirContext, - IResourceModifiedDao theIResourceModifiedDao, - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService) { + FhirContext theFhirContext, + IResourceModifiedDao theIResourceModifiedDao, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService) { return new ResourceModifiedMessagePersistenceSvcImpl( - theFhirContext, theIResourceModifiedDao, theDaoRegistry, theHapiTransactionService); + theFhirContext, theIResourceModifiedDao, theDaoRegistry, theHapiTransactionService); } @Bean @@ -922,29 +922,29 @@ public IMetaTagSorter metaTagSorter() { @Bean public ResourceHistoryCalculator resourceHistoryCalculator( - FhirContext theFhirContext, HibernatePropertiesProvider theHibernatePropertiesProvider) { + FhirContext theFhirContext, HibernatePropertiesProvider theHibernatePropertiesProvider) { return new ResourceHistoryCalculator(theFhirContext, theHibernatePropertiesProvider.isOracleDialect()); } @Bean public CacheTagDefinitionDao tagDefinitionDao( - ITagDefinitionDao tagDefinitionDao, MemoryCacheService memoryCacheService) { + ITagDefinitionDao tagDefinitionDao, MemoryCacheService memoryCacheService) { return new CacheTagDefinitionDao(tagDefinitionDao, memoryCacheService); } @Bean public IReplaceReferencesSvc replaceReferencesSvc( - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IResourceLinkDao theResourceLinkDao, - IJobCoordinator theJobCoordinator, - ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundle) { + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IResourceLinkDao theResourceLinkDao, + IJobCoordinator theJobCoordinator, + ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundle) { return new ReplaceReferencesSvcImpl( - theDaoRegistry, - theHapiTransactionService, - theResourceLinkDao, - theJobCoordinator, - theReplaceReferencesPatchBundle); + theDaoRegistry, + theHapiTransactionService, + theResourceLinkDao, + theJobCoordinator, + theReplaceReferencesPatchBundle); } @Bean diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index c728cfaf94e9..760a82f0270a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -58,11 +58,11 @@ public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { private final ReplaceReferencesPatchBundleSvc myReplaceReferencesPatchBundleSvc; public ReplaceReferencesSvcImpl( - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IResourceLinkDao theResourceLinkDao, - IJobCoordinator theJobCoordinator, - ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc) { + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IResourceLinkDao theResourceLinkDao, + IJobCoordinator theJobCoordinator, + ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc) { myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; myResourceLinkDao = theResourceLinkDao; @@ -72,7 +72,7 @@ public ReplaceReferencesSvcImpl( @Override public IBaseParameters replaceReferences( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { theReplaceReferenceRequest.validateOrThrowInvalidParameterException(); if (theRequestDetails.isPreferAsync()) { @@ -86,12 +86,12 @@ public IBaseParameters replaceReferences( public Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails) { return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { return myResourceLinkDao.countResourcesTargetingFhirTypeAndFhirId( - theResourceId.getResourceType(), theResourceId.getIdPart()); + theResourceId.getResourceType(), theResourceId.getIdPart()); }); } private IBaseParameters replaceReferencesPreferAsync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { Task task = new Task(); task.setStatus(Task.TaskStatus.INPROGRESS); IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(Task.class); @@ -110,8 +110,8 @@ private IBaseParameters replaceReferencesPreferAsync( task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); task.getMeta().setVersionId(null); retval.addParameter() - .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) - .setResource(task); + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) + .setResource(task); return retval; } @@ -120,36 +120,35 @@ private IBaseParameters replaceReferencesPreferAsync( */ @Nonnull private IBaseParameters replaceReferencesPreferSync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { // TODO KHS get partition from request StopLimitAccumulator accumulator = myHapiTransactionService - .withRequest(theRequestDetails) - .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); + .withRequest(theRequestDetails) + .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); if (accumulator.isTruncated()) { ourLog.warn("Too many results. Switching to asynchronous reference replacement."); return replaceReferencesPreferAsync(theReplaceReferenceRequest, theRequestDetails); } - - Bundle result = myReplaceReferencesPatchBundleSvc. - patchReferencingResources(theReplaceReferenceRequest, accumulator.getItemList(), theRequestDetails); + Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources( + theReplaceReferenceRequest, accumulator.getItemList(), theRequestDetails); Parameters retval = new Parameters(); retval.addParameter() - .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME) - .setResource(result); + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME) + .setResource(result); return retval; } private @Nonnull StopLimitAccumulator getAllPidsWithLimit( - ReplaceReferenceRequest theReplaceReferenceRequest) { + ReplaceReferenceRequest theReplaceReferenceRequest) { Stream idStream = myResourceLinkDao.streamSourceIdsForTargetFhirId( - theReplaceReferenceRequest.sourceId.getResourceType(), theReplaceReferenceRequest.sourceId.getIdPart()); + theReplaceReferenceRequest.sourceId.getResourceType(), theReplaceReferenceRequest.sourceId.getIdPart()); StopLimitAccumulator accumulator = - StopLimitAccumulator.fromStreamAndLimit(idStream, theReplaceReferenceRequest.batchSize); + StopLimitAccumulator.fromStreamAndLimit(idStream, theReplaceReferenceRequest.batchSize); return accumulator; } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportAppCtx.java index 9d9e90822c61..f898b388e4cc 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/export/BulkExportAppCtx.java @@ -30,7 +30,6 @@ import ca.uhn.fhir.util.Batch2JobDefinitionConstants; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Scope; @Configuration public class BulkExportAppCtx { diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java index 7abcbee46ebf..18cc989c3159 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java @@ -25,7 +25,8 @@ public class ReplaceReferenceUpdateStep private final FhirContext myFhirContext; private final ReplaceReferencesPatchBundleSvc myReplaceReferencesPatchBundleSvc; - public ReplaceReferenceUpdateStep(FhirContext theFhirContext, ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc) { + public ReplaceReferenceUpdateStep( + FhirContext theFhirContext, ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc) { myFhirContext = theFhirContext; myReplaceReferencesPatchBundleSvc = theReplaceReferencesPatchBundleSvc; } @@ -41,16 +42,18 @@ public RunOutcome run( ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); ReplaceReferenceRequest replaceReferencesRequest = params.asReplaceReferencesRequest(); - List fhirIds = theStepExecutionDetails.getData().getFhirIds().stream().map(FhirIdJson::asIdDt).collect(Collectors.toList()); + List fhirIds = theStepExecutionDetails.getData().getFhirIds().stream() + .map(FhirIdJson::asIdDt) + .collect(Collectors.toList()); SystemRequestDetails requestDetails = SystemRequestDetails.forRequestPartitionId(params.getPartitionId()); - Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources(replaceReferencesRequest, fhirIds, requestDetails); + Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources( + replaceReferencesRequest, fhirIds, requestDetails); ReplaceReferencePatchOutcomeJson data = new ReplaceReferencePatchOutcomeJson(myFhirContext, result); theDataSink.accept(data); return new RunOutcome(result.getEntry().size()); } - } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java index 1dd3ed52b997..749d64e2c8a8 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java @@ -2,8 +2,8 @@ import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; import ca.uhn.fhir.interceptor.model.RequestPartitionId; -import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; import ca.uhn.fhir.model.api.BaseBatchJobParameters; +import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; import com.fasterxml.jackson.annotation.JsonProperty; import org.hl7.fhir.instance.model.api.IIdType; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index 701a30087cdd..c7d9a816491b 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -27,8 +27,8 @@ import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; -import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.TokenAndListParam; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java index 4a00b5b121ed..603f9627dbcc 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java @@ -30,7 +30,7 @@ public interface IReplaceReferencesSvc { IBaseParameters replaceReferences( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails); + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails); Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java index d3526ece8c35..be20df4e28bb 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java @@ -6,7 +6,6 @@ import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.util.BundleBuilder; import ca.uhn.fhir.util.ResourceReferenceInfo; import jakarta.annotation.Nonnull; @@ -39,7 +38,10 @@ public ReplaceReferencesPatchBundleSvc(DaoRegistry theDaoRegistry) { myFhirContext = theDaoRegistry.getFhirContext(); } - public Bundle patchReferencingResources(ReplaceReferenceRequest theReplaceReferenceRequest, List theResourceIds, RequestDetails theRequestDetails) { + public Bundle patchReferencingResources( + ReplaceReferenceRequest theReplaceReferenceRequest, + List theResourceIds, + RequestDetails theRequestDetails) { Bundle patchBundle = buildPatchBundle(theReplaceReferenceRequest, theResourceIds, theRequestDetails); IFhirSystemDao systemDao = myDaoRegistry.getSystemDao(); Bundle result = systemDao.transaction(theRequestDetails, patchBundle); @@ -49,9 +51,9 @@ public Bundle patchReferencingResources(ReplaceReferenceRequest theReplaceRefere } private Bundle buildPatchBundle( - ReplaceReferenceRequest theReplaceReferenceRequest, - List theResourceIds, - RequestDetails theRequestDetails) { + ReplaceReferenceRequest theReplaceReferenceRequest, + List theResourceIds, + RequestDetails theRequestDetails) { BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); theResourceIds.forEach(referencingResourceId -> { @@ -65,31 +67,31 @@ private Bundle buildPatchBundle( } private @Nonnull Parameters buildPatchParams( - ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { + ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { Parameters params = new Parameters(); myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() - .filter(refInfo -> matches( - refInfo, - theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource - .map(refInfo -> createReplaceReferencePatchOperation( - referencingResource.fhirType() + "." + refInfo.getName(), - new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) - .forEach(params::addParameter); // Add each operation to parameters + .filter(refInfo -> matches( + refInfo, + theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource + .map(refInfo -> createReplaceReferencePatchOperation( + referencingResource.fhirType() + "." + refInfo.getName(), + new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) + .forEach(params::addParameter); // Add each operation to parameters return params; } private static boolean matches(ResourceReferenceInfo refInfo, IIdType theSourceId) { return refInfo.getResourceReference() - .getReferenceElement() - .toUnqualifiedVersionless() - .getValueAsString() - .equals(theSourceId.getValueAsString()); + .getReferenceElement() + .toUnqualifiedVersionless() + .getValueAsString() + .equals(theSourceId.getValueAsString()); } @Nonnull private Parameters.ParametersParameterComponent createReplaceReferencePatchOperation( - String thePath, Type theValue) { + String thePath, Type theValue) { Parameters.ParametersParameterComponent operation = new Parameters.ParametersParameterComponent(); operation.setName(PARAMETER_OPERATION); From 775090a8cc51362547bbeea561bdaf5a3debefd1 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 11 Dec 2024 15:07:37 -0500 Subject: [PATCH 070/148] add merge batch job --- .../jpa/provider/merge/MergeBatchTest.java | 84 +++++++++++++++++++ .../ReplaceReferencesBatchTest.java | 4 +- .../ReplaceReferencesTestHelper.java | 24 +++--- .../batch2/jobs/config/Batch2JobsConfig.java | 4 +- .../fhir/batch2/jobs/merge/MergeAppCtx.java | 63 ++++++++++++++ .../batch2/jobs/merge/MergeJobParameters.java | 7 ++ .../merge/MergeUpdateTaskReducerStep.java | 11 +++ .../ReplaceReferenceUpdateStep.java | 6 +- ...ReplaceReferenceUpdateTaskReducerStep.java | 12 +-- .../ReplaceReferencesAppCtx.java | 21 +++-- .../ReplaceReferencesQueryIdsStep.java | 6 +- 11 files changed, 206 insertions(+), 36 deletions(-) create mode 100644 hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java new file mode 100644 index 000000000000..56247ecd3c39 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java @@ -0,0 +1,84 @@ +package ca.uhn.fhir.jpa.provider.merge; + +import ca.uhn.fhir.batch2.api.IJobCoordinator; +import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; +import ca.uhn.fhir.batch2.jobs.merge.MergeJobParameters; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceResultsJson; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; +import ca.uhn.fhir.batch2.model.JobInstance; +import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; +import ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper; +import ca.uhn.fhir.jpa.test.BaseJpaR4Test; +import ca.uhn.fhir.jpa.test.Batch2JobHelper; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.util.JsonUtil; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Task; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static ca.uhn.fhir.batch2.jobs.merge.MergeAppCtx.JOB_MERGE; +import static ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx.JOB_REPLACE_REFERENCES; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MergeBatchTest extends BaseJpaR4Test { + + @Autowired + private IJobCoordinator myJobCoordinator; + @Autowired + private DaoRegistry myDaoRegistry; + @Autowired + private Batch2JobHelper myBatch2JobHelper; + + SystemRequestDetails mySrd = new SystemRequestDetails(); + + private ReplaceReferencesTestHelper myTestHelper; + + @Override + @BeforeEach + public void before() throws Exception { + super.before(); + + myTestHelper = new ReplaceReferencesTestHelper(myFhirContext, myDaoRegistry); + myTestHelper.beforeEach(); + + mySrd.setRequestPartitionId(RequestPartitionId.allPartitions()); + } + + @Test + public void testHappyPath() { + IIdType taskId = createReplaceReferencesTask(); + + MergeJobParameters jobParams = new MergeJobParameters(); + jobParams.setSourceId(new FhirIdJson(myTestHelper.getSourcePatientId())); + jobParams.setTargetId(new FhirIdJson(myTestHelper.getTargetPatientId())); + jobParams.setTaskId(taskId); + + JobInstanceStartRequest request = new JobInstanceStartRequest(JOB_MERGE, jobParams); + Batch2JobStartResponse jobStartResponse = myJobCoordinator.startInstance(mySrd, request); + JobInstance jobInstance = myBatch2JobHelper.awaitJobCompletion(jobStartResponse); + + // FIXME KHS assert outcome + String report = jobInstance.getReport(); + ReplaceReferenceResultsJson replaceReferenceResultsJson = JsonUtil.deserialize(report, ReplaceReferenceResultsJson.class); + IdDt resultTaskId = replaceReferenceResultsJson.getTaskId().asIdDt(); + assertEquals(taskId.getIdPart(), resultTaskId.getIdPart()); + + Bundle patchResultBundle = myTestHelper.validateCompletedTask(taskId); + myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES); + + myTestHelper.assertAllReferencesUpdated(); + } + + private IIdType createReplaceReferencesTask() { + Task task = new Task(); + task.setStatus(Task.TaskStatus.INPROGRESS); + return myTaskDao.create(task, mySrd).getId().toUnqualifiedVersionless(); + } +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java index 058ba01091d0..cbf34adb4a62 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java @@ -53,8 +53,8 @@ public void testHappyPath() { IIdType taskId = createReplaceReferencesTask(); ReplaceReferencesJobParameters jobParams = new ReplaceReferencesJobParameters(); - jobParams.setSourceId(new FhirIdJson(myTestHelper.mySourcePatientId)); - jobParams.setTargetId(new FhirIdJson(myTestHelper.myTargetPatientId)); + jobParams.setSourceId(new FhirIdJson(myTestHelper.getSourcePatientId())); + jobParams.setTargetId(new FhirIdJson(myTestHelper.getTargetPatientId())); jobParams.setTaskId(taskId); JobInstanceStartRequest request = new JobInstanceStartRequest(JOB_REPLACE_REFERENCES, jobParams); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index d9b5ea213b4b..db027eda5de0 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -66,15 +66,15 @@ public class ReplaceReferencesTestHelper { private final IFhirResourceDao myCarePlanDao; private final IFhirResourceDao myObservationDao; - IIdType myOrgId; - IIdType mySourcePatientId; - IIdType mySourceCarePlanId; - IIdType mySourceEncId1; - IIdType mySourceEncId2; - ArrayList mySourceObsIds; - IIdType myTargetPatientId; - IIdType myTargetEnc1; - Patient myResultPatient; + private IIdType myOrgId; + private IIdType mySourcePatientId; + private IIdType mySourceCarePlanId; + private IIdType mySourceEncId1; + private IIdType mySourceEncId2; + private ArrayList mySourceObsIds; + private IIdType myTargetPatientId; + private IIdType myTargetEnc1; + private Patient myResultPatient; private final FhirContext myFhirContext; private final SystemRequestDetails mySrd = new SystemRequestDetails(); @@ -168,7 +168,7 @@ public Patient readSourcePatient() { return myPatientDao.read(mySourcePatientId, mySrd); } - public Object getTargetPatientId() { + public IIdType getTargetPatientId() { return myTargetPatientId; } @@ -280,6 +280,10 @@ public PatientMergeInputParameters buildMultipleSourceMatchParameters(boolean th return inParams; } + public IIdType getSourcePatientId() { + return mySourcePatientId; + } + public static class PatientMergeInputParameters { public Type sourcePatient; public Type sourcePatientIdentifier; diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java index b4768653098e..3ae549ff96af 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java @@ -23,6 +23,7 @@ import ca.uhn.fhir.batch2.jobs.expunge.DeleteExpungeAppCtx; import ca.uhn.fhir.batch2.jobs.importpull.BulkImportPullConfig; import ca.uhn.fhir.batch2.jobs.imprt.BulkImportAppCtx; +import ca.uhn.fhir.batch2.jobs.merge.MergeAppCtx; import ca.uhn.fhir.batch2.jobs.reindex.ReindexAppCtx; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx; import ca.uhn.fhir.batch2.jobs.termcodesystem.TermCodeSystemJobConfig; @@ -39,6 +40,7 @@ BulkExportAppCtx.class, TermCodeSystemJobConfig.class, BulkImportPullConfig.class, - ReplaceReferencesAppCtx.class + ReplaceReferencesAppCtx.class, + MergeAppCtx.class, }) public class Batch2JobsConfig {} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java new file mode 100644 index 000000000000..1ce79f8f3727 --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java @@ -0,0 +1,63 @@ +package ca.uhn.fhir.batch2.jobs.merge; + +import ca.uhn.fhir.batch2.jobs.chunk.FhirIdListWorkChunkJson; +import ca.uhn.fhir.batch2.jobs.replacereferences.*; +import ca.uhn.fhir.batch2.model.JobDefinition; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc; +import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; +import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MergeAppCtx { + public static final String JOB_MERGE = "MERGE"; + + @Bean + public JobDefinition merge( + ReplaceReferencesQueryIdsStep theMergeQueryIds, + ReplaceReferenceUpdateStep theMergeUpdateStep, + MergeUpdateTaskReducerStep theMergeUpdateTaskReducerStep) { + return JobDefinition.newBuilder() + .setJobDefinitionId(JOB_MERGE) + .setJobDescription("Merge Resources") + .setJobDefinitionVersion(1) + .gatedExecution() + .setParametersType(MergeJobParameters.class) + .addFirstStep( + "query-ids", + "Query IDs of resources that link to the source resource", + FhirIdListWorkChunkJson.class, + theMergeQueryIds) + .addIntermediateStep( + "replace-references", + "Update all references from pointing to source to pointing to target", + ReplaceReferencePatchOutcomeJson.class, + theMergeUpdateStep) + .addFinalReducerStep( + "update-task", + "Waits for replace reference work to complete and updates Task.", + ReplaceReferenceResultsJson.class, + theMergeUpdateTaskReducerStep) + .build(); + } + + @Bean + public ReplaceReferencesQueryIdsStep mergeQueryIdsStep( + HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { + return new ReplaceReferencesQueryIdsStep(theHapiTransactionService, theBatch2DaoSvc); + } + + @Bean + public ReplaceReferenceUpdateStep mergeUpdateStep( + FhirContext theFhirContext, ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc) { + return new ReplaceReferenceUpdateStep(theFhirContext, theReplaceReferencesPatchBundleSvc); + } + + @Bean + public MergeUpdateTaskReducerStep mergeUpdateTaskStep(DaoRegistry theDaoRegistry) { + return new MergeUpdateTaskReducerStep(theDaoRegistry); + } +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java new file mode 100644 index 000000000000..30b9748604b2 --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java @@ -0,0 +1,7 @@ +package ca.uhn.fhir.batch2.jobs.merge; + +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; + +public class MergeJobParameters extends ReplaceReferencesJobParameters { +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java new file mode 100644 index 000000000000..4d78343239ec --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java @@ -0,0 +1,11 @@ +package ca.uhn.fhir.batch2.jobs.merge; + +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceUpdateTaskReducerStep; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; + +public class MergeUpdateTaskReducerStep extends ReplaceReferenceUpdateTaskReducerStep { + public MergeUpdateTaskReducerStep(DaoRegistry theDaoRegistry) { + super(theDaoRegistry); + } +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java index 18cc989c3159..368f003f0c69 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java @@ -18,9 +18,9 @@ import java.util.List; import java.util.stream.Collectors; -public class ReplaceReferenceUpdateStep +public class ReplaceReferenceUpdateStep implements IJobStepWorker< - ReplaceReferencesJobParameters, FhirIdListWorkChunkJson, ReplaceReferencePatchOutcomeJson> { + PT, FhirIdListWorkChunkJson, ReplaceReferencePatchOutcomeJson> { private final FhirContext myFhirContext; private final ReplaceReferencesPatchBundleSvc myReplaceReferencesPatchBundleSvc; @@ -35,7 +35,7 @@ public ReplaceReferenceUpdateStep( @Override public RunOutcome run( @Nonnull - StepExecutionDetails + StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java index 4e7e5314141e..a653db67f242 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java @@ -19,9 +19,9 @@ import java.util.ArrayList; import java.util.List; -public class ReplaceReferenceUpdateTaskReducerStep +public class ReplaceReferenceUpdateTaskReducerStep implements IReductionStepWorker< - ReplaceReferencesJobParameters, ReplaceReferencePatchOutcomeJson, ReplaceReferenceResultsJson> { + PT, ReplaceReferencePatchOutcomeJson, ReplaceReferenceResultsJson> { public static final String RESOURCE_TYPES_SYSTEM = "http://hl7.org/fhir/ValueSet/resource-types"; private final FhirContext myFhirContext; @@ -29,15 +29,15 @@ public class ReplaceReferenceUpdateTaskReducerStep private List myPatchOutputBundles = new ArrayList<>(); - public ReplaceReferenceUpdateTaskReducerStep(FhirContext theFhirContext, DaoRegistry theDaoRegistry) { - myFhirContext = theFhirContext; + public ReplaceReferenceUpdateTaskReducerStep(DaoRegistry theDaoRegistry) { myDaoRegistry = theDaoRegistry; + myFhirContext = theDaoRegistry.getFhirContext(); } @Nonnull @Override public ChunkOutcome consume( - ChunkExecutionDetails theChunkDetails) { + ChunkExecutionDetails theChunkDetails) { ReplaceReferencePatchOutcomeJson result = theChunkDetails.getData(); Bundle patchOutputBundle = myFhirContext.newJsonParser().parseResource(Bundle.class, result.getPatchResponseBundle()); @@ -49,7 +49,7 @@ public ChunkOutcome consume( @Override public RunOutcome run( @Nonnull - StepExecutionDetails + StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java index 603e9a6a37b9..0ab813c10549 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java @@ -15,10 +15,10 @@ public class ReplaceReferencesAppCtx { public static final String JOB_REPLACE_REFERENCES = "REPLACE_REFERENCES"; @Bean - public JobDefinition bulkImport2JobDefinition( - ReplaceReferencesQueryIdsStep theReplaceReferencesQueryIds, - ReplaceReferenceUpdateStep theReplaceReferenceUpdateStep, - ReplaceReferenceUpdateTaskReducerStep theReplaceReferenceUpdateTaskReducerStep) { + public JobDefinition replaceReferencesJobDefinition( + ReplaceReferencesQueryIdsStep theReplaceReferencesQueryIds, + ReplaceReferenceUpdateStep theReplaceReferenceUpdateStep, + ReplaceReferenceUpdateTaskReducerStep theReplaceReferenceUpdateTaskReducerStep) { return JobDefinition.newBuilder() .setJobDefinitionId(JOB_REPLACE_REFERENCES) .setJobDescription("Replace References") @@ -44,20 +44,19 @@ public JobDefinition bulkImport2JobDefinition( } @Bean - public ReplaceReferencesQueryIdsStep replaceReferencesQueryIdsStep( + public ReplaceReferencesQueryIdsStep replaceReferencesQueryIdsStep( HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { - return new ReplaceReferencesQueryIdsStep(theHapiTransactionService, theBatch2DaoSvc); + return new ReplaceReferencesQueryIdsStep<>(theHapiTransactionService, theBatch2DaoSvc); } @Bean - public ReplaceReferenceUpdateStep replaceReferenceUpdateStep( + public ReplaceReferenceUpdateStep replaceReferenceUpdateStep( FhirContext theFhirContext, ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc) { - return new ReplaceReferenceUpdateStep(theFhirContext, theReplaceReferencesPatchBundleSvc); + return new ReplaceReferenceUpdateStep<>(theFhirContext, theReplaceReferencesPatchBundleSvc); } @Bean - public ReplaceReferenceUpdateTaskReducerStep replaceReferenceUpdateTaskStep( - FhirContext theFhirContext, DaoRegistry theDaoRegistry) { - return new ReplaceReferenceUpdateTaskReducerStep(theFhirContext, theDaoRegistry); + public ReplaceReferenceUpdateTaskReducerStep replaceReferenceUpdateTaskStep(DaoRegistry theDaoRegistry) { + return new ReplaceReferenceUpdateTaskReducerStep<>(theDaoRegistry); } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java index 37056ebd9106..4246041fce71 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java @@ -18,8 +18,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; -public class ReplaceReferencesQueryIdsStep - implements IJobStepWorker { +public class ReplaceReferencesQueryIdsStep + implements IJobStepWorker { private final HapiTransactionService myHapiTransactionService; private final IBatch2DaoSvc myBatch2DaoSvc; @@ -33,7 +33,7 @@ public ReplaceReferencesQueryIdsStep( @Nonnull @Override public RunOutcome run( - @Nonnull StepExecutionDetails theStepExecutionDetails, + @Nonnull StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); From 5c2cac8f4ca2519524e2ac9581ed7ab04ce39dfd Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 11 Dec 2024 15:08:05 -0500 Subject: [PATCH 071/148] add merge batch job --- .../fhir/batch2/jobs/merge/MergeAppCtx.java | 52 +++++++++---------- .../batch2/jobs/merge/MergeJobParameters.java | 4 +- .../merge/MergeUpdateTaskReducerStep.java | 1 - .../ReplaceReferenceUpdateStep.java | 7 +-- ...ReplaceReferenceUpdateTaskReducerStep.java | 10 ++-- .../ReplaceReferencesAppCtx.java | 6 ++- 6 files changed, 36 insertions(+), 44 deletions(-) diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java index 1ce79f8f3727..4dd595578fff 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java @@ -17,42 +17,42 @@ public class MergeAppCtx { @Bean public JobDefinition merge( - ReplaceReferencesQueryIdsStep theMergeQueryIds, - ReplaceReferenceUpdateStep theMergeUpdateStep, - MergeUpdateTaskReducerStep theMergeUpdateTaskReducerStep) { + ReplaceReferencesQueryIdsStep theMergeQueryIds, + ReplaceReferenceUpdateStep theMergeUpdateStep, + MergeUpdateTaskReducerStep theMergeUpdateTaskReducerStep) { return JobDefinition.newBuilder() - .setJobDefinitionId(JOB_MERGE) - .setJobDescription("Merge Resources") - .setJobDefinitionVersion(1) - .gatedExecution() - .setParametersType(MergeJobParameters.class) - .addFirstStep( - "query-ids", - "Query IDs of resources that link to the source resource", - FhirIdListWorkChunkJson.class, - theMergeQueryIds) - .addIntermediateStep( - "replace-references", - "Update all references from pointing to source to pointing to target", - ReplaceReferencePatchOutcomeJson.class, - theMergeUpdateStep) - .addFinalReducerStep( - "update-task", - "Waits for replace reference work to complete and updates Task.", - ReplaceReferenceResultsJson.class, - theMergeUpdateTaskReducerStep) - .build(); + .setJobDefinitionId(JOB_MERGE) + .setJobDescription("Merge Resources") + .setJobDefinitionVersion(1) + .gatedExecution() + .setParametersType(MergeJobParameters.class) + .addFirstStep( + "query-ids", + "Query IDs of resources that link to the source resource", + FhirIdListWorkChunkJson.class, + theMergeQueryIds) + .addIntermediateStep( + "replace-references", + "Update all references from pointing to source to pointing to target", + ReplaceReferencePatchOutcomeJson.class, + theMergeUpdateStep) + .addFinalReducerStep( + "update-task", + "Waits for replace reference work to complete and updates Task.", + ReplaceReferenceResultsJson.class, + theMergeUpdateTaskReducerStep) + .build(); } @Bean public ReplaceReferencesQueryIdsStep mergeQueryIdsStep( - HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { + HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { return new ReplaceReferencesQueryIdsStep(theHapiTransactionService, theBatch2DaoSvc); } @Bean public ReplaceReferenceUpdateStep mergeUpdateStep( - FhirContext theFhirContext, ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc) { + FhirContext theFhirContext, ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc) { return new ReplaceReferenceUpdateStep(theFhirContext, theReplaceReferencesPatchBundleSvc); } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java index 30b9748604b2..fba8a18a3f52 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java @@ -1,7 +1,5 @@ package ca.uhn.fhir.batch2.jobs.merge; -import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; -public class MergeJobParameters extends ReplaceReferencesJobParameters { -} +public class MergeJobParameters extends ReplaceReferencesJobParameters {} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java index 4d78343239ec..8324f7111b33 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java @@ -1,7 +1,6 @@ package ca.uhn.fhir.batch2.jobs.merge; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceUpdateTaskReducerStep; -import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; public class MergeUpdateTaskReducerStep extends ReplaceReferenceUpdateTaskReducerStep { diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java index 368f003f0c69..7bba901a5398 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java @@ -19,8 +19,7 @@ import java.util.stream.Collectors; public class ReplaceReferenceUpdateStep - implements IJobStepWorker< - PT, FhirIdListWorkChunkJson, ReplaceReferencePatchOutcomeJson> { + implements IJobStepWorker { private final FhirContext myFhirContext; private final ReplaceReferencesPatchBundleSvc myReplaceReferencesPatchBundleSvc; @@ -34,9 +33,7 @@ public ReplaceReferenceUpdateStep( @Nonnull @Override public RunOutcome run( - @Nonnull - StepExecutionDetails - theStepExecutionDetails, + @Nonnull StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java index a653db67f242..f1d3176195e3 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java @@ -20,8 +20,7 @@ import java.util.List; public class ReplaceReferenceUpdateTaskReducerStep - implements IReductionStepWorker< - PT, ReplaceReferencePatchOutcomeJson, ReplaceReferenceResultsJson> { + implements IReductionStepWorker { public static final String RESOURCE_TYPES_SYSTEM = "http://hl7.org/fhir/ValueSet/resource-types"; private final FhirContext myFhirContext; @@ -36,8 +35,7 @@ public ReplaceReferenceUpdateTaskReducerStep(DaoRegistry theDaoRegistry) { @Nonnull @Override - public ChunkOutcome consume( - ChunkExecutionDetails theChunkDetails) { + public ChunkOutcome consume(ChunkExecutionDetails theChunkDetails) { ReplaceReferencePatchOutcomeJson result = theChunkDetails.getData(); Bundle patchOutputBundle = myFhirContext.newJsonParser().parseResource(Bundle.class, result.getPatchResponseBundle()); @@ -48,9 +46,7 @@ public ChunkOutcome consume( @Nonnull @Override public RunOutcome run( - @Nonnull - StepExecutionDetails - theStepExecutionDetails, + @Nonnull StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java index 0ab813c10549..d2432e9ee29b 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java @@ -18,7 +18,8 @@ public class ReplaceReferencesAppCtx { public JobDefinition replaceReferencesJobDefinition( ReplaceReferencesQueryIdsStep theReplaceReferencesQueryIds, ReplaceReferenceUpdateStep theReplaceReferenceUpdateStep, - ReplaceReferenceUpdateTaskReducerStep theReplaceReferenceUpdateTaskReducerStep) { + ReplaceReferenceUpdateTaskReducerStep + theReplaceReferenceUpdateTaskReducerStep) { return JobDefinition.newBuilder() .setJobDefinitionId(JOB_REPLACE_REFERENCES) .setJobDescription("Replace References") @@ -56,7 +57,8 @@ public ReplaceReferenceUpdateStep replaceReferen } @Bean - public ReplaceReferenceUpdateTaskReducerStep replaceReferenceUpdateTaskStep(DaoRegistry theDaoRegistry) { + public ReplaceReferenceUpdateTaskReducerStep replaceReferenceUpdateTaskStep( + DaoRegistry theDaoRegistry) { return new ReplaceReferenceUpdateTaskReducerStep<>(theDaoRegistry); } } From 2c5aa70f0b7f34244b857a96ad58f9dad8c2a96e Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 11 Dec 2024 15:21:55 -0500 Subject: [PATCH 072/148] added FIXMES for ED --- .../fhir/jpa/provider/merge/MergeBatchTest.java | 3 +++ .../fhir/batch2/jobs/merge/MergeJobParameters.java | 4 +++- .../jobs/merge/MergeUpdateTaskReducerStep.java | 14 ++++++++++++++ .../fhir/jpa/dao/merge/ResourceMergeService.java | 5 +++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java index 56247ecd3c39..f81d7e8e14df 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java @@ -59,6 +59,7 @@ public void testHappyPath() { jobParams.setSourceId(new FhirIdJson(myTestHelper.getSourcePatientId())); jobParams.setTargetId(new FhirIdJson(myTestHelper.getTargetPatientId())); jobParams.setTaskId(taskId); + // FIXME ED add to parameters JobInstanceStartRequest request = new JobInstanceStartRequest(JOB_MERGE, jobParams); Batch2JobStartResponse jobStartResponse = myJobCoordinator.startInstance(mySrd, request); @@ -73,6 +74,8 @@ public void testHappyPath() { Bundle patchResultBundle = myTestHelper.validateCompletedTask(taskId); myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES); + // FIXME ED validate other steps performed by final run merge step + myTestHelper.assertAllReferencesUpdated(); } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java index fba8a18a3f52..f397d225319a 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java @@ -2,4 +2,6 @@ import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; -public class MergeJobParameters extends ReplaceReferencesJobParameters {} +public class MergeJobParameters extends ReplaceReferencesJobParameters { + // FIXME ED add delete parameter, and maybe others +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java index 8324f7111b33..e7949ec42d5c 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java @@ -1,10 +1,24 @@ package ca.uhn.fhir.batch2.jobs.merge; +import ca.uhn.fhir.batch2.api.IJobDataSink; +import ca.uhn.fhir.batch2.api.JobExecutionFailedException; +import ca.uhn.fhir.batch2.api.RunOutcome; +import ca.uhn.fhir.batch2.api.StepExecutionDetails; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencePatchOutcomeJson; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceResultsJson; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceUpdateTaskReducerStep; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import jakarta.annotation.Nonnull; public class MergeUpdateTaskReducerStep extends ReplaceReferenceUpdateTaskReducerStep { public MergeUpdateTaskReducerStep(DaoRegistry theDaoRegistry) { super(theDaoRegistry); } + + @Nonnull + @Override + public RunOutcome run(@Nonnull StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { + // FIXME ED add in extra merge steps here e.g. updating source and target resources + return super.run(theStepExecutionDetails, theDataSink); + } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index c7d9a816491b..b15da1fa1657 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -92,6 +92,10 @@ public ResourceMergeService( public MergeOperationOutcome merge( MergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + // FIXME ED update to work like ReplaceReferencesSvcImpl.replaceReferences() + // in replaceReferencesPreferSync, still need to fallback to async if count exceeds batchSize, + // but don't need to stream resources, can just use count method for that. + MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); mergeOutcome.setOperationOutcome(operationOutcome); @@ -195,6 +199,7 @@ private void mergeInTransaction( theMergeOperationParameters.getBatchSize(), partitionId); + // FIXME ED this will need to change because this calls JOB_REPLACE_REFERENCES when you want to call JOB_MERGE Parameters replaceRefsOutParams = (Parameters) myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); From 629b9e4d287a132f335e6edbaef724637b305429 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 11 Dec 2024 15:22:07 -0500 Subject: [PATCH 073/148] added FIXMES for ED --- .../fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java index e7949ec42d5c..70a97a1c49cc 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java @@ -17,7 +17,10 @@ public MergeUpdateTaskReducerStep(DaoRegistry theDaoRegistry) { @Nonnull @Override - public RunOutcome run(@Nonnull StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { + public RunOutcome run( + @Nonnull StepExecutionDetails theStepExecutionDetails, + @Nonnull IJobDataSink theDataSink) + throws JobExecutionFailedException { // FIXME ED add in extra merge steps here e.g. updating source and target resources return super.run(theStepExecutionDetails, theDataSink); } From 293d03ece8cdde38b2666b3947f59d892b7e7783 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Wed, 11 Dec 2024 14:58:47 -0500 Subject: [PATCH 074/148] update test to not validate task in preview mode --- .../java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index bc32e53e54c3..b033275c9961 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -28,6 +28,7 @@ import org.junit.jupiter.params.provider.CsvSource; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; @@ -128,8 +129,8 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa assertTrue(input.equalsDeep(inParameters)); - // Assert Task - if (isAsync) { + // Assert Task inAsync mode, unless it is preview in which case we don't return a task + if (isAsync && !withPreview) { Task task = (Task) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource(); assertNull(task.getIdElement().getVersionIdPart()); ourLog.info("Got task {}", task.getId()); From 1fe54ca1a8d7ca5a0011da706229208559b23c66 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Wed, 11 Dec 2024 15:36:39 -0500 Subject: [PATCH 075/148] mark identfier copied from source to target as old --- .../jpa/dao/merge/ResourceMergeService.java | 10 ++++++---- .../dao/merge/ResourceMergeServiceTest.java | 20 ++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java index b15da1fa1657..eacc20fc3aa2 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java @@ -427,7 +427,7 @@ private Patient prepareTargetPatientForUpdate( } // copy all identifiers from the source to the target - copyIdentifiers(theSourceResource, theTargetResource); + copyIdentifiersAndMarkOld(theSourceResource, theTargetResource); return theTargetResource; } @@ -450,18 +450,20 @@ private boolean containsIdentifier(List theIdentifiers, Identifier t /** * Copies each identifier from theSourceResource to theTargetResource, after checking that theTargetResource does - * not already contain the source identifier + * not already contain the source identifier. Marks the copied identifiers marked as old. * * @param theSourceResource the source resource to copy identifiers from * @param theTargetResource the target resource to copy identifiers to */ - private void copyIdentifiers(Patient theSourceResource, Patient theTargetResource) { + private void copyIdentifiersAndMarkOld(Patient theSourceResource, Patient theTargetResource) { if (theSourceResource.hasIdentifier()) { List sourceIdentifiers = theSourceResource.getIdentifier(); List targetIdentifiers = theTargetResource.getIdentifier(); for (Identifier sourceIdentifier : sourceIdentifiers) { if (!containsIdentifier(targetIdentifiers, sourceIdentifier)) { - theTargetResource.addIdentifier(sourceIdentifier); + Identifier copyOfSrcIdentifier = sourceIdentifier.copy(); + copyOfSrcIdentifier.setUse(Identifier.IdentifierUse.OLD); + theTargetResource.addIdentifier(copyOfSrcIdentifier); } } } diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java index 6d2efba1853a..eb22bc024bbc 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java @@ -104,11 +104,12 @@ void testMerge_WithoutResultResource_Success() { Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); //the identifiers should be copied from the source to the target, without creating duplicates on the target - sourcePatient.addIdentifier(new Identifier().setSystem("sysA").setValue("val1")); - sourcePatient.addIdentifier(new Identifier().setSystem("sysB").setValue("val2")); - sourcePatient.addIdentifier(new Identifier().setSystem("sysC").setValue("val3")); + sourcePatient.addIdentifier(new Identifier().setSystem("sysSource").setValue("valS1")); + sourcePatient.addIdentifier(new Identifier().setSystem("sysSource").setValue("valS2")); + sourcePatient.addIdentifier(new Identifier().setSystem("sysCommon").setValue("valCommon")); Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); - targetPatient.addIdentifier(new Identifier().setSystem("sysC").setValue("val3")); + targetPatient.addIdentifier(new Identifier().setSystem("sysCommon").setValue("valCommon")); + targetPatient.addIdentifier(new Identifier().setSystem("sysTarget").setValue("valT1")); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); setupDaoMockForSuccessfulSourcePatientUpdate(sourcePatient, new Patient()); @@ -123,10 +124,12 @@ void testMerge_WithoutResultResource_Success() { // Then verifySuccessfulOutcome(mergeOutcome, patientReturnedFromDaoAfterTargetUpdate); verifyUpdatedSourcePatient(); + // the identifiers copied over from the source should be marked as OLD List expectedIdentifiers = List.of( - new Identifier().setSystem("sysC").setValue("val3"), - new Identifier().setSystem("sysA").setValue("val1"), - new Identifier().setSystem("sysB").setValue("val2")); + new Identifier().setSystem("sysCommon").setValue("valCommon"), + new Identifier().setSystem("sysTarget").setValue("valT1"), + new Identifier().setSystem("sysSource").setValue("valS1").setUse(Identifier.IdentifierUse.OLD), + new Identifier().setSystem("sysSource").setValue("valS2").setUse(Identifier.IdentifierUse.OLD)); verifyUpdatedTargetPatient(true, expectedIdentifiers); verifyNoMoreInteractions(myDaoMock); } @@ -169,6 +172,9 @@ void testMerge_WithResultResource_Success() { resultPatient.addLink().setType(Patient.LinkType.REPLACES).setOther(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setResultResource(resultPatient); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + //when result resource exists, the identifiers should not be copied. so we don't expect this identifier when + //target is updated + sourcePatient.addIdentifier(new Identifier().setSystem("sysSource").setValue("valS1")); Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); setupDaoMockForSuccessfulRead(sourcePatient); From d3ad9f91e6e738589ef0b0c618a2afdfbbba9e50 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Wed, 11 Dec 2024 15:36:55 -0500 Subject: [PATCH 076/148] copyright headers --- .../uhn/fhir/util/StopLimitAccumulator.java | 19 +++++++++++++++++++ .../ReplaceReferencePatchOutcomeJson.java | 19 +++++++++++++++++++ .../ReplaceReferenceResultsJson.java | 19 +++++++++++++++++++ .../ReplaceReferenceUpdateStep.java | 19 +++++++++++++++++++ ...ReplaceReferenceUpdateTaskReducerStep.java | 19 +++++++++++++++++++ .../ReplaceReferencesAppCtx.java | 19 +++++++++++++++++++ .../ReplaceReferencesJobParameters.java | 19 +++++++++++++++++++ .../ReplaceReferencesQueryIdsStep.java | 19 +++++++++++++++++++ .../jpa/provider/IReplaceReferencesSvc.java | 2 +- .../ReplaceReferenceRequest.java | 19 +++++++++++++++++++ .../ReplaceReferencesPatchBundleSvc.java | 19 +++++++++++++++++++ 11 files changed, 191 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java index ca280e5d3884..d15d4ec1549d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.util; import jakarta.annotation.Nonnull; diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencePatchOutcomeJson.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencePatchOutcomeJson.java index 8e6b4e867ab7..2c0b63b40473 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencePatchOutcomeJson.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencePatchOutcomeJson.java @@ -1,3 +1,22 @@ +/*- + * #%L + * hapi-fhir-storage-batch2-jobs + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.batch2.jobs.replacereferences; import ca.uhn.fhir.context.FhirContext; diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResultsJson.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResultsJson.java index d3caf23c7c4c..ab8451db9e83 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResultsJson.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceResultsJson.java @@ -1,3 +1,22 @@ +/*- + * #%L + * hapi-fhir-storage-batch2-jobs + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.batch2.jobs.replacereferences; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java index 7bba901a5398..e146967e37bb 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java @@ -1,3 +1,22 @@ +/*- + * #%L + * hapi-fhir-storage-batch2-jobs + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.batch2.jobs.replacereferences; import ca.uhn.fhir.batch2.api.IJobDataSink; diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java index f1d3176195e3..d75b4fdec3c2 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java @@ -1,3 +1,22 @@ +/*- + * #%L + * hapi-fhir-storage-batch2-jobs + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.batch2.jobs.replacereferences; import ca.uhn.fhir.batch2.api.ChunkExecutionDetails; diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java index d2432e9ee29b..51ca58276e46 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java @@ -1,3 +1,22 @@ +/*- + * #%L + * hapi-fhir-storage-batch2-jobs + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.batch2.jobs.replacereferences; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdListWorkChunkJson; diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java index 749d64e2c8a8..5d8419fc1b7f 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java @@ -1,3 +1,22 @@ +/*- + * #%L + * hapi-fhir-storage-batch2-jobs + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.batch2.jobs.replacereferences; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java index 4246041fce71..2e551cb518b4 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java @@ -1,3 +1,22 @@ +/*- + * #%L + * hapi-fhir-storage-batch2-jobs + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.batch2.jobs.replacereferences; import ca.uhn.fhir.batch2.api.IJobDataSink; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java index 603f9627dbcc..43a99fb351dc 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java @@ -1,6 +1,6 @@ /*- * #%L - * HAPI FHIR JPA Server + * HAPI FHIR Storage api * %% * Copyright (C) 2014 - 2024 Smile CDR, Inc. * %% diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java index 4d97ea0dfc46..0543ec6e7a93 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR Storage api + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.replacereferences; import ca.uhn.fhir.i18n.Msg; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java index be20df4e28bb..dd5e30e3889b 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR Storage api + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.replacereferences; import ca.uhn.fhir.context.FhirContext; From d2ba602c6d5ea5e50ec3d222836e3aa1ae14f4e8 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Thu, 12 Dec 2024 14:49:56 -0500 Subject: [PATCH 077/148] runMaintenancePass before checking merge task status, and renamed test method for merge --- .../uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index b033275c9961..bb2e61ceb903 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -3,6 +3,7 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper; +import ca.uhn.fhir.jpa.test.Batch2JobHelper; import ca.uhn.fhir.parser.StrictErrorHandler; import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInput; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; @@ -26,6 +27,7 @@ import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; import java.util.List; import java.util.concurrent.TimeUnit; @@ -52,6 +54,9 @@ public class PatientMergeR4Test extends BaseResourceProviderR4Test { @RegisterExtension MyExceptionHandler ourExceptionHandler = new MyExceptionHandler(); + + @Autowired + Batch2JobHelper myBatch2JobHelper; ReplaceReferencesTestHelper myTestHelper; @@ -96,7 +101,7 @@ public void before() throws Exception { "false, true, false, true", "false, false, false, true", }) - public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPatient, boolean withPreview, boolean isAsync) { + public void testMerge(boolean withDelete, boolean withInputResultPatient, boolean withPreview, boolean isAsync) { // setup ReplaceReferencesTestHelper.PatientMergeInputParameters inParams = new ReplaceReferencesTestHelper.PatientMergeInputParameters(); @@ -134,7 +139,10 @@ public void testMergeWithoutResult(boolean withDelete, boolean withInputResultPa Task task = (Task) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource(); assertNull(task.getIdElement().getVersionIdPart()); ourLog.info("Got task {}", task.getId()); - await().until(() -> myTestHelper.taskCompleted(task.getIdElement())); + await().until(() -> { + myBatch2JobHelper.runMaintenancePass(); + return myTestHelper.taskCompleted(task.getIdElement()); + }); Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); From 22ec2938b4b21a79c887f05dd7f238317da67692 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 13 Dec 2024 09:57:01 -0500 Subject: [PATCH 078/148] fix test --- .../java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java | 6 +++--- .../ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java | 3 +-- .../uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java | 5 ++--- .../jpa/replacereferences/ReplaceReferencesBatchTest.java | 4 +++- .../jpa/replacereferences/ReplaceReferencesTestHelper.java | 5 +++-- 5 files changed, 12 insertions(+), 11 deletions(-) rename {hapi-fhir-jpaserver-test-r4/src/test => hapi-fhir-jpaserver-test-utilities/src/main}/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java (98%) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java index f81d7e8e14df..51f0d2f4d24a 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java @@ -4,7 +4,6 @@ import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; import ca.uhn.fhir.batch2.jobs.merge.MergeJobParameters; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceResultsJson; -import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; import ca.uhn.fhir.interceptor.model.RequestPartitionId; @@ -23,8 +22,9 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + import static ca.uhn.fhir.batch2.jobs.merge.MergeAppCtx.JOB_MERGE; -import static ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx.JOB_REPLACE_REFERENCES; import static org.junit.jupiter.api.Assertions.assertEquals; public class MergeBatchTest extends BaseJpaR4Test { @@ -72,7 +72,7 @@ public void testHappyPath() { assertEquals(taskId.getIdPart(), resultTaskId.getIdPart()); Bundle patchResultBundle = myTestHelper.validateCompletedTask(taskId); - myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES); + myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of("Observation", "Encounter", "CarePlan")); // FIXME ED validate other steps performed by final run merge step diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index bb2e61ceb903..6b2d46dbef87 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -30,7 +30,6 @@ import org.springframework.beans.factory.annotation.Autowired; import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; @@ -166,7 +165,7 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea Reference outputRef = (Reference) taskOutput.getValue(); Bundle patchResultBundle = (Bundle) outputRef.getResource(); assertTrue(containedBundle.equalsDeep(patchResultBundle)); - myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES); + myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of("Observation", "Encounter", "CarePlan")); } else { // Synchronous case // Assert outcome OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index 62a452686bcc..3ae985c654fc 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -23,7 +23,6 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -64,7 +63,7 @@ void testReplaceReferences(boolean isAsync) throws IOException { } // validate - myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES); + myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of("Observation", "Encounter", "CarePlan")); // Check that the linked resources were updated @@ -128,7 +127,7 @@ void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { // validate entriesLeft -= ReplaceReferencesTestHelper.SMALL_BATCH_SIZE; int expectedNumberOfEntries = Math.min(entriesLeft, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); - myTestHelper.validatePatchResultBundle(patchResultBundle, expectedNumberOfEntries); + myTestHelper.validatePatchResultBundle(patchResultBundle, expectedNumberOfEntries, List.of("Observation", "Encounter", "CarePlan")); } // Check that the linked resources were updated diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java index cbf34adb4a62..684789952c92 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java @@ -21,6 +21,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + import static ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx.JOB_REPLACE_REFERENCES; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -68,7 +70,7 @@ public void testHappyPath() { assertEquals(taskId.getIdPart(), resultTaskId.getIdPart()); Bundle patchResultBundle = myTestHelper.validateCompletedTask(taskId); - myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES); + myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of("Observation", "Encounter", "CarePlan")); myTestHelper.assertAllReferencesUpdated(); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java similarity index 98% rename from hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java rename to hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index db027eda5de0..81ea5856bc15 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -320,8 +320,9 @@ public Parameters asParametersResource() { } } - public void validatePatchResultBundle(Bundle patchResultBundle, int theTotalExpectedPatches) { - Pattern expectedPatchIssuePattern = Pattern.compile("Successfully patched resource \"(Observation|Encounter|CarePlan)/\\d+/_history/\\d+\"."); + public static void validatePatchResultBundle(Bundle patchResultBundle, int theTotalExpectedPatches, List theExpectedResourceTypes) { + String resourceMatchString = "(" + String.join("|", theExpectedResourceTypes) + ")"; + Pattern expectedPatchIssuePattern = Pattern.compile("Successfully patched resource \"" + resourceMatchString + "/\\d+/_history/\\d+\"."); assertThat(patchResultBundle.getEntry()).hasSize(theTotalExpectedPatches) .allSatisfy(entry -> assertThat(entry.getResponse().getOutcome()) From b5894d108bd09758b2643cb3f18cee9d8babcd7a Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 13 Dec 2024 10:02:04 -0500 Subject: [PATCH 079/148] fix test --- .../ReplaceReferencesTestHelper.java | 108 ++++++++++-------- 1 file changed, 61 insertions(+), 47 deletions(-) diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index 81ea5856bc15..5c2a92a1f93a 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -51,11 +51,16 @@ public class ReplaceReferencesTestHelper { private static final Logger ourLog = LoggerFactory.getLogger(ReplaceReferencesTestHelper.class); - static final Identifier pat1IdentifierA = new Identifier().setSystem("SYS1A").setValue("VAL1A"); - static final Identifier pat1IdentifierB = new Identifier().setSystem("SYS1B").setValue("VAL1B"); - static final Identifier pat2IdentifierA = new Identifier().setSystem("SYS2A").setValue("VAL2A"); - static final Identifier pat2IdentifierB = new Identifier().setSystem("SYS2B").setValue("VAL2B"); - static final Identifier patBothIdentifierC = new Identifier().setSystem("SYSC").setValue("VALC"); + static final Identifier pat1IdentifierA = + new Identifier().setSystem("SYS1A").setValue("VAL1A"); + static final Identifier pat1IdentifierB = + new Identifier().setSystem("SYS1B").setValue("VAL1B"); + static final Identifier pat2IdentifierA = + new Identifier().setSystem("SYS2A").setValue("VAL2A"); + static final Identifier pat2IdentifierB = + new Identifier().setSystem("SYS2B").setValue("VAL2B"); + static final Identifier patBothIdentifierC = + new Identifier().setSystem("SYSC").setValue("VALC"); public static final int TOTAL_EXPECTED_PATCHES = 23; public static final int SMALL_BATCH_SIZE = 5; public static final int EXPECTED_SMALL_BATCHES = (TOTAL_EXPECTED_PATCHES + SMALL_BATCH_SIZE - 1) / SMALL_BATCH_SIZE; @@ -108,7 +113,7 @@ public void beforeEach() throws Exception { patient2.addIdentifier(pat2IdentifierB); patient2.addIdentifier(patBothIdentifierC); patient2.getManagingOrganization().setReferenceElement(myOrgId); - myTargetPatientId = myPatientDao.create(patient2, mySrd).getId().toUnqualifiedVersionless(); + myTargetPatientId = myPatientDao.create(patient2, mySrd).getId().toUnqualifiedVersionless(); Encounter enc1 = new Encounter(); enc1.setStatus(Encounter.EncounterStatus.CANCELLED); @@ -145,7 +150,6 @@ public void beforeEach() throws Exception { myResultPatient = new Patient(); myResultPatient.setIdElement((IdType) myTargetPatientId); myResultPatient.addIdentifier(pat1IdentifierA); - } public void setSourceAndTarget(PatientMergeInputParameters inParams) { @@ -154,8 +158,7 @@ public void setSourceAndTarget(PatientMergeInputParameters inParams) { } public void setResultPatient(PatientMergeInputParameters theInParams, boolean theWithDelete) { - if (!theWithDelete) - { + if (!theWithDelete) { // add the link only if we are not deleting the source Patient.PatientLinkComponent link = myResultPatient.addLink(); link.setOther(new Reference(mySourcePatientId)); @@ -176,14 +179,15 @@ private Set getTargetEverythingResourceIds() { PatientEverythingParameters everythingParams = new PatientEverythingParameters(); everythingParams.setCount(new IntegerType(100)); - IBundleProvider bundleProvider = myPatientDao.patientInstanceEverything(null, mySrd, everythingParams, myTargetPatientId); + IBundleProvider bundleProvider = + myPatientDao.patientInstanceEverything(null, mySrd, everythingParams, myTargetPatientId); assertNull(bundleProvider.getNextPageId()); return bundleProvider.getAllResources().stream() - .map(IBaseResource::getIdElement) - .map(IIdType::toUnqualifiedVersionless) - .collect(Collectors.toSet()); + .map(IBaseResource::getIdElement) + .map(IIdType::toUnqualifiedVersionless) + .collect(Collectors.toSet()); } public Boolean taskCompleted(IdType theTaskId) { @@ -196,23 +200,29 @@ public Parameters callReplaceReferences(IGenericClient theFhirClient, boolean th return callReplaceReferencesWithBatchSize(theFhirClient, theIsAsync, null); } - public Parameters callReplaceReferencesWithBatchSize(IGenericClient theFhirClient, boolean theIsAsync, Integer theBatchSize) { - IOperationUntypedWithInputAndPartialOutput request = theFhirClient.operation() - .onServer() - .named(OPERATION_REPLACE_REFERENCES) - .withParameter(Parameters.class, ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID, new StringType(mySourcePatientId.getValue())) - .andParameter(ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID, new StringType(myTargetPatientId.getValue())); + public Parameters callReplaceReferencesWithBatchSize( + IGenericClient theFhirClient, boolean theIsAsync, Integer theBatchSize) { + IOperationUntypedWithInputAndPartialOutput request = theFhirClient + .operation() + .onServer() + .named(OPERATION_REPLACE_REFERENCES) + .withParameter( + Parameters.class, + ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID, + new StringType(mySourcePatientId.getValue())) + .andParameter( + ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID, + new StringType(myTargetPatientId.getValue())); if (theBatchSize != null) { - request.andParameter(ProviderConstants.OPERATION_REPLACE_REFERENCES_BATCH_SIZE, new IntegerType(theBatchSize)); + request.andParameter( + ProviderConstants.OPERATION_REPLACE_REFERENCES_BATCH_SIZE, new IntegerType(theBatchSize)); } if (theIsAsync) { request.withAdditionalHeader(HEADER_PREFER, HEADER_PREFER_RESPOND_ASYNC); } - return request - .returnResourceType(Parameters.class) - .execute(); + return request.returnResourceType(Parameters.class).execute(); } public void assertAllReferencesUpdated() { @@ -252,7 +262,8 @@ public void assertNothingChanged() { assertThat(actual).contains(myTargetEnc1); } - public PatientMergeInputParameters buildMultipleTargetMatchParameters(boolean theWithDelete, boolean theWithInputResultPatient, boolean theWithPreview) { + public PatientMergeInputParameters buildMultipleTargetMatchParameters( + boolean theWithDelete, boolean theWithInputResultPatient, boolean theWithPreview) { PatientMergeInputParameters inParams = new PatientMergeInputParameters(); inParams.sourcePatient = new Reference().setReferenceElement(mySourcePatientId); inParams.targetPatientIdentifier = patBothIdentifierC; @@ -266,7 +277,8 @@ public PatientMergeInputParameters buildMultipleTargetMatchParameters(boolean th return inParams; } - public PatientMergeInputParameters buildMultipleSourceMatchParameters(boolean theWithDelete, boolean theWithInputResultPatient, boolean theWithPreview) { + public PatientMergeInputParameters buildMultipleSourceMatchParameters( + boolean theWithDelete, boolean theWithInputResultPatient, boolean theWithPreview) { PatientMergeInputParameters inParams = new PatientMergeInputParameters(); inParams.sourcePatientIdentifier = patBothIdentifierC; inParams.targetPatient = new Reference().setReferenceElement(mySourcePatientId); @@ -320,27 +332,32 @@ public Parameters asParametersResource() { } } - public static void validatePatchResultBundle(Bundle patchResultBundle, int theTotalExpectedPatches, List theExpectedResourceTypes) { + public static void validatePatchResultBundle( + Bundle patchResultBundle, int theTotalExpectedPatches, List theExpectedResourceTypes) { String resourceMatchString = "(" + String.join("|", theExpectedResourceTypes) + ")"; - Pattern expectedPatchIssuePattern = Pattern.compile("Successfully patched resource \"" + resourceMatchString + "/\\d+/_history/\\d+\"."); - assertThat(patchResultBundle.getEntry()).hasSize(theTotalExpectedPatches) - .allSatisfy(entry -> - assertThat(entry.getResponse().getOutcome()) - .isInstanceOf(OperationOutcome.class) - .extracting(OperationOutcome.class::cast) - .extracting(OperationOutcome::getIssue) - .satisfies(issues -> - assertThat(issues).hasSize(1) - .element(0) - .extracting(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics) - .satisfies(diagnostics -> assertThat(diagnostics).matches(expectedPatchIssuePattern)))); + Pattern expectedPatchIssuePattern = + Pattern.compile("Successfully patched resource \"" + resourceMatchString + "/\\d+/_history/\\d+\"."); + assertThat(patchResultBundle.getEntry()) + .hasSize(theTotalExpectedPatches) + .allSatisfy(entry -> assertThat(entry.getResponse().getOutcome()) + .isInstanceOf(OperationOutcome.class) + .extracting(OperationOutcome.class::cast) + .extracting(OperationOutcome::getIssue) + .satisfies(issues -> assertThat(issues) + .hasSize(1) + .element(0) + .extracting(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics) + .satisfies( + diagnostics -> assertThat(diagnostics).matches(expectedPatchIssuePattern)))); } public Bundle validateCompletedTask(IIdType theTaskId) { Bundle patchResultBundle; Task taskWithOutput = myTaskDao.read(theTaskId, mySrd); assertThat(taskWithOutput.getStatus()).isEqualTo(Task.TaskStatus.COMPLETED); - ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); + ourLog.info( + "Complete Task: {}", + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); @@ -350,20 +367,17 @@ public Bundle validateCompletedTask(IIdType theTaskId) { assertEquals("Bundle", taskType.getCode()); List containedResources = taskWithOutput.getContained(); - assertThat(containedResources) - .hasSize(1) - .element(0) - .isInstanceOf(Bundle.class); + assertThat(containedResources).hasSize(1).element(0).isInstanceOf(Bundle.class); Bundle containedBundle = (Bundle) containedResources.get(0); Reference outputRef = (Reference) taskOutput.getValue(); patchResultBundle = (Bundle) outputRef.getResource(); -// ourLog.info("containedBundle: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(containedBundle)); -// ourLog.info("patchResultBundle: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(patchResultBundle)); + // ourLog.info("containedBundle: {}", + // myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(containedBundle)); + // ourLog.info("patchResultBundle: {}", + // myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(patchResultBundle)); assertTrue(containedBundle.equalsDeep(patchResultBundle)); return patchResultBundle; } - - } From 49917ec3f4135e97bfdfe112372eac6dca658674 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 13 Dec 2024 10:08:05 -0500 Subject: [PATCH 080/148] default --- .../src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java index 62feeb8942f7..f1b425101575 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java @@ -80,5 +80,7 @@ default IResourcePidStream fetchResourceIdStream( theStart, theEnd, 20000 /* ResourceIdListStep.DEFAULT_PAGE_SIZE */, theTargetPartitionId, theUrl)); } - Stream streamSourceIdsThatReferenceTargetId(IIdType theTargetId); + default Stream streamSourceIdsThatReferenceTargetId(IIdType theTargetId) { + throw new UnsupportedOperationException(); + } } From 33fd9ff38c68506e5967209445e539ba9c60024a Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Fri, 13 Dec 2024 17:36:49 -0500 Subject: [PATCH 081/148] invoke async merge job in merge service --- .../BaseJpaResourceProviderPatient.java | 32 +- .../jpa/provider/IReplaceReferencesSvc.java | 2 +- .../provider/ReplaceReferencesSvcImpl.java | 55 ++- .../merge/MergeOperationInputParameters.java | 4 +- .../merge/MergeOperationOutcome.java | 4 +- .../PatientMergeOperationInputParameters.java | 4 +- .../provider}/merge/ResourceMergeService.java | 323 ++++++++++-------- .../merge/ResourceMergeServiceTest.java | 118 ++++--- .../jpa/provider/r4/PatientMergeR4Test.java | 79 +++-- .../ReplaceReferencesTestHelper.java | 4 + .../fhir/batch2/jobs/merge/MergeAppCtx.java | 25 +- .../fhir/batch2/jobs/merge/MergeHelper.java | 171 ++++++++++ .../batch2/jobs/merge/MergeJobParameters.java | 42 ++- .../merge/MergeUpdateTaskReducerStep.java | 53 ++- ...ReplaceReferenceUpdateTaskReducerStep.java | 4 +- .../ReplaceReferencesJobParameters.java | 15 +- .../BatchJobParametersWithTaskId.java | 38 +++ .../uhn/fhir/batch2/util/Batch2TaskUtils.java | 58 ++++ .../ReplaceReferenceRequest.java | 10 + 19 files changed, 764 insertions(+), 277 deletions(-) rename {hapi-fhir-storage => hapi-fhir-jpaserver-base}/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java (97%) rename {hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao => hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider}/merge/MergeOperationInputParameters.java (98%) rename {hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao => hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider}/merge/MergeOperationOutcome.java (96%) rename {hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao => hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider}/merge/PatientMergeOperationInputParameters.java (96%) rename {hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao => hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider}/merge/ResourceMergeService.java (76%) rename {hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao => hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider}/merge/ResourceMergeServiceTest.java (94%) create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeHelper.java create mode 100644 hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/parameters/BatchJobParametersWithTaskId.java create mode 100644 hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskUtils.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index df7649df194c..e8f399e2fbae 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -19,16 +19,19 @@ */ package ca.uhn.fhir.jpa.provider; +import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +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.dao.merge.MergeOperationInputParameters; -import ca.uhn.fhir.jpa.dao.merge.MergeOperationOutcome; -import ca.uhn.fhir.jpa.dao.merge.PatientMergeOperationInputParameters; -import ca.uhn.fhir.jpa.dao.merge.ResourceMergeService; import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; +import ca.uhn.fhir.jpa.provider.merge.MergeOperationInputParameters; +import ca.uhn.fhir.jpa.provider.merge.MergeOperationOutcome; +import ca.uhn.fhir.jpa.provider.merge.PatientMergeOperationInputParameters; +import ca.uhn.fhir.jpa.provider.merge.ResourceMergeService; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.valueset.BundleTypeEnum; @@ -60,6 +63,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Task; import org.springframework.beans.factory.annotation.Autowired; import java.util.Arrays; @@ -72,6 +76,9 @@ public abstract class BaseJpaResourceProviderPatient extends BaseJpaResourceProvider { + @Autowired + private DaoRegistry myDaoRegistry; + @Autowired private IReplaceReferencesSvc myReplaceReferencesSvc; @@ -81,6 +88,9 @@ public abstract class BaseJpaResourceProviderPatient ex @Autowired private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; + @Autowired + private IJobCoordinator myJobCoordinator; + /** * Patient/123/$everything */ @@ -316,11 +326,17 @@ public IBaseParameters patientMerge( theResultPatient, batchSize); - IFhirResourceDaoPatient dao = (IFhirResourceDaoPatient) getDao(); + IFhirResourceDaoPatient patientDao = (IFhirResourceDaoPatient) getDao(); + IFhirResourceDao taskDao = myDaoRegistry.getResourceDao(Task.class); ResourceMergeService resourceMergeService = new ResourceMergeService( - dao, myReplaceReferencesSvc, myHapiTransactionService, myRequestPartitionHelperSvc); - - FhirContext fhirContext = dao.getContext(); + patientDao, + taskDao, + myReplaceReferencesSvc, + myHapiTransactionService, + myRequestPartitionHelperSvc, + myJobCoordinator); + + FhirContext fhirContext = patientDao.getContext(); MergeOperationOutcome mergeOutcome = resourceMergeService.merge(mergeOperationParameters, theRequestDetails); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java similarity index 97% rename from hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java index 43a99fb351dc..603f9627dbcc 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java @@ -1,6 +1,6 @@ /*- * #%L - * HAPI FHIR Storage api + * HAPI FHIR JPA Server * %% * Copyright (C) 2014 - 2024 Smile CDR, Inc. * %% diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 760a82f0270a..6ae8e0ba52c7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -21,10 +21,8 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; -import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; +import ca.uhn.fhir.batch2.util.Batch2TaskUtils; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.model.primitive.IdDt; @@ -41,10 +39,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.stream.Collectors; import java.util.stream.Stream; import static ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx.JOB_REPLACE_REFERENCES; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.HAPI_BATCH_JOB_ID_SYSTEM; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; @@ -77,6 +76,8 @@ public IBaseParameters replaceReferences( if (theRequestDetails.isPreferAsync()) { return replaceReferencesPreferAsync(theReplaceReferenceRequest, theRequestDetails); + } else if (theReplaceReferenceRequest.isForceSync()) { + return replaceReferencesForceSync(theReplaceReferenceRequest, theRequestDetails); } else { return replaceReferencesPreferSync(theReplaceReferenceRequest, theRequestDetails); } @@ -92,19 +93,13 @@ public Integer countResourcesReferencingResource(IIdType theResourceId, RequestD private IBaseParameters replaceReferencesPreferAsync( ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { - Task task = new Task(); - task.setStatus(Task.TaskStatus.INPROGRESS); - IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(Task.class); - resourceDao.create(task, theRequestDetails); - ReplaceReferencesJobParameters jobParams = new ReplaceReferencesJobParameters(theReplaceReferenceRequest); - jobParams.setTaskId(task.getIdElement().toUnqualifiedVersionless()); - - JobInstanceStartRequest request = new JobInstanceStartRequest(JOB_REPLACE_REFERENCES, jobParams); - Batch2JobStartResponse jobStartResponse = myJobCoordinator.startInstance(theRequestDetails, request); - - task.addIdentifier().setSystem(HAPI_BATCH_JOB_ID_SYSTEM).setValue(jobStartResponse.getInstanceId()); - resourceDao.update(task, theRequestDetails); + Task task = Batch2TaskUtils.startJobAndCreateAssociatedTask( + myDaoRegistry.getResourceDao(Task.class), + theRequestDetails, + myJobCoordinator, + JOB_REPLACE_REFERENCES, + new ReplaceReferencesJobParameters(theReplaceReferenceRequest)); Parameters retval = new Parameters(); task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); @@ -142,6 +137,34 @@ private IBaseParameters replaceReferencesPreferSync( return retval; } + /** + * Perform the operation synchronously. This should be only called if the number of resources to be + * updated is predetermined before calling, and it is small enough to handle synchronously. + */ + @Nonnull + private IBaseParameters replaceReferencesForceSync( + ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + + // TODO KHS get partition from request + List allIds = myHapiTransactionService + .withRequest(theRequestDetails) + .execute(() -> { + Stream idStream = myResourceLinkDao.streamSourceIdsForTargetFhirId( + theReplaceReferenceRequest.sourceId.getResourceType(), + theReplaceReferenceRequest.sourceId.getIdPart()); + return idStream.collect(Collectors.toList()); + }); + + Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources( + theReplaceReferenceRequest, allIds, theRequestDetails); + + Parameters retval = new Parameters(); + retval.addParameter() + .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME) + .setResource(result); + return retval; + } + private @Nonnull StopLimitAccumulator getAllPidsWithLimit( ReplaceReferenceRequest theReplaceReferenceRequest) { diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationInputParameters.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParameters.java similarity index 98% rename from hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationInputParameters.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParameters.java index 1a8adaa4d2d1..cbc3915de079 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationInputParameters.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParameters.java @@ -1,6 +1,6 @@ /*- * #%L - * HAPI FHIR Storage api + * HAPI FHIR JPA Server * %% * Copyright (C) 2014 - 2024 Smile CDR, Inc. * %% @@ -17,7 +17,7 @@ * limitations under the License. * #L% */ -package ca.uhn.fhir.jpa.dao.merge; +package ca.uhn.fhir.jpa.provider.merge; import ca.uhn.fhir.util.CanonicalIdentifier; import org.hl7.fhir.instance.model.api.IBaseReference; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationOutcome.java similarity index 96% rename from hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationOutcome.java index a240521a9bfb..a30d929c87a4 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/MergeOperationOutcome.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationOutcome.java @@ -1,6 +1,6 @@ /*- * #%L - * HAPI FHIR Storage api + * HAPI FHIR JPA Server * %% * Copyright (C) 2014 - 2024 Smile CDR, Inc. * %% @@ -17,7 +17,7 @@ * limitations under the License. * #L% */ -package ca.uhn.fhir.jpa.dao.merge; +package ca.uhn.fhir.jpa.provider.merge; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationInputParameters.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java similarity index 96% rename from hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationInputParameters.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java index e2a9b67f309e..c480d1034677 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/PatientMergeOperationInputParameters.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java @@ -1,6 +1,6 @@ /*- * #%L - * HAPI FHIR Storage api + * HAPI FHIR JPA Server * %% * Copyright (C) 2014 - 2024 Smile CDR, Inc. * %% @@ -17,7 +17,7 @@ * limitations under the License. * #L% */ -package ca.uhn.fhir.jpa.dao.merge; +package ca.uhn.fhir.jpa.provider.merge; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_RESULT_PATIENT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java similarity index 76% rename from hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index eacc20fc3aa2..077745dc511e 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -1,6 +1,6 @@ /*- * #%L - * HAPI FHIR Storage api + * HAPI FHIR JPA Server * %% * Copyright (C) 2014 - 2024 Smile CDR, Inc. * %% @@ -17,13 +17,18 @@ * limitations under the License. * #L% */ -package ca.uhn.fhir.jpa.dao.merge; +package ca.uhn.fhir.jpa.provider.merge; +import ca.uhn.fhir.batch2.api.IJobCoordinator; +import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; +import ca.uhn.fhir.batch2.jobs.merge.MergeHelper; +import ca.uhn.fhir.batch2.jobs.merge.MergeJobParameters; +import ca.uhn.fhir.batch2.util.Batch2TaskUtils; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; -import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; @@ -37,7 +42,6 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.CanonicalIdentifier; import ca.uhn.fhir.util.OperationOutcomeUtil; -import jakarta.annotation.Nullable; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseReference; @@ -45,9 +49,9 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; -import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,31 +59,40 @@ import java.util.List; import java.util.stream.Collectors; +import static ca.uhn.fhir.batch2.jobs.merge.MergeAppCtx.JOB_MERGE; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_200_OK; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_400_BAD_REQUEST; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_500_INTERNAL_ERROR; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; public class ResourceMergeService { private static final Logger ourLog = LoggerFactory.getLogger(ResourceMergeService.class); - private final IFhirResourceDaoPatient myDao; + private final IFhirResourceDaoPatient myPatientDao; private final IReplaceReferencesSvc myReplaceReferencesSvc; private final IHapiTransactionService myHapiTransactionService; private final FhirContext myFhirContext; private final IRequestPartitionHelperSvc myRequestPartitionHelperSvc; + private final IFhirResourceDao myTaskDao; + private final IJobCoordinator myJobCoordinator; + private final MergeHelper myMergeHelper; public ResourceMergeService( IFhirResourceDaoPatient thePatientDao, + IFhirResourceDao theTaskDao, IReplaceReferencesSvc theReplaceReferencesSvc, IHapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc) { - myDao = thePatientDao; + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator) { + + myPatientDao = thePatientDao; + myTaskDao = theTaskDao; myReplaceReferencesSvc = theReplaceReferencesSvc; myRequestPartitionHelperSvc = theRequestPartitionHelperSvc; - myFhirContext = myDao.getContext(); + myJobCoordinator = theJobCoordinator; + myFhirContext = myPatientDao.getContext(); myHapiTransactionService = theHapiTransactionService; + myMergeHelper = new MergeHelper(myPatientDao); } /** @@ -92,10 +105,6 @@ public ResourceMergeService( public MergeOperationOutcome merge( MergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { - // FIXME ED update to work like ReplaceReferencesSvcImpl.replaceReferences() - // in replaceReferencesPreferSync, still need to fallback to async if count exceeds batchSize, - // but don't need to stream resources, can just use count method for that. - MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); mergeOutcome.setOperationOutcome(operationOutcome); @@ -121,11 +130,39 @@ private void validateAndMerge( RequestDetails theRequestDetails, MergeOperationOutcome theMergeOutcome) { + ValidationResult validationResult = validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); + + if (!validationResult.isValid) { + return; + } + + Patient sourceResource = validationResult.sourceResource; + Patient targetResource = validationResult.targetResource; + + if (theMergeOperationParameters.getPreview()) { + handlePreview( + sourceResource, targetResource, theMergeOperationParameters, theRequestDetails, theMergeOutcome); + return; + } + + doMerge(theMergeOperationParameters, sourceResource, targetResource, theRequestDetails, theMergeOutcome); + + String detailsText = "Merge operation completed successfully."; + addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); + } + + private ValidationResult validate( + MergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { + + ValidationResult validationResult = new ValidationResult(); IBaseOperationOutcome operationOutcome = theMergeOutcome.getOperationOutcome(); if (!validateMergeOperationParameters(theMergeOperationParameters, operationOutcome)) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); - return; + validationResult.isValid = false; + return validationResult; } // cast to Patient, since we only support merging Patient resources for now @@ -134,99 +171,159 @@ private void validateAndMerge( if (sourceResource == null) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); - return; + validationResult.isValid = false; + return validationResult; } + validationResult.sourceResource = sourceResource; + // cast to Patient, since we only support merging Patient resources for now Patient targetResource = (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (targetResource == null) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); - return; + validationResult.isValid = false; + return validationResult; } + validationResult.targetResource = targetResource; + if (!validateSourceAndTargetAreSuitableForMerge(sourceResource, targetResource, operationOutcome)) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); - return; + validationResult.isValid = false; + return validationResult; } if (!validateResultResourceIfExists( theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); - return; + return validationResult; } - if (theMergeOperationParameters.getPreview()) { - Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( - sourceResource.getIdElement(), theRequestDetails); - - // in preview mode, we should also return how the target would look like - Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); - Patient targetPatientAsIfUpdated = prepareTargetPatientForUpdate( - targetResource, sourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); - theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); - - // adding +2 because the source and the target resources themselved would be updated as well - String diagnosticsMsg = String.format("Merge would update %d resources", referencingResourceCount + 2); - String detailsText = "Preview only merge operation - no issues detected"; - addInfoToOperationOutcome(operationOutcome, diagnosticsMsg, detailsText); - return; - } + validationResult.isValid = true; + return validationResult; + } - mergeInTransaction( - theMergeOperationParameters, sourceResource, targetResource, theRequestDetails, theMergeOutcome); + private void handlePreview( + Patient theSourceResource, + Patient theTargetResource, + MergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { - String detailsText = "Merge operation completed successfully."; - addInfoToOperationOutcome(operationOutcome, null, detailsText); + Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( + theSourceResource.getIdElement(), theRequestDetails); + + // in preview mode, we should also return how the target would look like + Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); + Patient targetPatientAsIfUpdated = myMergeHelper.prepareTargetPatientForUpdate( + theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); + theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); + + // adding +2 because the source and the target resources themselved would be updated as well + String diagnosticsMsg = String.format("Merge would update %d resources", referencingResourceCount + 2); + String detailsText = "Preview only merge operation - no issues detected"; + addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), diagnosticsMsg, detailsText); } - private void mergeInTransaction( + private void doMerge( MergeOperationInputParameters theMergeOperationParameters, Patient theSourceResource, Patient theTargetResource, RequestDetails theRequestDetails, MergeOperationOutcome theMergeOutcome) { - // TODO: cannot do this in transaction yet, because systemDAO.transaction called by replaceReferences complains - // that there is an active transaction already. RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); + if (theRequestDetails.isPreferAsync()) { + // client prefers async processing, do async + doMergeAsync( + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); + } else { + // count the number of refs, if it is larger than batch size then process async, otherwise process sync + Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( + theSourceResource.getIdElement(), theRequestDetails); + if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { + doMergeAsync( + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); + } else { + doMergeSync( + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); + } + } + } + + private void doMergeSync( + MergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId partitionId) { + ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest( theSourceResource.getIdElement(), theTargetResource.getIdElement(), theMergeOperationParameters.getBatchSize(), partitionId); + replaceReferenceRequest.setForceSync(true); - // FIXME ED this will need to change because this calls JOB_REPLACE_REFERENCES when you want to call JOB_MERGE - Parameters replaceRefsOutParams = - (Parameters) myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); + myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); - Parameters.ParametersParameterComponent taskOutParam = - replaceRefsOutParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK); - if (taskOutParam != null) { - theMergeOutcome.setTask(taskOutParam.getResource()); - } + Patient updatedTarget = myMergeHelper.updateMergedResourcesAfterReferencesReplaced( + myHapiTransactionService, + theSourceResource, + theTargetResource, + (Patient) theMergeOperationParameters.getResultResource(), + theMergeOperationParameters.getDeleteSource(), + theRequestDetails); + theMergeOutcome.setUpdatedTargetResource(updatedTarget); + } - myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { - Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); - Patient patientToUpdate = prepareTargetPatientForUpdate( - theTargetResource, - theSourceResource, - theResultResource, - theMergeOperationParameters.getDeleteSource()); - // update the target patient resource after the references are updated - Patient targetPatientAfterUpdate = updateResource(patientToUpdate, theRequestDetails); - theMergeOutcome.setUpdatedTargetResource(targetPatientAfterUpdate); - - if (theMergeOperationParameters.getDeleteSource()) { - deleteResource(theSourceResource, theRequestDetails); - } else { - prepareSourceResourceForUpdate(theSourceResource, theTargetResource); - updateResource(theSourceResource, theRequestDetails); - } - }); + // FIXME ED add unit tests for async case + private void doMergeAsync( + MergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId partitionId) { + + MergeJobParameters mergeJobParameters = new MergeJobParameters(); + if (theMergeOperationParameters.getResultResource() != null) { + mergeJobParameters.setResultResource(myFhirContext + .newJsonParser() + .encodeResourceToString(theMergeOperationParameters.getResultResource())); + } + mergeJobParameters.setDeleteSource(theMergeOperationParameters.getDeleteSource()); + mergeJobParameters.setBatchSize(theMergeOperationParameters.getBatchSize()); + mergeJobParameters.setSourceId(new FhirIdJson(theSourceResource.getIdElement())); + mergeJobParameters.setTargetId(new FhirIdJson(theTargetResource.getIdElement())); + mergeJobParameters.setPartitionId(partitionId); + + Task task = Batch2TaskUtils.startJobAndCreateAssociatedTask( + myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); + + task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); + task.getMeta().setVersionId(null); + theMergeOutcome.setTask(task); } private boolean validateResultResourceIfExists( @@ -398,86 +495,6 @@ private boolean validateSourceAndTargetAreSuitableForMerge( return true; } - private void prepareSourceResourceForUpdate(Patient theSourceResource, Patient theTargetResource) { - theSourceResource.setActive(false); - theSourceResource - .addLink() - .setType(Patient.LinkType.REPLACEDBY) - .setOther(new Reference(theTargetResource.getIdElement().toVersionless())); - } - - private Patient prepareTargetPatientForUpdate( - Patient theTargetResource, - Patient theSourceResource, - @Nullable Patient theResultResource, - boolean theDeleteSource) { - - // if the client provided a result resource as input then use it to update the target resource - if (theResultResource != null) { - return theResultResource; - } - - // client did not provide a result resource, we should update the target resource, - // add the replaces link to the target resource, if the source resource is not to be deleted - if (!theDeleteSource) { - theTargetResource - .addLink() - .setType(Patient.LinkType.REPLACES) - .setOther(new Reference(theSourceResource.getIdElement().toVersionless())); - } - - // copy all identifiers from the source to the target - copyIdentifiersAndMarkOld(theSourceResource, theTargetResource); - - return theTargetResource; - } - - /** - * Checks if theIdentifiers contains theIdentifier using equalsDeep - * - * @param theIdentifiers the list of identifiers - * @param theIdentifier the identifier to check - * @return true if theIdentifiers contains theIdentifier, false otherwise - */ - private boolean containsIdentifier(List theIdentifiers, Identifier theIdentifier) { - for (Identifier identifier : theIdentifiers) { - if (identifier.equalsDeep(theIdentifier)) { - return true; - } - } - return false; - } - - /** - * Copies each identifier from theSourceResource to theTargetResource, after checking that theTargetResource does - * not already contain the source identifier. Marks the copied identifiers marked as old. - * - * @param theSourceResource the source resource to copy identifiers from - * @param theTargetResource the target resource to copy identifiers to - */ - private void copyIdentifiersAndMarkOld(Patient theSourceResource, Patient theTargetResource) { - if (theSourceResource.hasIdentifier()) { - List sourceIdentifiers = theSourceResource.getIdentifier(); - List targetIdentifiers = theTargetResource.getIdentifier(); - for (Identifier sourceIdentifier : sourceIdentifiers) { - if (!containsIdentifier(targetIdentifiers, sourceIdentifier)) { - Identifier copyOfSrcIdentifier = sourceIdentifier.copy(); - copyOfSrcIdentifier.setUse(Identifier.IdentifierUse.OLD); - theTargetResource.addIdentifier(copyOfSrcIdentifier); - } - } - } - } - - private Patient updateResource(Patient theResource, RequestDetails theRequestDetails) { - DaoMethodOutcome outcome = myDao.update(theResource, theRequestDetails); - return (Patient) outcome.getResource(); - } - - private void deleteResource(Patient theResource, RequestDetails theRequestDetails) { - myDao.delete(theResource.getIdElement(), theRequestDetails); - } - /** * Validates the merge operation parameters and adds validation errors to the outcome * @@ -597,7 +614,7 @@ private IBaseResource resolveResourceByIdentifiers( searchParameterMap.add("identifier", tokenAndListParam); searchParameterMap.setCount(2); - IBundleProvider bundle = myDao.search(searchParameterMap, theRequestDetails); + IBundleProvider bundle = myPatientDao.search(searchParameterMap, theRequestDetails); List resources = bundle.getAllResources(); if (resources.isEmpty()) { String msg = String.format( @@ -627,7 +644,7 @@ private IBaseResource resolveResourceByReference( IIdType theResourceId = new IdType(r4ref.getReferenceElement().getValue()); IBaseResource resource; try { - resource = myDao.read(theResourceId.toVersionless(), theRequestDetails); + resource = myPatientDao.read(theResourceId.toVersionless(), theRequestDetails); } catch (ResourceNotFoundException e) { String msg = String.format( "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); @@ -676,4 +693,10 @@ private void addInfoToOperationOutcome( private void addErrorToOperationOutcome(IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theCode) { OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "error", theDiagnosticMsg, null, theCode); } + + private static class ValidationResult { + protected Patient sourceResource; + protected Patient targetResource; + protected boolean isValid; + } } diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java similarity index 94% rename from hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java rename to hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java index eb22bc024bbc..9f492ebc7e2f 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/dao/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java @@ -1,14 +1,15 @@ -package ca.uhn.fhir.jpa.dao.merge; +package ca.uhn.fhir.jpa.provider.merge; +import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; -import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; @@ -21,6 +22,7 @@ import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Task; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -46,7 +48,7 @@ @ExtendWith(MockitoExtension.class) public class ResourceMergeServiceTest { private static final Integer PAGE_SIZE = 1024; - + private static final String MISSING_SOURCE_PARAMS_MSG = "There are no source resource parameters provided, include either a 'source-patient', or a 'source-patient-identifier' parameter."; private static final String MISSING_TARGET_PARAMS_MSG = @@ -64,7 +66,10 @@ public class ResourceMergeServiceTest { private static final String TARGET_PATIENT_TEST_ID_WITH_VERSION_2 = TARGET_PATIENT_TEST_ID + "/_history/2"; @Mock - private IFhirResourceDaoPatient myDaoMock; + IFhirResourceDaoPatient myPatientDaoMock; + + @Mock + IFhirResourceDaoPatient myTaskDaoMock; @Mock IReplaceReferencesSvc myReplaceReferencesSvcMock; @@ -78,6 +83,9 @@ public class ResourceMergeServiceTest { @Mock IRequestPartitionHelperSvc myRequestPartitionHelperSvcMock; + @Mock + IJobCoordinator myJobCoordinatorMock; + private ResourceMergeService myResourceMergeService; @@ -89,8 +97,9 @@ public class ResourceMergeServiceTest { @BeforeEach void setup() { - when(myDaoMock.getContext()).thenReturn(myFhirContext); - myResourceMergeService = new ResourceMergeService(myDaoMock, myReplaceReferencesSvcMock, myTransactionServiceMock, myRequestPartitionHelperSvcMock); + when(myPatientDaoMock.getContext()).thenReturn(myFhirContext); + myResourceMergeService = new ResourceMergeService(myPatientDaoMock, myTaskDaoMock, myReplaceReferencesSvcMock, + myTransactionServiceMock, myRequestPartitionHelperSvcMock, myJobCoordinatorMock); } @@ -131,7 +140,7 @@ void testMerge_WithoutResultResource_Success() { new Identifier().setSystem("sysSource").setValue("valS1").setUse(Identifier.IdentifierUse.OLD), new Identifier().setSystem("sysSource").setValue("valS2").setUse(Identifier.IdentifierUse.OLD)); verifyUpdatedTargetPatient(true, expectedIdentifiers); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @@ -159,7 +168,7 @@ void testMerge_WithoutResultResource_TargetSetToActiveExplicitly_Success() { verifySuccessfulOutcome(mergeOutcome, patientReturnedFromDaoAfterTargetUpdate); verifyUpdatedSourcePatient(); verifyUpdatedTargetPatient(true, Collections.emptyList()); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -193,7 +202,7 @@ void testMerge_WithResultResource_Success() { verifySuccessfulOutcome(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); verifyUpdatedSourcePatient(); verifyUpdatedTargetPatient(true, Collections.emptyList()); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @@ -235,7 +244,7 @@ void testMerge_WithResultResource_ResultHasAllTargetIdentifiers_Success() { new Identifier().setSystem("sys").setValue("val2") ); verifyUpdatedTargetPatient(true, expectedIdentifiers); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -250,7 +259,7 @@ void testMerge_WithDeleteSourceTrue_Success() { setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); - when(myDaoMock.delete(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenReturn(new DaoMethodOutcome()); + when(myPatientDaoMock.delete(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenReturn(new DaoMethodOutcome()); Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientToBeReturnedFromDaoAfterTargetUpdate); setupTransactionServiceMock(); @@ -263,7 +272,7 @@ void testMerge_WithDeleteSourceTrue_Success() { // Then verifySuccessfulOutcome(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); verifyUpdatedTargetPatient(false, Collections.emptyList()); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @@ -281,7 +290,7 @@ void testMerge_WithDeleteSourceTrue_And_WithResultResource_Success() { setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); - when(myDaoMock.delete(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenReturn(new DaoMethodOutcome()); + when(myPatientDaoMock.delete(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenReturn(new DaoMethodOutcome()); Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); setupDaoMockForSuccessfulTargetPatientUpdate(resultPatient, patientToBeReturnedFromDaoAfterTargetUpdate); setupTransactionServiceMock(); @@ -294,7 +303,7 @@ void testMerge_WithDeleteSourceTrue_And_WithResultResource_Success() { // Then verifySuccessfulOutcome(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); verifyUpdatedTargetPatient(false, Collections.emptyList()); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -325,7 +334,7 @@ void testMerge_WithPreviewTrue_Success() { assertThat(issue.getDetails().getText()).contains("Preview only merge operation - no issues detected"); assertThat(issue.getDiagnostics()).contains("Merge would update 12 resources"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -351,7 +360,7 @@ void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersio verifySuccessfulOutcome(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); verifyUpdatedSourcePatient(); verifyUpdatedTargetPatient(true, Collections.emptyList()); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } // ERROR CASES @@ -364,7 +373,7 @@ void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheExcepti mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); ForbiddenOperationException ex = new ForbiddenOperationException("this is the exception message"); - when(myDaoMock.read(any(), eq(myRequestDetailsMock))).thenThrow(ex); + when(myPatientDaoMock.read(any(), eq(myRequestDetailsMock))).thenThrow(ex); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -378,7 +387,7 @@ void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheExcepti assertThat(issue.getDiagnostics()).contains("this is the exception message"); assertThat(issue.getCode().toCode()).isEqualTo("exception"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -389,7 +398,7 @@ void testMerge_UnhandledExceptionThrown_Uses500StatusCode() { mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); RuntimeException ex = new RuntimeException("this is the exception message"); - when(myDaoMock.read(any(), eq(myRequestDetailsMock))).thenThrow(ex); + when(myPatientDaoMock.read(any(), eq(myRequestDetailsMock))).thenThrow(ex); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -403,7 +412,7 @@ void testMerge_UnhandledExceptionThrown_Uses500StatusCode() { assertThat(issue.getDiagnostics()).contains("this is the exception message"); assertThat(issue.getCode().toCode()).isEqualTo("exception"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -425,7 +434,7 @@ void testMerge_ValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorW assertThat(issue.getDiagnostics()).contains(MISSING_SOURCE_PARAMS_MSG); assertThat(issue.getCode().toCode()).isEqualTo("required"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @@ -449,7 +458,7 @@ void testMerge_ValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorW assertThat(issue.getDiagnostics()).contains(MISSING_TARGET_PARAMS_MSG); assertThat(issue.getCode().toCode()).isEqualTo("required"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -474,7 +483,7 @@ void testMerge_ValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ assertThat(issue2.getDiagnostics()).contains(MISSING_TARGET_PARAMS_MSG); assertThat(issue2.getCode().toCode()).isEqualTo("required"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -499,7 +508,7 @@ void testMerge_ValidatesInputParameters_BothSourceResourceAndSourceIdentifierPar assertThat(issue.getCode().toCode()).isEqualTo("required"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @@ -524,7 +533,7 @@ void testMerge_ValidatesInputParameters_BothTargetResourceAndTargetIdentifiersPa assertThat(issue.getDiagnostics()).contains(BOTH_TARGET_PARAMS_PROVIDED_MSG); assertThat(issue.getCode().toCode()).isEqualTo("required"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @@ -549,7 +558,7 @@ void testMerge_ValidatesInputParameters_SourceResourceParamHasNoReferenceElement assertThat(issue.getDiagnostics()).contains("Reference specified in 'source-patient' parameter does not have a reference element."); assertThat(issue.getCode().toCode()).isEqualTo("required"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @@ -575,7 +584,7 @@ void testMerge_ValidatesInputParameters_TargetResourceParamHasNoReferenceElement "a reference element."); assertThat(issue.getCode().toCode()).isEqualTo("required"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -584,7 +593,7 @@ void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWi MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); - when(myDaoMock.read(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); + when(myPatientDaoMock.read(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -599,7 +608,7 @@ void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWi assertThat(issue.getDiagnostics()).contains("Resource not found for the reference specified in 'source-patient'"); assertThat(issue.getCode().toCode()).isEqualTo("not-found"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -610,7 +619,7 @@ void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWi mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); setupDaoMockForSuccessfulRead(sourcePatient); - when(myDaoMock.read(new IdType(TARGET_PATIENT_TEST_ID), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); + when(myPatientDaoMock.read(new IdType(TARGET_PATIENT_TEST_ID), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -625,7 +634,7 @@ void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWi assertThat(issue.getDiagnostics()).contains("Resource not found for the reference specified in 'target-patient'"); assertThat(issue.getCode().toCode()).isEqualTo("not-found"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -653,7 +662,7 @@ void testMerge_ResolvesSourceResourceByIdentifiers_NoMatchFound_ReturnsErrorWith assertThat(issue.getDiagnostics()).contains("No resources found matching the identifier(s) specified in 'source-patient-identifier'"); assertThat(issue.getCode().toCode()).isEqualTo("not-found"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @@ -685,7 +694,7 @@ void testMerge_ResolvesSourceResourceByIdentifiers_MultipleMatchesFound_ReturnsE " 'source-patient-identifier'"); assertThat(issue.getCode().toCode()).isEqualTo("multiple-matches"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @@ -717,7 +726,7 @@ void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith "'target-patient-identifier'"); assertThat(issue.getCode().toCode()).isEqualTo("not-found"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -750,7 +759,7 @@ void testMerge_ResolvesTargetResourceByIdentifiers_MultipleMatchesFound_ReturnsE assertThat(issue.getDiagnostics()).contains("Multiple resources found matching the identifier(s) specified in 'target-patient-identifier'"); assertThat(issue.getCode().toCode()).isEqualTo("multiple-matches"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -776,7 +785,7 @@ void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVe assertThat(issue.getDiagnostics()).contains("The reference in 'source-patient' parameter has a version specified, but it is not the latest version of the resource"); assertThat(issue.getCode().toCode()).isEqualTo("conflict"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -805,7 +814,7 @@ void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVe "specified, but it is not the latest version of the resource"); assertThat(issue.getCode().toCode()).isEqualTo("conflict"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @@ -864,7 +873,7 @@ void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status() { assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); assertThat(issue.getDiagnostics()).contains("Target resource is not active, it must be active to be the target of a merge operation"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -893,7 +902,7 @@ void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsError "reference 'Patient/replacing-res-id', it is " + "not a suitable target for merging."); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -921,7 +930,7 @@ void testMerge_SourceResourceWasPreviouslyReplacedByAnotherResource_ReturnsError assertThat(issue.getDiagnostics()).contains("Source resource was previously replaced by a resource with " + "reference 'Patient/replacing-res-id', it is not a suitable source for merging."); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -953,7 +962,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetRes "as the actual" + " resolved target resource. The actual resolved target resource's id is: '" + TARGET_PATIENT_TEST_ID +"'"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @@ -990,7 +999,7 @@ void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersPr assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); assertThat(issue.getDiagnostics()).contains("'result-patient' must have all the identifiers provided in target-patient-identifier"); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @@ -1020,7 +1029,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLinkAtAll_Retu assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); assertThat(issue.getDiagnostics()).contains("'result-patient' must have a 'replaces' link to the source resource."); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -1051,7 +1060,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLinkToSource_R assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); assertThat(issue.getDiagnostics()).contains("'result-patient' must have a 'replaces' link to the source resource."); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -1082,7 +1091,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasReplacesLinkAndDeleteSou assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); assertThat(issue.getDiagnostics()).contains("'result-patient' must not have a 'replaces' link to the source resource when the source resource will be deleted, as the link may prevent deleting the source resource."); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } @Test @@ -1115,7 +1124,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasRedundantReplacesLinksTo assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); assertThat(issue.getDiagnostics()).contains("'result-patient' has multiple 'replaces' links to the source resource. There should be only one."); - verifyNoMoreInteractions(myDaoMock); + verifyNoMoreInteractions(myPatientDaoMock); } private void verifySuccessfulOutcome(MergeOperationOutcome theMergeOutcome, Patient theExpectedTargetResource) { @@ -1156,7 +1165,7 @@ private void setupTransactionServiceMock() { private void setupDaoMockForSuccessfulRead(Patient resource) { assertThat(resource.getIdElement()).isNotNull(); //dao reads the versionless id - when(myDaoMock.read(resource.getIdElement().toVersionless(), myRequestDetailsMock)).thenReturn(resource); + when(myPatientDaoMock.read(resource.getIdElement().toVersionless(), myRequestDetailsMock)).thenReturn(resource); } @@ -1172,7 +1181,7 @@ private void setupDaoMockSearchForIdentifiers(List> theMatch IBundleProvider bundleProviderMock = mock(IBundleProvider.class); when(bundleProviderMock.getAllResources()).thenReturn(matchingResources); if (ongoingStubbing == null) { - ongoingStubbing = when(myDaoMock.search(any(), eq(myRequestDetailsMock))).thenReturn(bundleProviderMock); + ongoingStubbing = when(myPatientDaoMock.search(any(), eq(myRequestDetailsMock))).thenReturn(bundleProviderMock); } else { ongoingStubbing.thenReturn(bundleProviderMock); @@ -1191,7 +1200,7 @@ private void setupDaoMockForSuccessfulSourcePatientUpdate(Patient thePatientExpe Patient thePatientToReturnInDaoOutcome) { DaoMethodOutcome daoMethodOutcome = new DaoMethodOutcome(); daoMethodOutcome.setResource(thePatientToReturnInDaoOutcome); - when(myDaoMock.update(thePatientExpectedAsInput, myRequestDetailsMock)) + when(myPatientDaoMock.update(thePatientExpectedAsInput, myRequestDetailsMock)) .thenAnswer(t -> { myCapturedSourcePatientForUpdate = t.getArgument(0); @@ -1230,18 +1239,18 @@ private void setupDaoMockForSuccessfulTargetPatientUpdate(Patient thePatientExpe Patient thePatientToReturnInDaoOutcome) { DaoMethodOutcome daoMethodOutcome = new DaoMethodOutcome(); daoMethodOutcome.setResource(thePatientToReturnInDaoOutcome); - when(myDaoMock.update(thePatientExpectedAsInput, myRequestDetailsMock)) + when(myPatientDaoMock.update(thePatientExpectedAsInput, myRequestDetailsMock)) .thenAnswer(t -> { myCapturedTargetPatientForUpdate = t.getArgument(0); DaoMethodOutcome outcome = new DaoMethodOutcome(); - outcome.setResource(thePatientToReturnInDaoOutcome); - return outcome; - }); + outcome.setResource(thePatientToReturnInDaoOutcome); + return outcome; + }); } private void verifySearchParametersOnDaoSearchInvocations(List> theExpectedIdentifierParams) { ArgumentCaptor captor = ArgumentCaptor.forClass(SearchParameterMap.class); - verify(myDaoMock, times(theExpectedIdentifierParams.size())).search(captor.capture(), eq(myRequestDetailsMock)); + verify(myPatientDaoMock, times(theExpectedIdentifierParams.size())).search(captor.capture(), eq(myRequestDetailsMock)); List maps = captor.getAllValues(); assertThat(maps).hasSameSizeAs(theExpectedIdentifierParams); for (int i = 0; i < maps.size(); i++) { @@ -1260,3 +1269,4 @@ private void verifySearchParameterOnSingleDaoSearchInvocation(SearchParameterMap } } } + diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 6b2d46dbef87..d1a6d778823a 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -8,6 +8,7 @@ import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInput; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import jakarta.annotation.Nonnull; import org.hl7.fhir.r4.model.Bundle; @@ -46,6 +47,7 @@ import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; public class PatientMergeR4Test extends BaseResourceProviderR4Test { @@ -133,6 +135,20 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea assertTrue(input.equalsDeep(inParameters)); + List expectedIdentifiersOnTargetAfterMerge = null; + if (withInputResultPatient) { + expectedIdentifiersOnTargetAfterMerge = List.of(new Identifier().setSystem("SYS1A").setValue("VAL1A")); + } else { + //the identifiers copied over from source should be marked as old + expectedIdentifiersOnTargetAfterMerge = List.of( + new Identifier().setSystem("SYS2A").setValue("VAL2A"), + new Identifier().setSystem("SYS2B").setValue("VAL2B"), + new Identifier().setSystem("SYSC").setValue("VALC"), + new Identifier().setSystem("SYS1A").setValue("VAL1A").copy().setUse(Identifier.IdentifierUse.OLD), + new Identifier().setSystem("SYS1B").setValue("VAL1B").copy().setUse(Identifier.IdentifierUse.OLD) + ); + } + // Assert Task inAsync mode, unless it is preview in which case we don't return a task if (isAsync && !withPreview) { Task task = (Task) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource(); @@ -192,28 +208,7 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea // Assert Merged Patient Patient mergedPatient = (Patient) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_RESULT).getResource(); List identifiers = mergedPatient.getIdentifier(); - if (withInputResultPatient) { - assertThat(identifiers).hasSize(1); - assertThat(identifiers.get(0).getSystem()).isEqualTo("SYS1A"); - assertThat(identifiers.get(0).getValue()).isEqualTo("VAL1A"); - } else { - assertThat(identifiers).hasSize(5); - assertThat(identifiers) - .extracting(Identifier::getSystem) - .containsExactlyInAnyOrder("SYS1A", "SYS1B", "SYS2A", "SYS2B", "SYSC"); - assertThat(identifiers) - .extracting(Identifier::getValue) - .containsExactlyInAnyOrder("VAL1A", "VAL1B", "VAL2A", "VAL2B", "VALC"); - } - if (!withPreview && !withDelete) { - // assert source has link to target - Patient source = myTestHelper.readSourcePatient(); - assertThat(source.getLink()) - .hasSize(1) - .element(0) - .extracting(link -> link.getOther().getReferenceElement()) - .isEqualTo(myTestHelper.getTargetPatientId()); - } + assertIdentifiers(identifiers, expectedIdentifiersOnTargetAfterMerge); } // Check that the linked resources were updated @@ -223,6 +218,8 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea myTestHelper.assertNothingChanged(); } else { myTestHelper.assertAllReferencesUpdated(withDelete); + assertSourcePatientUpdatedOrDeleted(withDelete); + assertTargetPatientUpdated(withDelete, expectedIdentifiersOnTargetAfterMerge); } } @@ -267,6 +264,44 @@ public void testMultipleSourceMatchesFails(boolean withDelete, boolean withInput assertUnprocessibleEntityWithMessage(inParameters, "Multiple resources found matching the identifier(s) specified in 'source-patient-identifier'"); } + + private void assertSourcePatientUpdatedOrDeleted(boolean withDelete) { + if (withDelete) { + // the spec says the deleted resource should return 404 but we seem to return 410 Gone + assertThrows(ResourceGoneException.class, () -> myTestHelper.readSourcePatient()); + } + else { + Patient source = myTestHelper.readSourcePatient(); + assertThat(source.getLink()).hasSize(1); + Patient.PatientLinkComponent link = source.getLink().get(0); + assertThat(link.getOther().getReferenceElement()).isEqualTo(myTestHelper.getTargetPatientId()); + assertThat(link.getType()).isEqualTo(Patient.LinkType.REPLACEDBY); + } + + } + + private void assertTargetPatientUpdated(boolean withDelete, List theExpectedIdentifiers) { + Patient target = myTestHelper.readTargetPatient(); + if (!withDelete) { + assertThat(target.getLink()).hasSize(1); + Patient.PatientLinkComponent link = target.getLink().get(0); + assertThat(link.getOther().getReferenceElement()).isEqualTo(myTestHelper.getSourcePatientId()); + assertThat(link.getType()).isEqualTo(Patient.LinkType.REPLACES); + } + //assertExpected Identifiers found on the target + assertIdentifiers(target.getIdentifier(), theExpectedIdentifiers); + } + + private void assertIdentifiers(List theActualIdentifiers, List theExpectedIdentifiers) { + assertThat(theActualIdentifiers).hasSize(theExpectedIdentifiers.size()); + for (int i = 0; i < theExpectedIdentifiers.size(); i++) { + Identifier expectedIdentifier = theExpectedIdentifiers.get(i); + Identifier actualIdentifier = theActualIdentifiers.get(i); + assertThat(expectedIdentifier.equalsDeep(actualIdentifier)).isTrue(); + } + } + + private void assertUnprocessibleEntityWithMessage(Parameters inParameters, String theExpectedMessage) { assertThatThrownBy(() -> callMergeOperation(inParameters)) diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index 5c2a92a1f93a..db9186ab5c6a 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -171,6 +171,10 @@ public Patient readSourcePatient() { return myPatientDao.read(mySourcePatientId, mySrd); } + public Patient readTargetPatient() { + return myPatientDao.read(myTargetPatientId, mySrd); + } + public IIdType getTargetPatientId() { return myTargetPatientId; } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java index 4dd595578fff..90b22da35b8e 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java @@ -1,3 +1,22 @@ +/*- + * #%L + * hapi-fhir-storage-batch2-jobs + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.batch2.jobs.merge; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdListWorkChunkJson; @@ -7,6 +26,7 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -57,7 +77,8 @@ public ReplaceReferenceUpdateStep mergeUpdateStep( } @Bean - public MergeUpdateTaskReducerStep mergeUpdateTaskStep(DaoRegistry theDaoRegistry) { - return new MergeUpdateTaskReducerStep(theDaoRegistry); + public MergeUpdateTaskReducerStep mergeUpdateTaskStep( + DaoRegistry theDaoRegistry, IHapiTransactionService theHapiTransactionService) { + return new MergeUpdateTaskReducerStep(theDaoRegistry, theHapiTransactionService); } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeHelper.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeHelper.java new file mode 100644 index 000000000000..4360e917ee05 --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeHelper.java @@ -0,0 +1,171 @@ +/*- + * #%L + * hapi-fhir-storage-batch2-jobs + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.batch2.jobs.merge; + +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import jakarta.annotation.Nullable; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Reference; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * This class contains code that is used to update source and target resources after the references are replaced. + * This is the common functionality that is used in sync case and in the async case as the reduction step. + */ +public class MergeHelper { + + private final IFhirResourceDao myPatientDao; + + public MergeHelper(IFhirResourceDao theDao) { + myPatientDao = theDao; + } + + public void updateMergedResourcesAfterReferencesReplaced( + IHapiTransactionService myHapiTransactionService, + IIdType theSourceResourceId, + IIdType theTargetResourceId, + @Nullable Patient theResultResource, + boolean theDeleteSource, + RequestDetails theRequestDetails) { + Patient sourceResource = myPatientDao.read(theSourceResourceId, theRequestDetails); + Patient targetResource = myPatientDao.read(theTargetResourceId, theRequestDetails); + + updateMergedResourcesAfterReferencesReplaced( + myHapiTransactionService, + sourceResource, + targetResource, + theResultResource, + theDeleteSource, + theRequestDetails); + } + + public Patient updateMergedResourcesAfterReferencesReplaced( + IHapiTransactionService myHapiTransactionService, + Patient theSourceResource, + Patient theTargetResource, + @Nullable Patient theResultResource, + boolean theDeleteSource, + RequestDetails theRequestDetails) { + + AtomicReference targetPatientAfterUpdate = new AtomicReference<>(); + myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { + Patient patientToUpdate = prepareTargetPatientForUpdate( + theTargetResource, theSourceResource, theResultResource, theDeleteSource); + // update the target patient resource after the references are updated + targetPatientAfterUpdate.set(updateResource(patientToUpdate, theRequestDetails)); + + if (theDeleteSource) { + deleteResource(theSourceResource, theRequestDetails); + } else { + prepareSourcePatientForUpdate(theSourceResource, theTargetResource); + updateResource(theSourceResource, theRequestDetails); + } + }); + + return targetPatientAfterUpdate.get(); + } + + public Patient prepareTargetPatientForUpdate( + Patient theTargetResource, + Patient theSourceResource, + @Nullable Patient theResultResource, + boolean theDeleteSource) { + + // if the client provided a result resource as input then use it to update the target resource + if (theResultResource != null) { + return theResultResource; + } + + // client did not provide a result resource, we should update the target resource, + // add the replaces link to the target resource, if the source resource is not to be deleted + if (!theDeleteSource) { + theTargetResource + .addLink() + .setType(Patient.LinkType.REPLACES) + .setOther(new Reference(theSourceResource.getIdElement().toVersionless())); + } + + // copy all identifiers from the source to the target + copyIdentifiersAndMarkOld(theSourceResource, theTargetResource); + + return theTargetResource; + } + + private void prepareSourcePatientForUpdate(Patient theSourceResource, Patient theTargetResource) { + theSourceResource.setActive(false); + theSourceResource + .addLink() + .setType(Patient.LinkType.REPLACEDBY) + .setOther(new Reference(theTargetResource.getIdElement().toVersionless())); + } + + /** + * Copies each identifier from theSourceResource to theTargetResource, after checking that theTargetResource does + * not already contain the source identifier. Marks the copied identifiers marked as old. + * + * @param theSourceResource the source resource to copy identifiers from + * @param theTargetResource the target resource to copy identifiers to + */ + private void copyIdentifiersAndMarkOld(Patient theSourceResource, Patient theTargetResource) { + if (theSourceResource.hasIdentifier()) { + List sourceIdentifiers = theSourceResource.getIdentifier(); + List targetIdentifiers = theTargetResource.getIdentifier(); + for (Identifier sourceIdentifier : sourceIdentifiers) { + if (!containsIdentifier(targetIdentifiers, sourceIdentifier)) { + Identifier copyOfSrcIdentifier = sourceIdentifier.copy(); + copyOfSrcIdentifier.setUse(Identifier.IdentifierUse.OLD); + theTargetResource.addIdentifier(copyOfSrcIdentifier); + } + } + } + } + + /** + * Checks if theIdentifiers contains theIdentifier using equalsDeep + * + * @param theIdentifiers the list of identifiers + * @param theIdentifier the identifier to check + * @return true if theIdentifiers contains theIdentifier, false otherwise + */ + private boolean containsIdentifier(List theIdentifiers, Identifier theIdentifier) { + for (Identifier identifier : theIdentifiers) { + if (identifier.equalsDeep(theIdentifier)) { + return true; + } + } + return false; + } + + private Patient updateResource(Patient theResource, RequestDetails theRequestDetails) { + DaoMethodOutcome outcome = myPatientDao.update(theResource, theRequestDetails); + return (Patient) outcome.getResource(); + } + + private void deleteResource(Patient theResource, RequestDetails theRequestDetails) { + myPatientDao.delete(theResource.getIdElement(), theRequestDetails); + } +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java index f397d225319a..84a2ad119658 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java @@ -1,7 +1,47 @@ +/*- + * #%L + * hapi-fhir-storage-batch2-jobs + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.batch2.jobs.merge; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; +import com.fasterxml.jackson.annotation.JsonProperty; public class MergeJobParameters extends ReplaceReferencesJobParameters { - // FIXME ED add delete parameter, and maybe others + @JsonProperty("deleteSource") + private boolean myDeleteSource; + + @JsonProperty("resultResource") + private String myResultResource; + + public void setResultResource(String theResultResource) { + myResultResource = theResultResource; + } + + public String getResultResource() { + return myResultResource; + } + + public boolean isDeleteSource() { + return myDeleteSource; + } + + public void setDeleteSource(boolean theDeleteSource) { + this.myDeleteSource = theDeleteSource; + } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java index 70a97a1c49cc..2f8c209f63e0 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java @@ -1,3 +1,22 @@ +/*- + * #%L + * hapi-fhir-storage-batch2-jobs + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.batch2.jobs.merge; import ca.uhn.fhir.batch2.api.IJobDataSink; @@ -8,11 +27,18 @@ import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceResultsJson; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceUpdateTaskReducerStep; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import jakarta.annotation.Nonnull; +import org.hl7.fhir.r4.model.Patient; public class MergeUpdateTaskReducerStep extends ReplaceReferenceUpdateTaskReducerStep { - public MergeUpdateTaskReducerStep(DaoRegistry theDaoRegistry) { + private final IHapiTransactionService myHapiTransactionService; + + public MergeUpdateTaskReducerStep(DaoRegistry theDaoRegistry, IHapiTransactionService theHapiTransactionService) { super(theDaoRegistry); + this.myHapiTransactionService = theHapiTransactionService; } @Nonnull @@ -21,7 +47,30 @@ public RunOutcome run( @Nonnull StepExecutionDetails theStepExecutionDetails, @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { - // FIXME ED add in extra merge steps here e.g. updating source and target resources + + MergeJobParameters mergeJobParameters = theStepExecutionDetails.getParameters(); + + SystemRequestDetails requestDetails = + SystemRequestDetails.forRequestPartitionId(mergeJobParameters.getPartitionId()); + + Patient resultResource = null; + if (mergeJobParameters.getResultResource() != null) { + resultResource = + myFhirContext.newJsonParser().parseResource(Patient.class, mergeJobParameters.getResultResource()); + } + + IFhirResourceDao patientDao = myDaoRegistry.getResourceDao(Patient.class); + + MergeHelper helper = new MergeHelper(patientDao); + + helper.updateMergedResourcesAfterReferencesReplaced( + myHapiTransactionService, + mergeJobParameters.getSourceId().asIdDt(), + mergeJobParameters.getTargetId().asIdDt(), + resultResource, + mergeJobParameters.isDeleteSource(), + requestDetails); + return super.run(theStepExecutionDetails, theDataSink); } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java index d75b4fdec3c2..1cb22c1c0d35 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java @@ -42,8 +42,8 @@ public class ReplaceReferenceUpdateTaskReducerStep { public static final String RESOURCE_TYPES_SYSTEM = "http://hl7.org/fhir/ValueSet/resource-types"; - private final FhirContext myFhirContext; - private final DaoRegistry myDaoRegistry; + protected final FhirContext myFhirContext; + protected final DaoRegistry myDaoRegistry; private List myPatchOutputBundles = new ArrayList<>(); diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java index 5d8419fc1b7f..1d8e9458a867 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java @@ -20,18 +20,15 @@ package ca.uhn.fhir.batch2.jobs.replacereferences; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; +import ca.uhn.fhir.batch2.jobs.parameters.BatchJobParametersWithTaskId; import ca.uhn.fhir.interceptor.model.RequestPartitionId; -import ca.uhn.fhir.model.api.BaseBatchJobParameters; import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; import com.fasterxml.jackson.annotation.JsonProperty; -import org.hl7.fhir.instance.model.api.IIdType; import static ca.uhn.fhir.jpa.api.config.JpaStorageSettings.DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE; import static ca.uhn.fhir.jpa.api.config.JpaStorageSettings.DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING; -public class ReplaceReferencesJobParameters extends BaseBatchJobParameters { - @JsonProperty("taskId") - private FhirIdJson myTaskId; +public class ReplaceReferencesJobParameters extends BatchJobParametersWithTaskId { @JsonProperty("sourceId") private FhirIdJson mySourceId; @@ -92,14 +89,6 @@ public void setPartitionId(RequestPartitionId thePartitionId) { myPartitionId = thePartitionId; } - public void setTaskId(IIdType theTaskId) { - myTaskId = new FhirIdJson(theTaskId); - } - - public FhirIdJson getTaskId() { - return myTaskId; - } - public ReplaceReferenceRequest asReplaceReferencesRequest() { return new ReplaceReferenceRequest(mySourceId.asIdDt(), myTargetId.asIdDt(), myBatchSize, myPartitionId); } diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/parameters/BatchJobParametersWithTaskId.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/parameters/BatchJobParametersWithTaskId.java new file mode 100644 index 000000000000..222b1e3d817f --- /dev/null +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/parameters/BatchJobParametersWithTaskId.java @@ -0,0 +1,38 @@ +/*- + * #%L + * HAPI FHIR JPA Server - Batch2 Task Processor + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.batch2.jobs.parameters; + +import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; +import ca.uhn.fhir.model.api.BaseBatchJobParameters; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.hl7.fhir.instance.model.api.IIdType; + +public class BatchJobParametersWithTaskId extends BaseBatchJobParameters { + @JsonProperty("taskId") + private FhirIdJson myTaskId; + + public void setTaskId(IIdType theTaskId) { + myTaskId = new FhirIdJson(theTaskId); + } + + public FhirIdJson getTaskId() { + return myTaskId; + } +} diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskUtils.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskUtils.java new file mode 100644 index 000000000000..47f684f3b8dd --- /dev/null +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskUtils.java @@ -0,0 +1,58 @@ +/*- + * #%L + * HAPI FHIR JPA Server - Batch2 Task Processor + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.batch2.util; + +import ca.uhn.fhir.batch2.api.IJobCoordinator; +import ca.uhn.fhir.batch2.jobs.parameters.BatchJobParametersWithTaskId; +import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.hl7.fhir.r4.model.Task; + +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.HAPI_BATCH_JOB_ID_SYSTEM; + +public class Batch2TaskUtils { + + private Batch2TaskUtils() { + // non-instantiable + } + + public static Task startJobAndCreateAssociatedTask( + IFhirResourceDao theTaskDao, + RequestDetails theRequestDetails, + IJobCoordinator theJobCoordinator, + String theJobDefinitionId, + BatchJobParametersWithTaskId theJobParams) { + Task task = new Task(); + task.setStatus(Task.TaskStatus.INPROGRESS); + theTaskDao.create(task, theRequestDetails); + + theJobParams.setTaskId(task.getIdElement().toUnqualifiedVersionless()); + + JobInstanceStartRequest request = new JobInstanceStartRequest(theJobDefinitionId, theJobParams); + Batch2JobStartResponse jobStartResponse = theJobCoordinator.startInstance(theRequestDetails, request); + + task.addIdentifier().setSystem(HAPI_BATCH_JOB_ID_SYSTEM).setValue(jobStartResponse.getInstanceId()); + theTaskDao.update(task, theRequestDetails); + + return task; + } +} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java index 0543ec6e7a93..d409275ece16 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java @@ -45,6 +45,8 @@ public class ReplaceReferenceRequest { public final RequestPartitionId partitionId; + private boolean myForceSync = false; + public ReplaceReferenceRequest( @Nonnull IIdType theSourceId, @Nonnull IIdType theTargetId, @@ -83,4 +85,12 @@ public SearchParameterMap getSearchParameterMap() { // Note we do not set the count since we will be streaming return retval; } + + public boolean isForceSync() { + return myForceSync; + } + + public void setForceSync(boolean forceSync) { + this.myForceSync = forceSync; + } } From 6fab7b7d6c29ffbed0f7fda61698058bdcfd3468 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 13 Dec 2024 17:59:45 -0500 Subject: [PATCH 082/148] starting work on new docs --- .../jpa/provider/r4/PatientMergeR4Test.java | 7 + .../provider/r4/ReplaceReferencesR4Test.java | 2 + .../src/test/resources/logback-test.xml | 4 +- .../provider/BaseResourceProviderR4Test.java | 122 +++++++++--------- 4 files changed, 74 insertions(+), 61 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 6b2d46dbef87..bd07fbb3f2b1 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -135,6 +135,8 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea // Assert Task inAsync mode, unless it is preview in which case we don't return a task if (isAsync && !withPreview) { + // FIXME KHS assert we got back a 202 Accepted + Task task = (Task) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource(); assertNull(task.getIdElement().getVersionIdPart()); ourLog.info("Got task {}", task.getId()); @@ -326,4 +328,9 @@ public void handleTestExecutionException(ExtensionContext theExtensionContext, T .map(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics) .collect(Collectors.joining(", ")); } + + @Override + protected boolean verboseClientLogging() { + return true; + } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index 3ae985c654fc..56d4e2f4ad0b 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -49,6 +49,8 @@ void testReplaceReferences(boolean isAsync) throws IOException { Bundle patchResultBundle; if (isAsync) { + // FIXME KHS assert we got back a 202 Accepted + Task task = (Task) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).getResource(); assertNull(task.getIdElement().getVersionIdPart()); ourLog.info("Got task {}", task.getId()); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml b/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml index dcada7fec76c..d7283de32e54 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml @@ -6,7 +6,7 @@ - + @@ -16,7 +16,7 @@ - + diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java index 64b24b42e239..4d53a80388ba 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java @@ -76,60 +76,60 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { @RegisterExtension protected RestfulServerConfigurerExtension myServerConfigurer = new RestfulServerConfigurerExtension(() -> myServer) - .withServerBeforeAll(s -> { - s.registerProviders(myResourceProviders.createProviders()); - s.setDefaultResponseEncoding(EncodingEnum.XML); - s.setDefaultPrettyPrint(false); - - myFhirContext.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator()); - - s.registerProvider(mySystemProvider); - s.registerProvider(myBinaryAccessProvider); - s.registerProvider(myAppCtx.getBean(BulkDataExportProvider.class)); - s.registerProvider(myAppCtx.getBean(DeleteExpungeProvider.class)); - s.registerProvider(myAppCtx.getBean(DiffProvider.class)); - s.registerProvider(myAppCtx.getBean(GraphQLProvider.class)); - s.registerProvider(myAppCtx.getBean(ProcessMessageProvider.class)); - s.registerProvider(myAppCtx.getBean(ReindexProvider.class)); - s.registerProvider(myAppCtx.getBean(SubscriptionTriggeringProvider.class)); - s.registerProvider(myAppCtx.getBean(TerminologyUploaderProvider.class)); - s.registerProvider(myAppCtx.getBean(ValueSetOperationProvider.class)); - - s.setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class)); - - JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider( - s, mySystemDao, myStorageSettings, mySearchParamRegistry, myValidationSupport); - confProvider.setImplementationDescription("THIS IS THE DESC"); - s.setServerConformanceProvider(confProvider); - - // Register a CORS filter - CorsConfiguration config = new CorsConfiguration(); - CorsInterceptor corsInterceptor = new CorsInterceptor(config); - config.addAllowedHeader("Accept"); - config.addAllowedHeader("Access-Control-Request-Headers"); - config.addAllowedHeader("Access-Control-Request-Method"); - config.addAllowedHeader("Cache-Control"); - config.addAllowedHeader("Content-Type"); - config.addAllowedHeader("Origin"); - config.addAllowedHeader("Prefer"); - config.addAllowedHeader("x-fhir-starter"); - config.addAllowedHeader("X-Requested-With"); - config.addAllowedOrigin("*"); - config.addExposedHeader("Location"); - config.addExposedHeader("Content-Location"); - config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); - s.registerInterceptor(corsInterceptor); - }) - .withServerBeforeEach(s -> { - myPort = myServer.getPort(); - myServerBase = myServer.getBaseUrl(); - myClient = myServer.getFhirClient(); - - myClient.getInterceptorService().unregisterInterceptorsIf(t -> t instanceof LoggingInterceptor); - if (shouldLogClient()) { - myClient.registerInterceptor(new LoggingInterceptor()); - } - }); + .withServerBeforeAll(s -> { + s.registerProviders(myResourceProviders.createProviders()); + s.setDefaultResponseEncoding(EncodingEnum.XML); + s.setDefaultPrettyPrint(false); + + myFhirContext.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator()); + + s.registerProvider(mySystemProvider); + s.registerProvider(myBinaryAccessProvider); + s.registerProvider(myAppCtx.getBean(BulkDataExportProvider.class)); + s.registerProvider(myAppCtx.getBean(DeleteExpungeProvider.class)); + s.registerProvider(myAppCtx.getBean(DiffProvider.class)); + s.registerProvider(myAppCtx.getBean(GraphQLProvider.class)); + s.registerProvider(myAppCtx.getBean(ProcessMessageProvider.class)); + s.registerProvider(myAppCtx.getBean(ReindexProvider.class)); + s.registerProvider(myAppCtx.getBean(SubscriptionTriggeringProvider.class)); + s.registerProvider(myAppCtx.getBean(TerminologyUploaderProvider.class)); + s.registerProvider(myAppCtx.getBean(ValueSetOperationProvider.class)); + + s.setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class)); + + JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider( + s, mySystemDao, myStorageSettings, mySearchParamRegistry, myValidationSupport); + confProvider.setImplementationDescription("THIS IS THE DESC"); + s.setServerConformanceProvider(confProvider); + + // Register a CORS filter + CorsConfiguration config = new CorsConfiguration(); + CorsInterceptor corsInterceptor = new CorsInterceptor(config); + config.addAllowedHeader("Accept"); + config.addAllowedHeader("Access-Control-Request-Headers"); + config.addAllowedHeader("Access-Control-Request-Method"); + config.addAllowedHeader("Cache-Control"); + config.addAllowedHeader("Content-Type"); + config.addAllowedHeader("Origin"); + config.addAllowedHeader("Prefer"); + config.addAllowedHeader("x-fhir-starter"); + config.addAllowedHeader("X-Requested-With"); + config.addAllowedOrigin("*"); + config.addExposedHeader("Location"); + config.addExposedHeader("Content-Location"); + config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + s.registerInterceptor(corsInterceptor); + }) + .withServerBeforeEach(s -> { + myPort = myServer.getPort(); + myServerBase = myServer.getBaseUrl(); + myClient = myServer.getFhirClient(); + + myClient.getInterceptorService().unregisterInterceptorsIf(t -> t instanceof LoggingInterceptor); + if (shouldLogClient()) { + myClient.registerInterceptor(new LoggingInterceptor(verboseClientLogging())); + } + }); @Autowired protected SubscriptionLoader mySubscriptionLoader; @@ -157,14 +157,18 @@ protected boolean shouldLogClient() { return true; } + protected boolean verboseClientLogging() { + return false; + } + protected List toNameList(Bundle resp) { List names = new ArrayList<>(); for (BundleEntryComponent next : resp.getEntry()) { Patient nextPt = (Patient) next.getResource(); String nextStr = nextPt.getName().size() > 0 - ? nextPt.getName().get(0).getGivenAsSingleString() + " " - + nextPt.getName().get(0).getFamily() - : ""; + ? nextPt.getName().get(0).getGivenAsSingleString() + " " + + nextPt.getName().get(0).getFamily() + : ""; if (isNotBlank(nextStr)) { names.add(nextStr); } @@ -206,7 +210,7 @@ public static List getParametersByName(Parameters } public static ParametersParameterComponent getPartByName( - ParametersParameterComponent theParameter, String theName) { + ParametersParameterComponent theParameter, String theName) { for (ParametersParameterComponent part : theParameter.getPart()) { if (part.getName().equals(theName)) { return part; @@ -236,7 +240,7 @@ protected List searchAndReturnUnqualifiedVersionlessIdValues(String uri) Bundle bundle = myFhirContext.newXmlParser().parseResource(Bundle.class, resp); ids = toUnqualifiedVersionlessIdValues(bundle); ourLog.debug("Observation: \n" - + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle)); + + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle)); } return ids; From 6da071c425f3d4fdb2595589e79c78e7df450669 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 13 Dec 2024 18:00:14 -0500 Subject: [PATCH 083/148] starting work on new docs --- .../provider/BaseResourceProviderR4Test.java | 118 +++++++++--------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java index 4d53a80388ba..a2fafb952be4 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java @@ -76,60 +76,60 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { @RegisterExtension protected RestfulServerConfigurerExtension myServerConfigurer = new RestfulServerConfigurerExtension(() -> myServer) - .withServerBeforeAll(s -> { - s.registerProviders(myResourceProviders.createProviders()); - s.setDefaultResponseEncoding(EncodingEnum.XML); - s.setDefaultPrettyPrint(false); - - myFhirContext.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator()); - - s.registerProvider(mySystemProvider); - s.registerProvider(myBinaryAccessProvider); - s.registerProvider(myAppCtx.getBean(BulkDataExportProvider.class)); - s.registerProvider(myAppCtx.getBean(DeleteExpungeProvider.class)); - s.registerProvider(myAppCtx.getBean(DiffProvider.class)); - s.registerProvider(myAppCtx.getBean(GraphQLProvider.class)); - s.registerProvider(myAppCtx.getBean(ProcessMessageProvider.class)); - s.registerProvider(myAppCtx.getBean(ReindexProvider.class)); - s.registerProvider(myAppCtx.getBean(SubscriptionTriggeringProvider.class)); - s.registerProvider(myAppCtx.getBean(TerminologyUploaderProvider.class)); - s.registerProvider(myAppCtx.getBean(ValueSetOperationProvider.class)); - - s.setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class)); - - JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider( - s, mySystemDao, myStorageSettings, mySearchParamRegistry, myValidationSupport); - confProvider.setImplementationDescription("THIS IS THE DESC"); - s.setServerConformanceProvider(confProvider); - - // Register a CORS filter - CorsConfiguration config = new CorsConfiguration(); - CorsInterceptor corsInterceptor = new CorsInterceptor(config); - config.addAllowedHeader("Accept"); - config.addAllowedHeader("Access-Control-Request-Headers"); - config.addAllowedHeader("Access-Control-Request-Method"); - config.addAllowedHeader("Cache-Control"); - config.addAllowedHeader("Content-Type"); - config.addAllowedHeader("Origin"); - config.addAllowedHeader("Prefer"); - config.addAllowedHeader("x-fhir-starter"); - config.addAllowedHeader("X-Requested-With"); - config.addAllowedOrigin("*"); - config.addExposedHeader("Location"); - config.addExposedHeader("Content-Location"); - config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); - s.registerInterceptor(corsInterceptor); - }) - .withServerBeforeEach(s -> { - myPort = myServer.getPort(); - myServerBase = myServer.getBaseUrl(); - myClient = myServer.getFhirClient(); - - myClient.getInterceptorService().unregisterInterceptorsIf(t -> t instanceof LoggingInterceptor); - if (shouldLogClient()) { - myClient.registerInterceptor(new LoggingInterceptor(verboseClientLogging())); - } - }); + .withServerBeforeAll(s -> { + s.registerProviders(myResourceProviders.createProviders()); + s.setDefaultResponseEncoding(EncodingEnum.XML); + s.setDefaultPrettyPrint(false); + + myFhirContext.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator()); + + s.registerProvider(mySystemProvider); + s.registerProvider(myBinaryAccessProvider); + s.registerProvider(myAppCtx.getBean(BulkDataExportProvider.class)); + s.registerProvider(myAppCtx.getBean(DeleteExpungeProvider.class)); + s.registerProvider(myAppCtx.getBean(DiffProvider.class)); + s.registerProvider(myAppCtx.getBean(GraphQLProvider.class)); + s.registerProvider(myAppCtx.getBean(ProcessMessageProvider.class)); + s.registerProvider(myAppCtx.getBean(ReindexProvider.class)); + s.registerProvider(myAppCtx.getBean(SubscriptionTriggeringProvider.class)); + s.registerProvider(myAppCtx.getBean(TerminologyUploaderProvider.class)); + s.registerProvider(myAppCtx.getBean(ValueSetOperationProvider.class)); + + s.setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class)); + + JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider( + s, mySystemDao, myStorageSettings, mySearchParamRegistry, myValidationSupport); + confProvider.setImplementationDescription("THIS IS THE DESC"); + s.setServerConformanceProvider(confProvider); + + // Register a CORS filter + CorsConfiguration config = new CorsConfiguration(); + CorsInterceptor corsInterceptor = new CorsInterceptor(config); + config.addAllowedHeader("Accept"); + config.addAllowedHeader("Access-Control-Request-Headers"); + config.addAllowedHeader("Access-Control-Request-Method"); + config.addAllowedHeader("Cache-Control"); + config.addAllowedHeader("Content-Type"); + config.addAllowedHeader("Origin"); + config.addAllowedHeader("Prefer"); + config.addAllowedHeader("x-fhir-starter"); + config.addAllowedHeader("X-Requested-With"); + config.addAllowedOrigin("*"); + config.addExposedHeader("Location"); + config.addExposedHeader("Content-Location"); + config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + s.registerInterceptor(corsInterceptor); + }) + .withServerBeforeEach(s -> { + myPort = myServer.getPort(); + myServerBase = myServer.getBaseUrl(); + myClient = myServer.getFhirClient(); + + myClient.getInterceptorService().unregisterInterceptorsIf(t -> t instanceof LoggingInterceptor); + if (shouldLogClient()) { + myClient.registerInterceptor(new LoggingInterceptor(verboseClientLogging())); + } + }); @Autowired protected SubscriptionLoader mySubscriptionLoader; @@ -166,9 +166,9 @@ protected List toNameList(Bundle resp) { for (BundleEntryComponent next : resp.getEntry()) { Patient nextPt = (Patient) next.getResource(); String nextStr = nextPt.getName().size() > 0 - ? nextPt.getName().get(0).getGivenAsSingleString() + " " - + nextPt.getName().get(0).getFamily() - : ""; + ? nextPt.getName().get(0).getGivenAsSingleString() + " " + + nextPt.getName().get(0).getFamily() + : ""; if (isNotBlank(nextStr)) { names.add(nextStr); } @@ -210,7 +210,7 @@ public static List getParametersByName(Parameters } public static ParametersParameterComponent getPartByName( - ParametersParameterComponent theParameter, String theName) { + ParametersParameterComponent theParameter, String theName) { for (ParametersParameterComponent part : theParameter.getPart()) { if (part.getName().equals(theName)) { return part; @@ -240,7 +240,7 @@ protected List searchAndReturnUnqualifiedVersionlessIdValues(String uri) Bundle bundle = myFhirContext.newXmlParser().parseResource(Bundle.class, resp); ids = toUnqualifiedVersionlessIdValues(bundle); ourLog.debug("Observation: \n" - + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle)); + + myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle)); } return ids; From 45ccd7187f7d93a467f6cacfa1a5c11aa2004121 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Sun, 15 Dec 2024 10:49:03 -0500 Subject: [PATCH 084/148] document $replace-references --- .../uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java | 5 +++++ .../src/test/resources/logback-test.xml | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index 56d4e2f4ad0b..0ea5ee2cfad7 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -136,4 +136,9 @@ void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { myTestHelper.assertAllReferencesUpdated(); } + + @Override + protected boolean verboseClientLogging() { + return true; + } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml b/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml index d7283de32e54..00cc938dde93 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml @@ -17,7 +17,13 @@ + + + + + + From 2b22bd30cb4a4c10cdad195aef02fac440f6637f Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Sun, 15 Dec 2024 10:59:08 -0500 Subject: [PATCH 085/148] separate default from max --- .../BaseJpaResourceProviderPatient.java | 8 +-- .../fhir/jpa/provider/JpaSystemProvider.java | 6 +- .../jpa/api/config/JpaStorageSettings.java | 63 ++++++++++++++++++- 3 files changed, 65 insertions(+), 12 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index e8f399e2fbae..e7d709811b17 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -71,7 +71,6 @@ import java.util.stream.Collectors; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; import static org.apache.commons.lang3.StringUtils.isNotBlank; public abstract class BaseJpaResourceProviderPatient extends BaseJpaResourceProvider { @@ -309,11 +308,8 @@ public IBaseParameters patientMerge( IPrimitiveType theBatchSize) { startRequest(theServletRequest); - int batchSize = defaultIfNull( - IPrimitiveType.toValueOrNull(theBatchSize), myStorageSettings.getMaxTransactionEntriesForWrite()); - if (batchSize > myStorageSettings.getMaxTransactionEntriesForWrite()) { - batchSize = myStorageSettings.getMaxTransactionEntriesForWrite(); - } + + int batchSize = myStorageSettings.getTransactionWriteBatchSizeFromOperationParameter(theBatchSize); try { MergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index faf90ec266c8..5a41aafd319f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -169,11 +169,7 @@ public IBaseParameters replaceReferences( IPrimitiveType theBatchSize, RequestDetails theRequestDetails) { validateReplaceReferencesParams(theSourceId, theTargetId); - int batchSize = defaultIfNull( - IPrimitiveType.toValueOrNull(theBatchSize), myStorageSettings.getMaxTransactionEntriesForWrite()); - if (batchSize > myStorageSettings.getMaxTransactionEntriesForWrite()) { - batchSize = myStorageSettings.getMaxTransactionEntriesForWrite(); - } + int batchSize = myStorageSettings.getTransactionWriteBatchSizeFromOperationParameter(theBatchSize); IdDt sourceId = new IdDt(theSourceId); IdDt targetId = new IdDt(theTargetId); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java index a9d88561cefa..d1e3031683fa 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java @@ -36,6 +36,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.time.DateUtils; +import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Bundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,6 +48,8 @@ import java.util.Set; import java.util.TreeSet; +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; + @SuppressWarnings("JavadocLinkAsPlainText") public class JpaStorageSettings extends StorageSettings { private static final Logger ourLog = LoggerFactory.getLogger(JpaStorageSettings.class); @@ -117,10 +120,24 @@ public class JpaStorageSettings extends StorageSettings { private static final boolean DEFAULT_PREVENT_INVALIDATING_CONDITIONAL_MATCH_CRITERIA = false; private static final long DEFAULT_REST_DELETE_BY_URL_RESOURCE_ID_THRESHOLD = 10000; - public static final String DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING = "512"; + /** + * If we are batching write operations in transactions, what should the maximum number of write operations per + * transaction be? + * @since 7.8.0 + */ + public static final String DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING = "10000"; public static final int DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE = Integer.parseInt(DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING); + /** + * If we are batching write operations in transactions, what should the default number of write operations per + * transaction be? + * @since 7.8.0 + */ + public static final String DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE_STRING = "512"; + public static final int DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE = + Integer.parseInt(DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE_STRING); + /** * Do not change default of {@code 0}! * @@ -396,8 +413,20 @@ public class JpaStorageSettings extends StorageSettings { @Beta private boolean myIncludeHashIdentityForTokenSearches = false; + /** + * If we are batching write operations in transactions, what should the maximum number of write operations per + * transaction be? + * @since 7.8.0 + */ private int myMaxTransactionEntriesForWrite = DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE; + /** + * If we are batching write operations in transactions, what should the default number of write operations per + * transaction be? + * @since 7.8.0 + */ + private int myDefaultTransactionEntriesForWrite = DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE; + /** * Constructor */ @@ -2656,10 +2685,13 @@ public void setRestDeleteByUrlResourceIdThreshold(long theRestDeleteByUrlResourc myRestDeleteByUrlResourceIdThreshold = theRestDeleteByUrlResourceIdThreshold; } + /** * If we are batching write operations in transactions, what should the maximum number of write operations per * transaction be? + * @since 7.8.0 */ + public int getMaxTransactionEntriesForWrite() { return myMaxTransactionEntriesForWrite; } @@ -2667,11 +2699,40 @@ public int getMaxTransactionEntriesForWrite() { /** * If we are batching write operations in transactions, what should the maximum number of write operations per * transaction be? + * @since 7.8.0 */ + public void setMaxTransactionEntriesForWrite(int theMaxTransactionEntriesForWrite) { myMaxTransactionEntriesForWrite = theMaxTransactionEntriesForWrite; } + /** + * If we are batching write operations in transactions, what should the default number of write operations per + * transaction be? + * @since 7.8.0 + */ + public int getDefaultTransactionEntriesForWrite() { + return myDefaultTransactionEntriesForWrite; + } + + /** + * If we are batching write operations in transactions, what should the default number of write operations per + * transaction be? + * @since 7.8.0 + */ + public void setDefaultTransactionEntriesForWrite(int theDefaultTransactionEntriesForWrite) { + myDefaultTransactionEntriesForWrite = theDefaultTransactionEntriesForWrite; + } + + public int getTransactionWriteBatchSizeFromOperationParameter(IPrimitiveType theBatchSize) { + int retval = defaultIfNull(IPrimitiveType.toValueOrNull(theBatchSize), getDefaultTransactionEntriesForWrite()); + if (retval > getMaxTransactionEntriesForWrite()) { + retval = getMaxTransactionEntriesForWrite(); + } + return retval; + } + + public enum StoreMetaSourceInformationEnum { NONE(false, false), SOURCE_URI(true, false), From 907cf8d395dfd1cecb112eb213a6e0bdad1a7332 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Sun, 15 Dec 2024 11:30:24 -0500 Subject: [PATCH 086/148] fixme --- .../fhir/jpa/provider/JpaSystemProvider.java | 11 +++++-- .../jpa/provider/r4/PatientMergeR4Test.java | 8 ++++- .../provider/r4/ReplaceReferencesR4Test.java | 5 +++- .../provider/BaseResourceProviderR4Test.java | 30 +++++++++++++++++++ .../jpa/api/config/JpaStorageSettings.java | 8 ++--- 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index 5a41aafd319f..e178c1169413 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -36,6 +36,7 @@ import ca.uhn.fhir.rest.server.provider.ProviderConstants; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ParametersUtil; +import jakarta.servlet.http.HttpServletResponse; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -47,6 +48,7 @@ import java.util.Map; import java.util.TreeMap; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; @@ -167,7 +169,7 @@ public IBaseParameters replaceReferences( String theTargetId, @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_BATCH_SIZE, typeName = "unsignedInt") IPrimitiveType theBatchSize, - RequestDetails theRequestDetails) { + ServletRequestDetails theRequestDetails) { validateReplaceReferencesParams(theSourceId, theTargetId); int batchSize = myStorageSettings.getTransactionWriteBatchSizeFromOperationParameter(theBatchSize); @@ -177,7 +179,12 @@ public IBaseParameters replaceReferences( theRequestDetails, ReadPartitionIdRequestDetails.forRead(targetId)); ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest(sourceId, targetId, batchSize, partitionId); - return getReplaceReferencesSvc().replaceReferences(replaceReferenceRequest, theRequestDetails); + IBaseParameters retval = getReplaceReferencesSvc().replaceReferences(replaceReferenceRequest, theRequestDetails); + if (ParametersUtil.getNamedParameter(getContext(), retval, OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).isPresent()) { + HttpServletResponse response = theRequestDetails.getServletResponse(); + response.setStatus(HttpServletResponse.SC_ACCEPTED); + } + return retval; } private static void validateReplaceReferencesParams(String theSourceId, String theTargetId) { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 52074a184ba7..6b7555471f8c 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -11,6 +11,7 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import jakarta.annotation.Nonnull; +import jakarta.servlet.http.HttpServletResponse; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Identifier; @@ -36,6 +37,7 @@ import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER_RESPOND_ASYNC; +import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_202_ACCEPTED; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME; @@ -151,7 +153,8 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea // Assert Task inAsync mode, unless it is preview in which case we don't return a task if (isAsync && !withPreview) { - // FIXME KHS assert we got back a 202 Accepted + // FIXME ED see ProviderConstants.OPERATION_REPLACE_REFERENCES in JpaSystemProvider for how to get this to pass + assertThat(getLastHttpStatusCode()).isEqualTo(HttpServletResponse.SC_ACCEPTED); Task task = (Task) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource(); assertNull(task.getIdElement().getVersionIdPart()); @@ -225,6 +228,9 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea } } + // FIXME ED test case where another resource that links to source was added while the batch was running + // so the source can't be deleted + @ParameterizedTest @CsvSource({ // withDelete, withInputResultPatient, withPreview diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index 0ea5ee2cfad7..0b7d8a7c38a9 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -3,6 +3,7 @@ import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper; +import jakarta.servlet.http.HttpServletResponse; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Identifier; @@ -19,6 +20,7 @@ import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; import static ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper.EXPECTED_SMALL_BATCHES; +import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_202_ACCEPTED; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.HAPI_BATCH_JOB_ID_SYSTEM; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; @@ -49,7 +51,7 @@ void testReplaceReferences(boolean isAsync) throws IOException { Bundle patchResultBundle; if (isAsync) { - // FIXME KHS assert we got back a 202 Accepted + assertThat(getLastHttpStatusCode()).isEqualTo(HttpServletResponse.SC_ACCEPTED); Task task = (Task) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).getResource(); assertNull(task.getIdElement().getVersionIdPart()); @@ -89,6 +91,7 @@ void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { // exec Parameters outParams = myTestHelper.callReplaceReferencesWithBatchSize(myClient, isAsync, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); + assertThat(getLastHttpStatusCode()).isEqualTo(HttpServletResponse.SC_ACCEPTED); assertThat(outParams.getParameter()).hasSize(1); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java index a2fafb952be4..49d9c1df686f 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java @@ -31,7 +31,10 @@ import ca.uhn.fhir.jpa.util.ResourceCountCache; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.client.api.IClientInterceptor; import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.api.IHttpRequest; +import ca.uhn.fhir.rest.client.api.IHttpResponse; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor; @@ -74,6 +77,8 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { @RegisterExtension protected RestfulServerExtension myServer; + private MyHttpCodeClientIntercepter myLastHttpResponseCodeCapture = new MyHttpCodeClientIntercepter(); + @RegisterExtension protected RestfulServerConfigurerExtension myServerConfigurer = new RestfulServerConfigurerExtension(() -> myServer) .withServerBeforeAll(s -> { @@ -129,6 +134,8 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { if (shouldLogClient()) { myClient.registerInterceptor(new LoggingInterceptor(verboseClientLogging())); } + + myClient.registerInterceptor(myLastHttpResponseCodeCapture); }); @Autowired @@ -176,6 +183,10 @@ protected List toNameList(Bundle resp) { return names; } + protected int getLastHttpStatusCode() { + return myLastHttpResponseCodeCapture.getLastHttpStatusCode(); + } + public static int getNumberOfParametersByName(Parameters theParameters, String theName) { int retVal = 0; @@ -245,4 +256,23 @@ protected List searchAndReturnUnqualifiedVersionlessIdValues(String uri) return ids; } + + private class MyHttpCodeClientIntercepter implements IClientInterceptor { + + private int myLastHttpStatusCode; + + @Override + public void interceptRequest(IHttpRequest theRequest) { + + } + + @Override + public void interceptResponse(IHttpResponse theResponse) throws IOException { + myLastHttpStatusCode = theResponse.getStatus(); + } + + public int getLastHttpStatusCode() { + return myLastHttpStatusCode; + } + } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java index d1e3031683fa..9c967fd43f27 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java @@ -126,6 +126,7 @@ public class JpaStorageSettings extends StorageSettings { * @since 7.8.0 */ public static final String DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING = "10000"; + public static final int DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE = Integer.parseInt(DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING); @@ -135,8 +136,9 @@ public class JpaStorageSettings extends StorageSettings { * @since 7.8.0 */ public static final String DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE_STRING = "512"; + public static final int DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE = - Integer.parseInt(DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE_STRING); + Integer.parseInt(DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE_STRING); /** * Do not change default of {@code 0}! @@ -2685,13 +2687,11 @@ public void setRestDeleteByUrlResourceIdThreshold(long theRestDeleteByUrlResourc myRestDeleteByUrlResourceIdThreshold = theRestDeleteByUrlResourceIdThreshold; } - /** * If we are batching write operations in transactions, what should the maximum number of write operations per * transaction be? * @since 7.8.0 */ - public int getMaxTransactionEntriesForWrite() { return myMaxTransactionEntriesForWrite; } @@ -2701,7 +2701,6 @@ public int getMaxTransactionEntriesForWrite() { * transaction be? * @since 7.8.0 */ - public void setMaxTransactionEntriesForWrite(int theMaxTransactionEntriesForWrite) { myMaxTransactionEntriesForWrite = theMaxTransactionEntriesForWrite; } @@ -2732,7 +2731,6 @@ public int getTransactionWriteBatchSizeFromOperationParameter(IPrimitiveType Date: Sun, 15 Dec 2024 11:43:20 -0500 Subject: [PATCH 087/148] moar fixme --- .../uhn/fhir/jpa/provider/JpaSystemProvider.java | 6 ++++-- .../fhir/jpa/provider/merge/MergeBatchTest.java | 8 +------- .../fhir/jpa/provider/r4/PatientMergeR4Test.java | 1 - .../jpa/provider/r4/ReplaceReferencesR4Test.java | 11 +++++------ .../ReplaceReferencesBatchTest.java | 8 +------- .../jpa/provider/BaseResourceProviderR4Test.java | 4 +--- .../ReplaceReferencesTestHelper.java | 15 ++++++++++++++- .../ReplaceReferenceRequest.java | 9 --------- 8 files changed, 26 insertions(+), 36 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index e178c1169413..e8d2ff408b4e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -179,8 +179,10 @@ public IBaseParameters replaceReferences( theRequestDetails, ReadPartitionIdRequestDetails.forRead(targetId)); ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest(sourceId, targetId, batchSize, partitionId); - IBaseParameters retval = getReplaceReferencesSvc().replaceReferences(replaceReferenceRequest, theRequestDetails); - if (ParametersUtil.getNamedParameter(getContext(), retval, OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK).isPresent()) { + IBaseParameters retval = + getReplaceReferencesSvc().replaceReferences(replaceReferenceRequest, theRequestDetails); + if (ParametersUtil.getNamedParameter(getContext(), retval, OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) + .isPresent()) { HttpServletResponse response = theRequestDetails.getServletResponse(); response.setStatus(HttpServletResponse.SC_ACCEPTED); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java index 51f0d2f4d24a..af7bf4251663 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java @@ -65,13 +65,7 @@ public void testHappyPath() { Batch2JobStartResponse jobStartResponse = myJobCoordinator.startInstance(mySrd, request); JobInstance jobInstance = myBatch2JobHelper.awaitJobCompletion(jobStartResponse); - // FIXME KHS assert outcome - String report = jobInstance.getReport(); - ReplaceReferenceResultsJson replaceReferenceResultsJson = JsonUtil.deserialize(report, ReplaceReferenceResultsJson.class); - IdDt resultTaskId = replaceReferenceResultsJson.getTaskId().asIdDt(); - assertEquals(taskId.getIdPart(), resultTaskId.getIdPart()); - - Bundle patchResultBundle = myTestHelper.validateCompletedTask(taskId); + Bundle patchResultBundle = myTestHelper.validateCompletedTask(jobInstance, taskId); myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of("Observation", "Encounter", "CarePlan")); // FIXME ED validate other steps performed by final run merge step diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 6b7555471f8c..fd7c42b2c9ca 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -167,7 +167,6 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); - // FIXME KHS the rest of these asserts will likely need to be tweaked Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); // Assert on the output type diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index 0b7d8a7c38a9..86260afc8ceb 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -11,6 +11,7 @@ import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.Task; +import org.hl7.fhir.r4.model.Type; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -57,11 +58,9 @@ void testReplaceReferences(boolean isAsync) throws IOException { assertNull(task.getIdElement().getVersionIdPart()); ourLog.info("Got task {}", task.getId()); - awaitJobCompletion(task); + JobInstance jobInstance = awaitJobCompletion(task); -// FIXME KHS verify report - - patchResultBundle = myTestHelper.validateCompletedTask(task.getIdElement()); + patchResultBundle = myTestHelper.validateCompletedTask(jobInstance, task.getIdElement()); } else { patchResultBundle = (Bundle) outParams.getParameter(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME).getResource(); } @@ -74,14 +73,14 @@ void testReplaceReferences(boolean isAsync) throws IOException { myTestHelper.assertAllReferencesUpdated(); } - private void awaitJobCompletion(Task task) { + private JobInstance awaitJobCompletion(Task task) { assertThat(task.getIdentifier()).hasSize(1) .element(0) .extracting(Identifier::getSystem) .isEqualTo(HAPI_BATCH_JOB_ID_SYSTEM); String jobId = task.getIdentifierFirstRep().getValue(); - JobInstance jobInstance = myBatch2JobHelper.awaitJobCompletion(jobId); + return myBatch2JobHelper.awaitJobCompletion(jobId); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java index 684789952c92..3113ceec25c3 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java @@ -63,13 +63,7 @@ public void testHappyPath() { Batch2JobStartResponse jobStartResponse = myJobCoordinator.startInstance(mySrd, request); JobInstance jobInstance = myBatch2JobHelper.awaitJobCompletion(jobStartResponse); - // FIXME KHS assert outcome - String report = jobInstance.getReport(); - ReplaceReferenceResultsJson replaceReferenceResultsJson = JsonUtil.deserialize(report, ReplaceReferenceResultsJson.class); - IdDt resultTaskId = replaceReferenceResultsJson.getTaskId().asIdDt(); - assertEquals(taskId.getIdPart(), resultTaskId.getIdPart()); - - Bundle patchResultBundle = myTestHelper.validateCompletedTask(taskId); + Bundle patchResultBundle = myTestHelper.validateCompletedTask(jobInstance, taskId); myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of("Observation", "Encounter", "CarePlan")); myTestHelper.assertAllReferencesUpdated(); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java index 49d9c1df686f..81a0dabc77ee 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java @@ -262,9 +262,7 @@ private class MyHttpCodeClientIntercepter implements IClientInterceptor { private int myLastHttpStatusCode; @Override - public void interceptRequest(IHttpRequest theRequest) { - - } + public void interceptRequest(IHttpRequest theRequest) {} @Override public void interceptResponse(IHttpResponse theResponse) throws IOException { diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index db9186ab5c6a..8e0d5d968d9e 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -1,15 +1,19 @@ package ca.uhn.fhir.jpa.replacereferences; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceResultsJson; +import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 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.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.server.provider.ProviderConstants; +import ca.uhn.fhir.util.JsonUtil; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.BooleanType; @@ -355,7 +359,9 @@ public static void validatePatchResultBundle( diagnostics -> assertThat(diagnostics).matches(expectedPatchIssuePattern)))); } - public Bundle validateCompletedTask(IIdType theTaskId) { + public Bundle validateCompletedTask(JobInstance theJobInstance, IIdType theTaskId) { + validateJobReport(theJobInstance, theTaskId); + Bundle patchResultBundle; Task taskWithOutput = myTaskDao.read(theTaskId, mySrd); assertThat(taskWithOutput.getStatus()).isEqualTo(Task.TaskStatus.COMPLETED); @@ -384,4 +390,11 @@ public Bundle validateCompletedTask(IIdType theTaskId) { assertTrue(containedBundle.equalsDeep(patchResultBundle)); return patchResultBundle; } + + private void validateJobReport(JobInstance theJobInstance, IIdType theTaskId) { + String report = theJobInstance.getReport(); + ReplaceReferenceResultsJson replaceReferenceResultsJson = JsonUtil.deserialize(report, ReplaceReferenceResultsJson.class); + IdDt resultTaskId = replaceReferenceResultsJson.getTaskId().asIdDt(); + assertEquals(theTaskId.getIdPart(), resultTaskId.getIdPart()); + } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java index d409275ece16..6cbc4b97f6ff 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java @@ -77,15 +77,6 @@ public void validateOrThrowInvalidParameterException() { } } - // FIXME KHS remove - public SearchParameterMap getSearchParameterMap() { - SearchParameterMap retval = SearchParameterMap.newSynchronous(); - retval.add(PARAM_ID, new StringParam(sourceId.getValue())); - retval.addRevInclude(new Include("*")); - // Note we do not set the count since we will be streaming - return retval; - } - public boolean isForceSync() { return myForceSync; } From cd92d11283b68dd818072675b7498fddd400a00f Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Sun, 15 Dec 2024 11:43:42 -0500 Subject: [PATCH 088/148] moar fixme --- .../jpa/replacereferences/ReplaceReferencesTestHelper.java | 3 ++- .../uhn/fhir/replacereferences/ReplaceReferenceRequest.java | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index 8e0d5d968d9e..833b0e36f819 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -393,7 +393,8 @@ public Bundle validateCompletedTask(JobInstance theJobInstance, IIdType theTaskI private void validateJobReport(JobInstance theJobInstance, IIdType theTaskId) { String report = theJobInstance.getReport(); - ReplaceReferenceResultsJson replaceReferenceResultsJson = JsonUtil.deserialize(report, ReplaceReferenceResultsJson.class); + ReplaceReferenceResultsJson replaceReferenceResultsJson = + JsonUtil.deserialize(report, ReplaceReferenceResultsJson.class); IdDt resultTaskId = replaceReferenceResultsJson.getTaskId().asIdDt(); assertEquals(theTaskId.getIdPart(), resultTaskId.getIdPart()); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java index 6cbc4b97f6ff..2b444179361a 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java @@ -21,15 +21,11 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.model.RequestPartitionId; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.model.api.Include; -import ca.uhn.fhir.rest.param.StringParam; import jakarta.annotation.Nonnull; import org.hl7.fhir.instance.model.api.IIdType; import java.security.InvalidParameterException; -import static ca.uhn.fhir.rest.api.Constants.PARAM_ID; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID; import static org.apache.commons.lang3.StringUtils.isBlank; From 9203a8326111ea17a032198ddd16a98c06ddc7b5 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Sun, 15 Dec 2024 11:44:31 -0500 Subject: [PATCH 089/148] ken last fixme --- .../java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index fd7c42b2c9ca..d538a4ddf986 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -346,8 +346,6 @@ void test_MissingRequiredParameters_Returns400BadRequest() { .isEqualTo(400); } - // FIXME KHS look at PatientEverythingR4Test for ideas for other tests - class MyExceptionHandler implements TestExecutionExceptionHandler { @Override public void handleTestExecutionException(ExtensionContext theExtensionContext, Throwable theThrowable) throws Throwable { From 7492498a63258a04ddbe06425b38ca89af4f3f16 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Mon, 16 Dec 2024 11:19:37 -0500 Subject: [PATCH 090/148] return 202 status on async merge, update MergeBatchTest to do more validations --- .../provider/merge/ResourceMergeService.java | 2 + .../jpa/provider/merge/MergeBatchTest.java | 35 ++++++--- .../jpa/provider/r4/PatientMergeR4Test.java | 66 ++-------------- .../ReplaceReferencesTestHelper.java | 75 ++++++++++++++++--- .../fhir/batch2/jobs/merge/MergeHelper.java | 2 +- 5 files changed, 95 insertions(+), 85 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 077745dc511e..93ea148d4964 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -61,6 +61,7 @@ import static ca.uhn.fhir.batch2.jobs.merge.MergeAppCtx.JOB_MERGE; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_200_OK; +import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_202_ACCEPTED; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_400_BAD_REQUEST; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_500_INTERNAL_ERROR; @@ -324,6 +325,7 @@ private void doMergeAsync( task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); task.getMeta().setVersionId(null); theMergeOutcome.setTask(task); + theMergeOutcome.setHttpStatusCode(STATUS_HTTP_202_ACCEPTED); } private boolean validateResultResourceIfExists( diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java index af7bf4251663..08abef8dd4f8 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java @@ -3,7 +3,6 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; import ca.uhn.fhir.batch2.jobs.merge.MergeJobParameters; -import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceResultsJson; import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; import ca.uhn.fhir.interceptor.model.RequestPartitionId; @@ -12,20 +11,18 @@ import ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper; import ca.uhn.fhir.jpa.test.BaseJpaR4Test; import ca.uhn.fhir.jpa.test.Batch2JobHelper; -import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; -import ca.uhn.fhir.util.JsonUtil; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Task; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; import static ca.uhn.fhir.batch2.jobs.merge.MergeAppCtx.JOB_MERGE; -import static org.junit.jupiter.api.Assertions.assertEquals; public class MergeBatchTest extends BaseJpaR4Test { @@ -51,29 +48,43 @@ public void before() throws Exception { mySrd.setRequestPartitionId(RequestPartitionId.allPartitions()); } - @Test - public void testHappyPath() { - IIdType taskId = createReplaceReferencesTask(); + @ParameterizedTest + @CsvSource({ + "true,true", + "false,true", + "true,false", + "false,false" + }) + public void testHappyPath(boolean theDeleteSource, boolean theWithResultResource) { + IIdType taskId = createTask(); MergeJobParameters jobParams = new MergeJobParameters(); jobParams.setSourceId(new FhirIdJson(myTestHelper.getSourcePatientId())); jobParams.setTargetId(new FhirIdJson(myTestHelper.getTargetPatientId())); jobParams.setTaskId(taskId); - // FIXME ED add to parameters + jobParams.setDeleteSource(theDeleteSource); + if (theWithResultResource) { + String encodedResultPatient = myFhirContext.newJsonParser().encodeResourceToString(myTestHelper.createResultPatient(theDeleteSource)); + jobParams.setResultResource(encodedResultPatient); + } JobInstanceStartRequest request = new JobInstanceStartRequest(JOB_MERGE, jobParams); Batch2JobStartResponse jobStartResponse = myJobCoordinator.startInstance(mySrd, request); JobInstance jobInstance = myBatch2JobHelper.awaitJobCompletion(jobStartResponse); Bundle patchResultBundle = myTestHelper.validateCompletedTask(jobInstance, taskId); - myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of("Observation", "Encounter", "CarePlan")); + myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, + List.of( + "Observation", "Encounter", "CarePlan")); - // FIXME ED validate other steps performed by final run merge step myTestHelper.assertAllReferencesUpdated(); + myTestHelper.assertSourcePatientUpdatedOrDeleted(theDeleteSource); + myTestHelper.assertTargetPatientUpdated(theDeleteSource, + myTestHelper.getExpectedIdentifiersForTargetAfterMerge(theWithResultResource)); } - private IIdType createReplaceReferencesTask() { + private IIdType createTask() { Task task = new Task(); task.setStatus(Task.TaskStatus.INPROGRESS); return myTaskDao.create(task, mySrd).getId().toUnqualifiedVersionless(); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index d538a4ddf986..3fdca0c9f281 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -8,7 +8,6 @@ import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInput; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import jakarta.annotation.Nonnull; import jakarta.servlet.http.HttpServletResponse; @@ -37,7 +36,6 @@ import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER_RESPOND_ASYNC; -import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_202_ACCEPTED; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME; @@ -49,7 +47,6 @@ import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; public class PatientMergeR4Test extends BaseResourceProviderR4Test { @@ -111,7 +108,7 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea myTestHelper.setSourceAndTarget(inParams); inParams.deleteSource = withDelete; if (withInputResultPatient) { - myTestHelper.setResultPatient(inParams, withDelete); + inParams.resultPatient = myTestHelper.createResultPatient(withDelete); } if (withPreview) { inParams.preview = true; @@ -137,19 +134,8 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea assertTrue(input.equalsDeep(inParameters)); - List expectedIdentifiersOnTargetAfterMerge = null; - if (withInputResultPatient) { - expectedIdentifiersOnTargetAfterMerge = List.of(new Identifier().setSystem("SYS1A").setValue("VAL1A")); - } else { - //the identifiers copied over from source should be marked as old - expectedIdentifiersOnTargetAfterMerge = List.of( - new Identifier().setSystem("SYS2A").setValue("VAL2A"), - new Identifier().setSystem("SYS2B").setValue("VAL2B"), - new Identifier().setSystem("SYSC").setValue("VALC"), - new Identifier().setSystem("SYS1A").setValue("VAL1A").copy().setUse(Identifier.IdentifierUse.OLD), - new Identifier().setSystem("SYS1B").setValue("VAL1B").copy().setUse(Identifier.IdentifierUse.OLD) - ); - } + List expectedIdentifiersOnTargetAfterMerge = + myTestHelper.getExpectedIdentifiersForTargetAfterMerge(withInputResultPatient); // Assert Task inAsync mode, unless it is preview in which case we don't return a task if (isAsync && !withPreview) { @@ -212,18 +198,16 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea // Assert Merged Patient Patient mergedPatient = (Patient) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_RESULT).getResource(); List identifiers = mergedPatient.getIdentifier(); - assertIdentifiers(identifiers, expectedIdentifiersOnTargetAfterMerge); + myTestHelper.assertIdentifiers(identifiers, expectedIdentifiersOnTargetAfterMerge); } // Check that the linked resources were updated - - if (withPreview) { myTestHelper.assertNothingChanged(); } else { myTestHelper.assertAllReferencesUpdated(withDelete); - assertSourcePatientUpdatedOrDeleted(withDelete); - assertTargetPatientUpdated(withDelete, expectedIdentifiersOnTargetAfterMerge); + myTestHelper.assertSourcePatientUpdatedOrDeleted(withDelete); + myTestHelper.assertTargetPatientUpdated(withDelete, expectedIdentifiersOnTargetAfterMerge); } } @@ -271,44 +255,6 @@ public void testMultipleSourceMatchesFails(boolean withDelete, boolean withInput assertUnprocessibleEntityWithMessage(inParameters, "Multiple resources found matching the identifier(s) specified in 'source-patient-identifier'"); } - - private void assertSourcePatientUpdatedOrDeleted(boolean withDelete) { - if (withDelete) { - // the spec says the deleted resource should return 404 but we seem to return 410 Gone - assertThrows(ResourceGoneException.class, () -> myTestHelper.readSourcePatient()); - } - else { - Patient source = myTestHelper.readSourcePatient(); - assertThat(source.getLink()).hasSize(1); - Patient.PatientLinkComponent link = source.getLink().get(0); - assertThat(link.getOther().getReferenceElement()).isEqualTo(myTestHelper.getTargetPatientId()); - assertThat(link.getType()).isEqualTo(Patient.LinkType.REPLACEDBY); - } - - } - - private void assertTargetPatientUpdated(boolean withDelete, List theExpectedIdentifiers) { - Patient target = myTestHelper.readTargetPatient(); - if (!withDelete) { - assertThat(target.getLink()).hasSize(1); - Patient.PatientLinkComponent link = target.getLink().get(0); - assertThat(link.getOther().getReferenceElement()).isEqualTo(myTestHelper.getSourcePatientId()); - assertThat(link.getType()).isEqualTo(Patient.LinkType.REPLACES); - } - //assertExpected Identifiers found on the target - assertIdentifiers(target.getIdentifier(), theExpectedIdentifiers); - } - - private void assertIdentifiers(List theActualIdentifiers, List theExpectedIdentifiers) { - assertThat(theActualIdentifiers).hasSize(theExpectedIdentifiers.size()); - for (int i = 0; i < theExpectedIdentifiers.size(); i++) { - Identifier expectedIdentifier = theExpectedIdentifiers.get(i); - Identifier actualIdentifier = theActualIdentifiers.get(i); - assertThat(expectedIdentifier.equalsDeep(actualIdentifier)).isTrue(); - } - } - - private void assertUnprocessibleEntityWithMessage(Parameters inParameters, String theExpectedMessage) { assertThatThrownBy(() -> callMergeOperation(inParameters)) diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index 833b0e36f819..9e0ebba00020 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -12,6 +12,7 @@ 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.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import ca.uhn.fhir.util.JsonUtil; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -50,6 +51,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; public class ReplaceReferencesTestHelper { @@ -83,7 +85,6 @@ public class ReplaceReferencesTestHelper { private ArrayList mySourceObsIds; private IIdType myTargetPatientId; private IIdType myTargetEnc1; - private Patient myResultPatient; private final FhirContext myFhirContext; private final SystemRequestDetails mySrd = new SystemRequestDetails(); @@ -150,10 +151,6 @@ public void beforeEach() throws Exception { IIdType obsId = myObservationDao.create(obs, mySrd).getId().toUnqualifiedVersionless(); mySourceObsIds.add(obsId); } - - myResultPatient = new Patient(); - myResultPatient.setIdElement((IdType) myTargetPatientId); - myResultPatient.addIdentifier(pat1IdentifierA); } public void setSourceAndTarget(PatientMergeInputParameters inParams) { @@ -161,14 +158,17 @@ public void setSourceAndTarget(PatientMergeInputParameters inParams) { inParams.targetPatient = new Reference().setReferenceElement(myTargetPatientId); } - public void setResultPatient(PatientMergeInputParameters theInParams, boolean theWithDelete) { - if (!theWithDelete) { + public Patient createResultPatient(boolean theDeleteSource) { + Patient resultPatient = new Patient(); + resultPatient.setIdElement((IdType) myTargetPatientId); + resultPatient.addIdentifier(pat1IdentifierA); + if (!theDeleteSource) { // add the link only if we are not deleting the source - Patient.PatientLinkComponent link = myResultPatient.addLink(); + Patient.PatientLinkComponent link = resultPatient.addLink(); link.setOther(new Reference(mySourcePatientId)); link.setType(Patient.LinkType.REPLACES); } - theInParams.resultPatient = myResultPatient; + return resultPatient; } public Patient readSourcePatient() { @@ -277,7 +277,7 @@ public PatientMergeInputParameters buildMultipleTargetMatchParameters( inParams.targetPatientIdentifier = patBothIdentifierC; inParams.deleteSource = theWithDelete; if (theWithInputResultPatient) { - inParams.resultPatient = myResultPatient; + inParams.resultPatient = createResultPatient(theWithDelete); } if (theWithPreview) { inParams.preview = true; @@ -292,7 +292,7 @@ public PatientMergeInputParameters buildMultipleSourceMatchParameters( inParams.targetPatient = new Reference().setReferenceElement(mySourcePatientId); inParams.deleteSource = theWithDelete; if (theWithInputResultPatient) { - inParams.resultPatient = myResultPatient; + inParams.resultPatient = createResultPatient(theWithDelete); } if (theWithPreview) { inParams.preview = true; @@ -340,7 +340,7 @@ public Parameters asParametersResource() { } } - public static void validatePatchResultBundle( + public void validatePatchResultBundle( Bundle patchResultBundle, int theTotalExpectedPatches, List theExpectedResourceTypes) { String resourceMatchString = "(" + String.join("|", theExpectedResourceTypes) + ")"; Pattern expectedPatchIssuePattern = @@ -398,4 +398,55 @@ private void validateJobReport(JobInstance theJobInstance, IIdType theTaskId) { IdDt resultTaskId = replaceReferenceResultsJson.getTaskId().asIdDt(); assertEquals(theTaskId.getIdPart(), resultTaskId.getIdPart()); } + + public List getExpectedIdentifiersForTargetAfterMerge(boolean theWithInputResultPatient) { + + List expectedIdentifiersOnTargetAfterMerge = null; + if (theWithInputResultPatient) { + expectedIdentifiersOnTargetAfterMerge = + List.of(new Identifier().setSystem("SYS1A").setValue("VAL1A")); + } else { + // the identifiers copied over from source should be marked as old + expectedIdentifiersOnTargetAfterMerge = List.of( + new Identifier().setSystem("SYS2A").setValue("VAL2A"), + new Identifier().setSystem("SYS2B").setValue("VAL2B"), + new Identifier().setSystem("SYSC").setValue("VALC"), + new Identifier().setSystem("SYS1A").setValue("VAL1A").copy().setUse(Identifier.IdentifierUse.OLD), + new Identifier().setSystem("SYS1B").setValue("VAL1B").copy().setUse(Identifier.IdentifierUse.OLD)); + } + return expectedIdentifiersOnTargetAfterMerge; + } + + public void assertSourcePatientUpdatedOrDeleted(boolean withDelete) { + if (withDelete) { + assertThrows(ResourceGoneException.class, () -> readSourcePatient()); + } else { + Patient source = readSourcePatient(); + assertThat(source.getLink()).hasSize(1); + Patient.PatientLinkComponent link = source.getLink().get(0); + assertThat(link.getOther().getReferenceElement()).isEqualTo(getTargetPatientId()); + assertThat(link.getType()).isEqualTo(Patient.LinkType.REPLACEDBY); + } + } + + public void assertTargetPatientUpdated(boolean withDelete, List theExpectedIdentifiers) { + Patient target = readTargetPatient(); + if (!withDelete) { + assertThat(target.getLink()).hasSize(1); + Patient.PatientLinkComponent link = target.getLink().get(0); + assertThat(link.getOther().getReferenceElement()).isEqualTo(getSourcePatientId()); + assertThat(link.getType()).isEqualTo(Patient.LinkType.REPLACES); + } + // assertExpected Identifiers found on the target + assertIdentifiers(target.getIdentifier(), theExpectedIdentifiers); + } + + public void assertIdentifiers(List theActualIdentifiers, List theExpectedIdentifiers) { + assertThat(theActualIdentifiers).hasSize(theExpectedIdentifiers.size()); + for (int i = 0; i < theExpectedIdentifiers.size(); i++) { + Identifier expectedIdentifier = theExpectedIdentifiers.get(i); + Identifier actualIdentifier = theActualIdentifiers.get(i); + assertThat(expectedIdentifier.equalsDeep(actualIdentifier)).isTrue(); + } + } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeHelper.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeHelper.java index 4360e917ee05..5be18aefd813 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeHelper.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeHelper.java @@ -75,7 +75,7 @@ public Patient updateMergedResourcesAfterReferencesReplaced( myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { Patient patientToUpdate = prepareTargetPatientForUpdate( theTargetResource, theSourceResource, theResultResource, theDeleteSource); - // update the target patient resource after the references are updated + targetPatientAfterUpdate.set(updateResource(patientToUpdate, theRequestDetails)); if (theDeleteSource) { From 21bdb0e1a4b67319cf63afcaea77580990f40f0a Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Tue, 17 Dec 2024 12:01:58 -0500 Subject: [PATCH 091/148] async success message, inject mergeservice, add async merge unit tests --- .../ca/uhn/fhir/jpa/config/JpaConfig.java | 30 +- .../BaseJpaResourceProviderPatient.java | 36 +- .../provider/ReplaceReferencesSvcImpl.java | 9 +- .../provider/merge/ResourceMergeService.java | 51 ++- .../merge/ResourceMergeServiceTest.java | 395 ++++++++++++++---- .../jpa/provider/r4/PatientMergeR4Test.java | 12 +- .../batch2/jobs/merge/MergeJobParameters.java | 2 +- .../merge/MergeUpdateTaskReducerStep.java | 2 +- ...h2TaskUtils.java => Batch2TaskHelper.java} | 8 +- 9 files changed, 389 insertions(+), 156 deletions(-) rename hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/{Batch2TaskUtils.java => Batch2TaskHelper.java} (93%) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index eed372a13c1b..1272aaed7d83 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -23,6 +23,7 @@ import ca.uhn.fhir.batch2.api.IJobPersistence; import ca.uhn.fhir.batch2.jobs.export.BulkDataExportProvider; import ca.uhn.fhir.batch2.jobs.expunge.DeleteExpungeJobSubmitterImpl; +import ca.uhn.fhir.batch2.util.Batch2TaskHelper; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.IValidationSupport; @@ -107,6 +108,7 @@ import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; import ca.uhn.fhir.jpa.provider.ValueSetOperationProvider; import ca.uhn.fhir.jpa.provider.ValueSetOperationProviderDstu2; +import ca.uhn.fhir.jpa.provider.merge.ResourceMergeService; import ca.uhn.fhir.jpa.sched.AutowiringSpringBeanJobFactory; import ca.uhn.fhir.jpa.sched.HapiSchedulerServiceImpl; import ca.uhn.fhir.jpa.search.ISynchronousSearchSvc; @@ -932,23 +934,47 @@ public CacheTagDefinitionDao tagDefinitionDao( return new CacheTagDefinitionDao(tagDefinitionDao, memoryCacheService); } + @Bean + public Batch2TaskHelper batch2TaskHelper() { + return new Batch2TaskHelper(); + } + @Bean public IReplaceReferencesSvc replaceReferencesSvc( DaoRegistry theDaoRegistry, HapiTransactionService theHapiTransactionService, IResourceLinkDao theResourceLinkDao, IJobCoordinator theJobCoordinator, - ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundle) { + ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundle, + Batch2TaskHelper theBatch2TaskHelper) { return new ReplaceReferencesSvcImpl( theDaoRegistry, theHapiTransactionService, theResourceLinkDao, theJobCoordinator, - theReplaceReferencesPatchBundle); + theReplaceReferencesPatchBundle, + theBatch2TaskHelper); } @Bean public ReplaceReferencesPatchBundleSvc replaceReferencesPatchBundleSvc(DaoRegistry theDaoRegistry) { return new ReplaceReferencesPatchBundleSvc(theDaoRegistry); } + + @Bean + public ResourceMergeService resourceMergeService( + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + HapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper) { + return new ResourceMergeService( + theDaoRegistry, + theReplaceReferencesSvc, + theHapiTransactionService, + theRequestPartitionHelperSvc, + theJobCoordinator, + theBatch2TaskHelper); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index e7d709811b17..4ed9918a699c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -19,15 +19,10 @@ */ package ca.uhn.fhir.jpa.provider; -import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.api.dao.DaoRegistry; -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.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.model.util.JpaConstants; -import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.provider.merge.MergeOperationInputParameters; import ca.uhn.fhir.jpa.provider.merge.MergeOperationOutcome; import ca.uhn.fhir.jpa.provider.merge.PatientMergeOperationInputParameters; @@ -63,7 +58,6 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.Task; import org.springframework.beans.factory.annotation.Autowired; import java.util.Arrays; @@ -76,20 +70,10 @@ public abstract class BaseJpaResourceProviderPatient extends BaseJpaResourceProvider { @Autowired - private DaoRegistry myDaoRegistry; + private ResourceMergeService myResourceMergeService; @Autowired - private IReplaceReferencesSvc myReplaceReferencesSvc; - - @Autowired - private IHapiTransactionService myHapiTransactionService; - - @Autowired - private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; - - @Autowired - private IJobCoordinator myJobCoordinator; - + private FhirContext myFhirContext; /** * Patient/123/$everything */ @@ -322,23 +306,11 @@ public IBaseParameters patientMerge( theResultPatient, batchSize); - IFhirResourceDaoPatient patientDao = (IFhirResourceDaoPatient) getDao(); - IFhirResourceDao taskDao = myDaoRegistry.getResourceDao(Task.class); - ResourceMergeService resourceMergeService = new ResourceMergeService( - patientDao, - taskDao, - myReplaceReferencesSvc, - myHapiTransactionService, - myRequestPartitionHelperSvc, - myJobCoordinator); - - FhirContext fhirContext = patientDao.getContext(); - MergeOperationOutcome mergeOutcome = - resourceMergeService.merge(mergeOperationParameters, theRequestDetails); + myResourceMergeService.merge(mergeOperationParameters, theRequestDetails); theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); - return buildMergeOperationOutputParameters(fhirContext, mergeOutcome, theRequestDetails.getResource()); + return buildMergeOperationOutputParameters(myFhirContext, mergeOutcome, theRequestDetails.getResource()); } finally { endRequest(theServletRequest); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 6ae8e0ba52c7..d0757577a52b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -21,7 +21,7 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; -import ca.uhn.fhir.batch2.util.Batch2TaskUtils; +import ca.uhn.fhir.batch2.util.Batch2TaskHelper; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; @@ -55,18 +55,21 @@ public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { private final IResourceLinkDao myResourceLinkDao; private final IJobCoordinator myJobCoordinator; private final ReplaceReferencesPatchBundleSvc myReplaceReferencesPatchBundleSvc; + private final Batch2TaskHelper myBatch2TaskHelper; public ReplaceReferencesSvcImpl( DaoRegistry theDaoRegistry, HapiTransactionService theHapiTransactionService, IResourceLinkDao theResourceLinkDao, IJobCoordinator theJobCoordinator, - ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc) { + ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc, + Batch2TaskHelper theBatch2TaskHelper) { myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; myResourceLinkDao = theResourceLinkDao; myJobCoordinator = theJobCoordinator; myReplaceReferencesPatchBundleSvc = theReplaceReferencesPatchBundleSvc; + myBatch2TaskHelper = theBatch2TaskHelper; } @Override @@ -94,7 +97,7 @@ public Integer countResourcesReferencingResource(IIdType theResourceId, RequestD private IBaseParameters replaceReferencesPreferAsync( ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { - Task task = Batch2TaskUtils.startJobAndCreateAssociatedTask( + Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( myDaoRegistry.getResourceDao(Task.class), theRequestDetails, myJobCoordinator, diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 93ea148d4964..974fe105a1b4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -23,12 +23,12 @@ import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; import ca.uhn.fhir.batch2.jobs.merge.MergeHelper; import ca.uhn.fhir.batch2.jobs.merge.MergeJobParameters; -import ca.uhn.fhir.batch2.util.Batch2TaskUtils; +import ca.uhn.fhir.batch2.util.Batch2TaskHelper; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; @@ -69,7 +69,7 @@ public class ResourceMergeService { private static final Logger ourLog = LoggerFactory.getLogger(ResourceMergeService.class); - private final IFhirResourceDaoPatient myPatientDao; + private final IFhirResourceDao myPatientDao; private final IReplaceReferencesSvc myReplaceReferencesSvc; private final IHapiTransactionService myHapiTransactionService; private final FhirContext myFhirContext; @@ -77,20 +77,22 @@ public class ResourceMergeService { private final IFhirResourceDao myTaskDao; private final IJobCoordinator myJobCoordinator; private final MergeHelper myMergeHelper; + private final Batch2TaskHelper myBatch2TaskHelper; public ResourceMergeService( - IFhirResourceDaoPatient thePatientDao, - IFhirResourceDao theTaskDao, + DaoRegistry theDaoRegistry, IReplaceReferencesSvc theReplaceReferencesSvc, IHapiTransactionService theHapiTransactionService, IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator) { + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper) { - myPatientDao = thePatientDao; - myTaskDao = theTaskDao; + myPatientDao = theDaoRegistry.getResourceDao(Patient.class); + myTaskDao = theDaoRegistry.getResourceDao(Task.class); myReplaceReferencesSvc = theReplaceReferencesSvc; myRequestPartitionHelperSvc = theRequestPartitionHelperSvc; myJobCoordinator = theJobCoordinator; + myBatch2TaskHelper = theBatch2TaskHelper; myFhirContext = myPatientDao.getContext(); myHapiTransactionService = theHapiTransactionService; myMergeHelper = new MergeHelper(myPatientDao); @@ -147,9 +149,6 @@ private void validateAndMerge( } doMerge(theMergeOperationParameters, sourceResource, targetResource, theRequestDetails, theMergeOutcome); - - String detailsText = "Merge operation completed successfully."; - addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); } private ValidationResult validate( @@ -214,7 +213,7 @@ private void handlePreview( MergeOperationOutcome theMergeOutcome) { Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement(), theRequestDetails); + theSourceResource.getIdElement().toVersionless(), theRequestDetails); // in preview mode, we should also return how the target would look like Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); @@ -250,7 +249,7 @@ private void doMerge( } else { // count the number of refs, if it is larger than batch size then process async, otherwise process sync Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement(), theRequestDetails); + theSourceResource.getIdElement().toVersionless(), theRequestDetails); if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { doMergeAsync( theMergeOperationParameters, @@ -296,9 +295,11 @@ private void doMergeSync( theMergeOperationParameters.getDeleteSource(), theRequestDetails); theMergeOutcome.setUpdatedTargetResource(updatedTarget); + + String detailsText = "Merge operation completed successfully."; + addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); } - // FIXME ED add unit tests for async case private void doMergeAsync( MergeOperationInputParameters theMergeOperationParameters, Patient theSourceResource, @@ -315,17 +316,23 @@ private void doMergeAsync( } mergeJobParameters.setDeleteSource(theMergeOperationParameters.getDeleteSource()); mergeJobParameters.setBatchSize(theMergeOperationParameters.getBatchSize()); - mergeJobParameters.setSourceId(new FhirIdJson(theSourceResource.getIdElement())); - mergeJobParameters.setTargetId(new FhirIdJson(theTargetResource.getIdElement())); + mergeJobParameters.setSourceId( + new FhirIdJson(theSourceResource.getIdElement().toVersionless())); + mergeJobParameters.setTargetId( + new FhirIdJson(theTargetResource.getIdElement().toVersionless())); mergeJobParameters.setPartitionId(partitionId); - Task task = Batch2TaskUtils.startJobAndCreateAssociatedTask( + Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); task.getMeta().setVersionId(null); theMergeOutcome.setTask(task); theMergeOutcome.setHttpStatusCode(STATUS_HTTP_202_ACCEPTED); + + String detailsText = "Merge request is accepted, and will be processed asynchronously. See" + + " task resource returned in this response for details."; + addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); } private boolean validateResultResourceIfExists( @@ -399,7 +406,7 @@ private boolean hasAllIdentifiers(Patient theResource, List private List getLinksToResource( Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { - List links = getLinksOfType(theResource, theLinkType); + List links = getLinksOfTypeWithNonNullReference(theResource, theLinkType); return links.stream() .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) .collect(Collectors.toList()); @@ -444,7 +451,7 @@ private boolean validateResultResourceReplacesLinkToSourceResource( return true; } - protected List getLinksOfType(Patient theResource, Patient.LinkType theLinkType) { + protected List getLinksOfTypeWithNonNullReference(Patient theResource, Patient.LinkType theLinkType) { List links = new ArrayList<>(); if (theResource.hasLink()) { for (Patient.PatientLinkComponent link : theResource.getLink()) { @@ -472,7 +479,8 @@ private boolean validateSourceAndTargetAreSuitableForMerge( return false; } - List replacedByLinksInTarget = getLinksOfType(theTargetResource, Patient.LinkType.REPLACEDBY); + List replacedByLinksInTarget = + getLinksOfTypeWithNonNullReference(theTargetResource, Patient.LinkType.REPLACEDBY); if (!replacedByLinksInTarget.isEmpty()) { String ref = replacedByLinksInTarget.get(0).getReference(); String msg = String.format( @@ -483,7 +491,8 @@ private boolean validateSourceAndTargetAreSuitableForMerge( return false; } - List replacedByLinksInSource = getLinksOfType(theSourceResource, Patient.LinkType.REPLACEDBY); + List replacedByLinksInSource = + getLinksOfTypeWithNonNullReference(theSourceResource, Patient.LinkType.REPLACEDBY); if (!replacedByLinksInSource.isEmpty()) { String ref = replacedByLinksInSource.get(0).getReference(); String msg = String.format( diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java index 9f492ebc7e2f..61cf4d301562 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java @@ -1,7 +1,12 @@ package ca.uhn.fhir.jpa.provider.merge; import ca.uhn.fhir.batch2.api.IJobCoordinator; +import ca.uhn.fhir.batch2.jobs.merge.MergeJobParameters; +import ca.uhn.fhir.batch2.jobs.parameters.BatchJobParametersWithTaskId; +import ca.uhn.fhir.batch2.util.Batch2TaskHelper; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; @@ -26,10 +31,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.OngoingStubbing; +import org.testcontainers.shaded.org.checkerframework.checker.nullness.qual.Nullable; import java.util.Collections; import java.util.List; @@ -57,7 +66,10 @@ public class ResourceMergeServiceTest { "Source resource must be provided either by 'source-patient' or by 'source-patient-identifier', not both."; private static final String BOTH_TARGET_PARAMS_PROVIDED_MSG = "Target resource must be provided either by 'target-patient' or by 'target-patient-identifier', not both."; - private static final String SUCCESSFUL_MERGE_MSG = "Merge operation completed successfully"; + private static final String SUCCESSFUL_SYNC_MERGE_MSG = "Merge operation completed successfully"; + private static final String SUCCESSFUL_ASYNC_MERGE_MSG = "Merge request is accepted, and will be " + + "processed asynchronously. See task resource returned in this response for details."; + private static final String SOURCE_PATIENT_TEST_ID = "Patient/123"; private static final String SOURCE_PATIENT_TEST_ID_WITH_VERSION_1= SOURCE_PATIENT_TEST_ID + "/_history/1"; private static final String SOURCE_PATIENT_TEST_ID_WITH_VERSION_2= SOURCE_PATIENT_TEST_ID + "/_history/2"; @@ -65,6 +77,9 @@ public class ResourceMergeServiceTest { private static final String TARGET_PATIENT_TEST_ID_WITH_VERSION_1 = TARGET_PATIENT_TEST_ID + "/_history/1"; private static final String TARGET_PATIENT_TEST_ID_WITH_VERSION_2 = TARGET_PATIENT_TEST_ID + "/_history/2"; + @Mock + DaoRegistry myDaoRegistryMock; + @Mock IFhirResourceDaoPatient myPatientDaoMock; @@ -86,6 +101,12 @@ public class ResourceMergeServiceTest { @Mock IJobCoordinator myJobCoordinatorMock; + @Mock + Batch2TaskHelper myBatch2TaskHelperMock; + + @Mock + RequestPartitionId myRequestPartitionIdMock; + private ResourceMergeService myResourceMergeService; @@ -97,10 +118,15 @@ public class ResourceMergeServiceTest { @BeforeEach void setup() { + when(myDaoRegistryMock.getResourceDao(eq(Patient.class))).thenReturn(myPatientDaoMock); + when(myDaoRegistryMock.getResourceDao(eq(Task.class))).thenReturn(myTaskDaoMock); when(myPatientDaoMock.getContext()).thenReturn(myFhirContext); - myResourceMergeService = new ResourceMergeService(myPatientDaoMock, myTaskDaoMock, myReplaceReferencesSvcMock, - myTransactionServiceMock, myRequestPartitionHelperSvcMock, myJobCoordinatorMock); - + myResourceMergeService = new ResourceMergeService(myDaoRegistryMock, + myReplaceReferencesSvcMock, + myTransactionServiceMock, + myRequestPartitionHelperSvcMock, + myJobCoordinatorMock, + myBatch2TaskHelperMock); } // SUCCESS CASES @@ -110,13 +136,13 @@ void testMerge_WithoutResultResource_Success() { MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); - Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); //the identifiers should be copied from the source to the target, without creating duplicates on the target sourcePatient.addIdentifier(new Identifier().setSystem("sysSource").setValue("valS1")); sourcePatient.addIdentifier(new Identifier().setSystem("sysSource").setValue("valS2")); sourcePatient.addIdentifier(new Identifier().setSystem("sysCommon").setValue("valCommon")); - Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_1); targetPatient.addIdentifier(new Identifier().setSystem("sysCommon").setValue("valCommon")); targetPatient.addIdentifier(new Identifier().setSystem("sysTarget").setValue("valT1")); setupDaoMockForSuccessfulRead(sourcePatient); @@ -131,7 +157,7 @@ void testMerge_WithoutResultResource_Success() { MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then - verifySuccessfulOutcome(mergeOutcome, patientReturnedFromDaoAfterTargetUpdate); + verifySuccessfulOutcomeForSync(mergeOutcome, patientReturnedFromDaoAfterTargetUpdate); verifyUpdatedSourcePatient(); // the identifiers copied over from the source should be marked as OLD List expectedIdentifiers = List.of( @@ -150,8 +176,8 @@ void testMerge_WithoutResultResource_TargetSetToActiveExplicitly_Success() { MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); - Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); - Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_1); targetPatient.setActive(true); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); @@ -165,7 +191,7 @@ void testMerge_WithoutResultResource_TargetSetToActiveExplicitly_Success() { MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then - verifySuccessfulOutcome(mergeOutcome, patientReturnedFromDaoAfterTargetUpdate); + verifySuccessfulOutcomeForSync(mergeOutcome, patientReturnedFromDaoAfterTargetUpdate); verifyUpdatedSourcePatient(); verifyUpdatedTargetPatient(true, Collections.emptyList()); verifyNoMoreInteractions(myPatientDaoMock); @@ -180,11 +206,11 @@ void testMerge_WithResultResource_Success() { Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); resultPatient.addLink().setType(Patient.LinkType.REPLACES).setOther(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setResultResource(resultPatient); - Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); //when result resource exists, the identifiers should not be copied. so we don't expect this identifier when //target is updated sourcePatient.addIdentifier(new Identifier().setSystem("sysSource").setValue("valS1")); - Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_1); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); @@ -199,7 +225,7 @@ void testMerge_WithResultResource_Success() { MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then - verifySuccessfulOutcome(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); + verifySuccessfulOutcomeForSync(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); verifyUpdatedSourcePatient(); verifyUpdatedTargetPatient(true, Collections.emptyList()); verifyNoMoreInteractions(myPatientDaoMock); @@ -220,8 +246,8 @@ void testMerge_WithResultResource_ResultHasAllTargetIdentifiers_Success() { resultPatient.addIdentifier().setSystem("sys").setValue("val1"); resultPatient.addIdentifier().setSystem("sys").setValue("val2"); mergeOperationParameters.setResultResource(resultPatient); - Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); - Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_1); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockSearchForIdentifiers(List.of(List.of(targetPatient))); @@ -237,7 +263,7 @@ void testMerge_WithResultResource_ResultHasAllTargetIdentifiers_Success() { MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then - verifySuccessfulOutcome(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); + verifySuccessfulOutcomeForSync(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); verifyUpdatedSourcePatient(); List expectedIdentifiers = List.of( new Identifier().setSystem("sys").setValue("val1"), @@ -254,12 +280,12 @@ void testMerge_WithDeleteSourceTrue_Success() { mergeOperationParameters.setDeleteSource(true); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); - Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); - Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_1); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); - when(myPatientDaoMock.delete(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenReturn(new DaoMethodOutcome()); + when(myPatientDaoMock.delete(new IdType(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1), myRequestDetailsMock)).thenReturn(new DaoMethodOutcome()); Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); setupDaoMockForSuccessfulTargetPatientUpdate(targetPatient, patientToBeReturnedFromDaoAfterTargetUpdate); setupTransactionServiceMock(); @@ -270,7 +296,7 @@ void testMerge_WithDeleteSourceTrue_Success() { MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then - verifySuccessfulOutcome(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); + verifySuccessfulOutcomeForSync(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); verifyUpdatedTargetPatient(false, Collections.emptyList()); verifyNoMoreInteractions(myPatientDaoMock); } @@ -283,14 +309,14 @@ void testMerge_WithDeleteSourceTrue_And_WithResultResource_Success() { mergeOperationParameters.setDeleteSource(true); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); - Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); - Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_1); Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); mergeOperationParameters.setResultResource(resultPatient); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); - when(myPatientDaoMock.delete(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenReturn(new DaoMethodOutcome()); + when(myPatientDaoMock.delete(new IdType(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1), myRequestDetailsMock)).thenReturn(new DaoMethodOutcome()); Patient patientToBeReturnedFromDaoAfterTargetUpdate = new Patient(); setupDaoMockForSuccessfulTargetPatientUpdate(resultPatient, patientToBeReturnedFromDaoAfterTargetUpdate); setupTransactionServiceMock(); @@ -301,7 +327,7 @@ void testMerge_WithDeleteSourceTrue_And_WithResultResource_Success() { MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then - verifySuccessfulOutcome(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); + verifySuccessfulOutcomeForSync(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); verifyUpdatedTargetPatient(false, Collections.emptyList()); verifyNoMoreInteractions(myPatientDaoMock); } @@ -313,8 +339,8 @@ void testMerge_WithPreviewTrue_Success() { mergeOperationParameters.setPreview(true); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); - Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); - Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_1); setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); @@ -357,18 +383,100 @@ void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersio MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); // Then - verifySuccessfulOutcome(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); + verifySuccessfulOutcomeForSync(mergeOutcome, patientToBeReturnedFromDaoAfterTargetUpdate); verifyUpdatedSourcePatient(); verifyUpdatedTargetPatient(true, Collections.emptyList()); verifyNoMoreInteractions(myPatientDaoMock); } - // ERROR CASES - @Test - void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheException() { + @ParameterizedTest + @CsvSource({ + "true, false", + "false, true", + "true, true", + "false, false" + }) + void testMerge_AsyncBecauseOfPreferHeader_Success(boolean theWithResultResource, boolean theWithDeleteSource) { + // Given + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + mergeOperationParameters.setDeleteSource(theWithDeleteSource); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); + when(myRequestDetailsMock.isPreferAsync()).thenReturn(true); + when(myRequestPartitionHelperSvcMock.determineReadPartitionForRequest(eq(myRequestDetailsMock), any())).thenReturn(myRequestPartitionIdMock); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_1); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + + Patient resultResource = null; + if (theWithResultResource) { + resultResource = createValidResultPatient(theWithDeleteSource); + mergeOperationParameters.setResultResource(resultResource); + } + + Task task = new Task(); + setupBatch2JobTaskHelperMock(task); + + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + verifySuccessfulOutcomeForAsync(mergeOutcome, task); + verifyBatch2JobTaskHelperMockInvocation(resultResource, theWithDeleteSource); + + verifyNoMoreInteractions(myPatientDaoMock); + } + + @ParameterizedTest + @CsvSource({ + "true, false", + "false, true", + "true, true", + "false, false" + }) + void testMerge_AsyncBecauseOfLargeNumberOfRefs_Success(boolean theWithResultResource, + boolean theWithDeleteSource) { + // Given + MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); + mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); + mergeOperationParameters.setDeleteSource(theWithDeleteSource); + Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); + when(myRequestDetailsMock.isPreferAsync()).thenReturn(false); + when(myRequestPartitionHelperSvcMock.determineReadPartitionForRequest(eq(myRequestDetailsMock), any())).thenReturn(myRequestPartitionIdMock); + Patient targetPatient = createPatient(TARGET_PATIENT_TEST_ID_WITH_VERSION_1); + setupDaoMockForSuccessfulRead(sourcePatient); + setupDaoMockForSuccessfulRead(targetPatient); + + when(myReplaceReferencesSvcMock.countResourcesReferencingResource(new IdType(SOURCE_PATIENT_TEST_ID), + myRequestDetailsMock)).thenReturn(PAGE_SIZE + 1); + + Patient resultResource = null; + if (theWithResultResource) { + resultResource = createValidResultPatient(theWithDeleteSource); + mergeOperationParameters.setResultResource(resultResource); + } + + Task task = new Task(); + setupBatch2JobTaskHelperMock(task); + + MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); + + verifySuccessfulOutcomeForAsync(mergeOutcome, task); + verifyBatch2JobTaskHelperMockInvocation(resultResource, theWithDeleteSource); + + verifyNoMoreInteractions(myPatientDaoMock); + + } + + // ERROR CASES + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheException(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -390,10 +498,12 @@ void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheExcepti verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_UnhandledExceptionThrown_Uses500StatusCode() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_UnhandledExceptionThrown_Uses500StatusCode(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -415,10 +525,12 @@ void testMerge_UnhandledExceptionThrown_Uses500StatusCode() { verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_ValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorWith400Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorWith400Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); // When @@ -438,10 +550,12 @@ void testMerge_ValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorW } - @Test - void testMerge_ValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorWith400Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorWith400Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); // When @@ -461,10 +575,12 @@ void testMerge_ValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorW verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_ValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ReturnsErrorsWith400Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ReturnsErrorsWith400Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); // When MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); @@ -486,10 +602,12 @@ void testMerge_ValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_ValidatesInputParameters_BothSourceResourceAndSourceIdentifierParamsProvided_ReturnsErrorWith400Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ValidatesInputParameters_BothSourceResourceAndSourceIdentifierParamsProvided_ReturnsErrorWith400Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -512,10 +630,12 @@ void testMerge_ValidatesInputParameters_BothSourceResourceAndSourceIdentifierPar } - @Test - void testMerge_ValidatesInputParameters_BothTargetResourceAndTargetIdentifiersParamsProvided_ReturnsErrorWith400Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ValidatesInputParameters_BothTargetResourceAndTargetIdentifiersParamsProvided_ReturnsErrorWith400Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); @@ -537,10 +657,12 @@ void testMerge_ValidatesInputParameters_BothTargetResourceAndTargetIdentifiersPa } - @Test - void testMerge_ValidatesInputParameters_SourceResourceParamHasNoReferenceElement_ReturnsErrorWith400Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ValidatesInputParameters_SourceResourceParamHasNoReferenceElement_ReturnsErrorWith400Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference()); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -562,10 +684,12 @@ void testMerge_ValidatesInputParameters_SourceResourceParamHasNoReferenceElement } - @Test - void testMerge_ValidatesInputParameters_TargetResourceParamHasNoReferenceElement_ReturnsErrorWith400Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ValidatesInputParameters_TargetResourceParamHasNoReferenceElement_ReturnsErrorWith400Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference()); @@ -587,10 +711,12 @@ void testMerge_ValidatesInputParameters_TargetResourceParamHasNoReferenceElement verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWith422Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWith422Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); when(myPatientDaoMock.read(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenThrow(ResourceNotFoundException.class); @@ -611,10 +737,12 @@ void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWi verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWith422Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWith422Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); @@ -637,10 +765,12 @@ void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWi verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_ResolvesSourceResourceByIdentifiers_NoMatchFound_ReturnsErrorWith422Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ResolvesSourceResourceByIdentifiers_NoMatchFound_ReturnsErrorWith422Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResourceIdentifiers(List.of( new CanonicalIdentifier().setSystem("sys").setValue("val1"), new CanonicalIdentifier().setSystem("sys").setValue("val2"))); @@ -666,10 +796,12 @@ void testMerge_ResolvesSourceResourceByIdentifiers_NoMatchFound_ReturnsErrorWith } - @Test - void testMerge_ResolvesSourceResourceByIdentifiers_MultipleMatchesFound_ReturnsErrorWith422Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ResolvesSourceResourceByIdentifiers_MultipleMatchesFound_ReturnsErrorWith422Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); setupDaoMockSearchForIdentifiers(List.of( @@ -698,10 +830,12 @@ void testMerge_ResolvesSourceResourceByIdentifiers_MultipleMatchesFound_ReturnsE } - @Test - void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith422Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith422Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of( new CanonicalIdentifier().setSystem("sys").setValue("val1"), @@ -729,10 +863,12 @@ void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_ResolvesTargetResourceByIdentifiers_MultipleMatchesFound_ReturnsErrorWith422Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ResolvesTargetResourceByIdentifiers_MultipleMatchesFound_ReturnsErrorWith422Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); setupDaoMockSearchForIdentifiers(List.of( @@ -762,10 +898,12 @@ void testMerge_ResolvesTargetResourceByIdentifiers_MultipleMatchesFound_ReturnsE verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); //make resolved patient has a more recent version than the one specified in the reference @@ -788,10 +926,12 @@ void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVe verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID_WITH_VERSION_1)); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); @@ -817,14 +957,12 @@ void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVe verifyNoMoreInteractions(myPatientDaoMock); } - - - - - @Test - void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val2"))); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); @@ -849,10 +987,12 @@ void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status() //verifyNoMoreInteractions(myDaoMock); } - @Test - void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); @@ -876,10 +1016,12 @@ void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status() { verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsErrorWith422Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsErrorWith422Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); @@ -905,10 +1047,12 @@ void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsError verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_SourceResourceWasPreviouslyReplacedByAnotherResource_ReturnsErrorWith422Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_SourceResourceWasPreviouslyReplacedByAnotherResource_ReturnsErrorWith422Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID); @@ -933,10 +1077,12 @@ void testMerge_SourceResourceWasPreviouslyReplacedByAnotherResource_ReturnsError verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetResource_ReturnsErrorWith400Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetResource_ReturnsErrorWith400Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); Patient resultPatient = createPatient("Patient/not-the-target-id"); @@ -966,10 +1112,12 @@ void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetRes } - @Test - void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersProvidedInTargetIdentifiers_ReturnsErrorWith400Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersProvidedInTargetIdentifiers_ReturnsErrorWith400Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of( new CanonicalIdentifier().setSystem("sysA").setValue("val1"), @@ -1003,10 +1151,12 @@ void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersPr } - @Test - void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLinkAtAll_ReturnsErrorWith400Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLinkAtAll_ReturnsErrorWith400Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -1032,10 +1182,12 @@ void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLinkAtAll_Retu verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLinkToSource_ReturnsErrorWith400Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLinkToSource_ReturnsErrorWith400Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -1063,10 +1215,12 @@ void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLinkToSource_R verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_ValidatesResultResource_ResultResourceHasReplacesLinkAndDeleteSourceIsTrue_ReturnsErrorWith400Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ValidatesResultResource_ResultResourceHasReplacesLinkAndDeleteSourceIsTrue_ReturnsErrorWith400Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); mergeOperationParameters.setDeleteSource(true); @@ -1094,10 +1248,12 @@ void testMerge_ValidatesResultResource_ResultResourceHasReplacesLinkAndDeleteSou verifyNoMoreInteractions(myPatientDaoMock); } - @Test - void testMerge_ValidatesResultResource_ResultResourceHasRedundantReplacesLinksToSource_ReturnsErrorWith400Status() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testMerge_ValidatesResultResource_ResultResourceHasRedundantReplacesLinksToSource_ReturnsErrorWith400Status(boolean thePreview) { // Given MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -1127,14 +1283,28 @@ void testMerge_ValidatesResultResource_ResultResourceHasRedundantReplacesLinksTo verifyNoMoreInteractions(myPatientDaoMock); } - private void verifySuccessfulOutcome(MergeOperationOutcome theMergeOutcome, Patient theExpectedTargetResource) { - OperationOutcome operationOutcome = (OperationOutcome) theMergeOutcome.getOperationOutcome(); + private void verifySuccessfulOutcomeForSync(MergeOperationOutcome theMergeOutcome, Patient theExpectedTargetResource) { assertThat(theMergeOutcome.getHttpStatusCode()).isEqualTo(200); + + OperationOutcome operationOutcome = (OperationOutcome) theMergeOutcome.getOperationOutcome(); assertThat(theMergeOutcome.getUpdatedTargetResource()).isEqualTo(theExpectedTargetResource); assertThat(operationOutcome.getIssue()).hasSize(1); OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); - assertThat(issue.getDetails().getText()).contains(SUCCESSFUL_MERGE_MSG); + assertThat(issue.getDetails().getText()).contains(SUCCESSFUL_SYNC_MERGE_MSG); + } + + private void verifySuccessfulOutcomeForAsync(MergeOperationOutcome theMergeOutcome, Task theExpectedTask) { + assertThat(theMergeOutcome.getHttpStatusCode()).isEqualTo(202); + assertThat(theMergeOutcome.getTask()).isEqualTo(theExpectedTask); + assertThat(theMergeOutcome.getUpdatedTargetResource()).isNull(); + OperationOutcome operationOutcome = (OperationOutcome) theMergeOutcome.getOperationOutcome(); + assertThat(theMergeOutcome.getUpdatedTargetResource()).isNull(); + assertThat(operationOutcome.getIssue()).hasSize(1); + OperationOutcome.OperationOutcomeIssueComponent issue = operationOutcome.getIssueFirstRep(); + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(issue.getDetails().getText()).contains(SUCCESSFUL_ASYNC_MERGE_MSG); + } private Patient createPatient(String theId) { @@ -1143,6 +1313,15 @@ private Patient createPatient(String theId) { return patient; } + private Patient createValidResultPatient(boolean theDeleteSource) { + + Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); + if (!theDeleteSource) { + addReplacesLink(resultPatient, SOURCE_PATIENT_TEST_ID); + } + return resultPatient; + } + private void addReplacedByLink(Patient thePatient, String theReplacingResourceId) { thePatient.addLink().setType(Patient.LinkType.REPLACEDBY).setOther(new Reference(theReplacingResourceId)); } @@ -1231,10 +1410,48 @@ private void verifyUpdatedTargetPatient(boolean theExpectLinkToSourcePatient, Li } private void setupReplaceReferencesForSuccessForSync() { + // set the count to less that the page size for sync processing + when(myReplaceReferencesSvcMock.countResourcesReferencingResource(new IdType(SOURCE_PATIENT_TEST_ID), + myRequestDetailsMock)).thenReturn(PAGE_SIZE - 1); when(myReplaceReferencesSvcMock.replaceReferences(isA(ReplaceReferenceRequest.class), eq(myRequestDetailsMock))).thenReturn(new Parameters()); } + private void setupBatch2JobTaskHelperMock(Task theTaskToReturn) { + when(myBatch2TaskHelperMock.startJobAndCreateAssociatedTask( + eq(myTaskDaoMock), + eq(myRequestDetailsMock), + eq(myJobCoordinatorMock), + eq("MERGE"), + any())).thenReturn(theTaskToReturn); + } + + private void verifyBatch2JobTaskHelperMockInvocation(@Nullable Patient theResultResource, + boolean theDeleteSource) { + ArgumentCaptor jobParametersCaptor = + ArgumentCaptor.forClass(BatchJobParametersWithTaskId.class); + verify(myBatch2TaskHelperMock).startJobAndCreateAssociatedTask( + eq(myTaskDaoMock), + eq(myRequestDetailsMock), + eq(myJobCoordinatorMock), + eq("MERGE"), + jobParametersCaptor.capture()); + + assertThat(jobParametersCaptor.getValue()).isInstanceOf(MergeJobParameters.class); + MergeJobParameters capturedJobParams = (MergeJobParameters) jobParametersCaptor.getValue(); + assertThat(capturedJobParams.getBatchSize()).isEqualTo(PAGE_SIZE); + assertThat(capturedJobParams.getSourceId().toString()).isEqualTo(SOURCE_PATIENT_TEST_ID); + assertThat(capturedJobParams.getTargetId().toString()).isEqualTo(TARGET_PATIENT_TEST_ID); + assertThat(capturedJobParams.getDeleteSource()).isEqualTo(theDeleteSource); + assertThat(capturedJobParams.getPartitionId()).isEqualTo(myRequestPartitionIdMock); + if (theResultResource != null) { + assertThat(capturedJobParams.getResultResource()).isEqualTo(myFhirContext.newJsonParser().encodeResourceToString(theResultResource)); + } + else { + assertThat(capturedJobParams.getResultResource()).isNull(); + } + } + private void setupDaoMockForSuccessfulTargetPatientUpdate(Patient thePatientExpectedAsInput, Patient thePatientToReturnInDaoOutcome) { DaoMethodOutcome daoMethodOutcome = new DaoMethodOutcome(); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 3fdca0c9f281..29e5bd8c2e13 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -139,7 +139,6 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea // Assert Task inAsync mode, unless it is preview in which case we don't return a task if (isAsync && !withPreview) { - // FIXME ED see ProviderConstants.OPERATION_REPLACE_REFERENCES in JpaSystemProvider for how to get this to pass assertThat(getLastHttpStatusCode()).isEqualTo(HttpServletResponse.SC_ACCEPTED); Task task = (Task) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource(); @@ -172,6 +171,17 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea Bundle patchResultBundle = (Bundle) outputRef.getResource(); assertTrue(containedBundle.equalsDeep(patchResultBundle)); myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of("Observation", "Encounter", "CarePlan")); + + OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); + assertThat(outcome.getIssue()) + .hasSize(1) + .element(0) + .satisfies(issue -> { + assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.INFORMATION); + assertThat(issue.getDetails().getText()).isEqualTo("Merge request is accepted, and will be " + + "processed asynchronously. See task resource returned in this response for details."); + }); + } else { // Synchronous case // Assert outcome OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java index 84a2ad119658..7bbbeab55ec5 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeJobParameters.java @@ -37,7 +37,7 @@ public String getResultResource() { return myResultResource; } - public boolean isDeleteSource() { + public boolean getDeleteSource() { return myDeleteSource; } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java index 2f8c209f63e0..715a1799ad3b 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java @@ -68,7 +68,7 @@ public RunOutcome run( mergeJobParameters.getSourceId().asIdDt(), mergeJobParameters.getTargetId().asIdDt(), resultResource, - mergeJobParameters.isDeleteSource(), + mergeJobParameters.getDeleteSource(), requestDetails); return super.run(theStepExecutionDetails, theDataSink); diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskUtils.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java similarity index 93% rename from hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskUtils.java rename to hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java index 47f684f3b8dd..eb2a49a365ac 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskUtils.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java @@ -29,13 +29,9 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.HAPI_BATCH_JOB_ID_SYSTEM; -public class Batch2TaskUtils { +public class Batch2TaskHelper { - private Batch2TaskUtils() { - // non-instantiable - } - - public static Task startJobAndCreateAssociatedTask( + public Task startJobAndCreateAssociatedTask( IFhirResourceDao theTaskDao, RequestDetails theRequestDetails, IJobCoordinator theJobCoordinator, From 9268b9461618382f8e0e6fdd6b865247ae4f8892 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Tue, 17 Dec 2024 13:34:15 -0500 Subject: [PATCH 092/148] make validatePatchResultBundle static again --- .../ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java | 2 +- .../ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java | 4 +++- .../uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java | 7 +++++-- .../jpa/replacereferences/ReplaceReferencesBatchTest.java | 3 ++- .../jpa/replacereferences/ReplaceReferencesTestHelper.java | 2 +- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java index 08abef8dd4f8..4302a294f973 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java @@ -73,7 +73,7 @@ public void testHappyPath(boolean theDeleteSource, boolean theWithResultResource JobInstance jobInstance = myBatch2JobHelper.awaitJobCompletion(jobStartResponse); Bundle patchResultBundle = myTestHelper.validateCompletedTask(jobInstance, taskId); - myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, + ReplaceReferencesTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of( "Observation", "Encounter", "CarePlan")); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 29e5bd8c2e13..4c72d6a0d039 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -170,7 +170,9 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea Reference outputRef = (Reference) taskOutput.getValue(); Bundle patchResultBundle = (Bundle) outputRef.getResource(); assertTrue(containedBundle.equalsDeep(patchResultBundle)); - myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of("Observation", "Encounter", "CarePlan")); + ReplaceReferencesTestHelper.validatePatchResultBundle(patchResultBundle, + ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, + List.of("Observation", "Encounter", "CarePlan")); OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); assertThat(outcome.getIssue()) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index 86260afc8ceb..23bb46beddcd 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -66,7 +66,9 @@ void testReplaceReferences(boolean isAsync) throws IOException { } // validate - myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of("Observation", "Encounter", "CarePlan")); + ReplaceReferencesTestHelper.validatePatchResultBundle(patchResultBundle, + ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of( + "Observation", "Encounter", "CarePlan")); // Check that the linked resources were updated @@ -131,7 +133,8 @@ void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { // validate entriesLeft -= ReplaceReferencesTestHelper.SMALL_BATCH_SIZE; int expectedNumberOfEntries = Math.min(entriesLeft, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); - myTestHelper.validatePatchResultBundle(patchResultBundle, expectedNumberOfEntries, List.of("Observation", "Encounter", "CarePlan")); + ReplaceReferencesTestHelper.validatePatchResultBundle(patchResultBundle, expectedNumberOfEntries, List.of("Observation", + "Encounter", "CarePlan")); } // Check that the linked resources were updated diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java index 3113ceec25c3..0af294796dd7 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java @@ -64,7 +64,8 @@ public void testHappyPath() { JobInstance jobInstance = myBatch2JobHelper.awaitJobCompletion(jobStartResponse); Bundle patchResultBundle = myTestHelper.validateCompletedTask(jobInstance, taskId); - myTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of("Observation", "Encounter", "CarePlan")); + ReplaceReferencesTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of( + "Observation", "Encounter", "CarePlan")); myTestHelper.assertAllReferencesUpdated(); } diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index 9e0ebba00020..6a5dffa443db 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -340,7 +340,7 @@ public Parameters asParametersResource() { } } - public void validatePatchResultBundle( + public static void validatePatchResultBundle( Bundle patchResultBundle, int theTotalExpectedPatches, List theExpectedResourceTypes) { String resourceMatchString = "(" + String.join("|", theExpectedResourceTypes) + ")"; Pattern expectedPatchIssuePattern = From 238ff3f2b16c0543eaf0de2e5ebb6d2308de33e1 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Wed, 18 Dec 2024 16:47:35 -0500 Subject: [PATCH 093/148] added test for adding reference while merge in progress, and merge error handler --- .../jpa/provider/r4/PatientMergeR4Test.java | 54 ++++++++++++++++--- .../provider/r4/ReplaceReferencesR4Test.java | 12 +---- .../ReplaceReferencesTestHelper.java | 36 +++++++++++-- .../fhir/batch2/jobs/merge/MergeAppCtx.java | 17 ++++-- .../batch2/jobs/merge/MergeErrorHandler.java | 31 +++++++++++ .../merge/MergeUpdateTaskReducerStep.java | 1 - .../fhir/batch2/util/Batch2TaskHelper.java | 31 +++++++++++ 7 files changed, 156 insertions(+), 26 deletions(-) create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeErrorHandler.java diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 4c72d6a0d039..b99ecabb7790 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -11,8 +11,10 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import jakarta.annotation.Nonnull; import jakarta.servlet.http.HttpServletResponse; +import org.apache.jena.base.Sys; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Parameters; @@ -31,6 +33,7 @@ import org.springframework.beans.factory.annotation.Autowired; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; @@ -120,7 +123,7 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea Parameters outParams = callMergeOperation(inParameters, isAsync); // validate - // in async mode, there will be an additional task in the output params + // in async mode, there will be an additional task resource in the output params assertThat(outParams.getParameter()).hasSizeBetween(3, 4); // Assert input @@ -144,12 +147,11 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea Task task = (Task) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource(); assertNull(task.getIdElement().getVersionIdPart()); ourLog.info("Got task {}", task.getId()); - await().until(() -> { - myBatch2JobHelper.runMaintenancePass(); - return myTestHelper.taskCompleted(task.getIdElement()); - }); + String jobId = myTestHelper.getJobIdFromTask(task); + myBatch2JobHelper.awaitJobCompletion(jobId); Task taskWithOutput = myTaskDao.read(task.getIdElement(), mySrd); + assertThat(taskWithOutput.getStatus()).isEqualTo(Task.TaskStatus.COMPLETED); ourLog.info("Complete Task: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(taskWithOutput)); Task.TaskOutputComponent taskOutput = taskWithOutput.getOutputFirstRep(); @@ -223,8 +225,46 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea } } - // FIXME ED test case where another resource that links to source was added while the batch was running - // so the source can't be deleted + @Test + void testMerge_SourceResourceCannotBeDeletedBecauseAnotherResourceReferencingSourceWasAddedWhileJobIsRunning_JobFails() { + ReplaceReferencesTestHelper.PatientMergeInputParameters inParams = new ReplaceReferencesTestHelper.PatientMergeInputParameters(); + myTestHelper.setSourceAndTarget(inParams); + inParams.deleteSource = true; + //using a small batch size that would result in multiple chunks to ensure that + //the job runs a bit slowly so that we have sometime to add a resource that references the source + //after the first step + inParams.batchSize = 5; + Parameters inParameters = inParams.asParametersResource(); + + // exec + Parameters outParams = callMergeOperation(inParameters, true); + Task task = (Task) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_TASK).getResource(); + assertNull(task.getIdElement().getVersionIdPart()); + ourLog.info("Got task {}", task.getId()); + String jobId = myTestHelper.getJobIdFromTask(task); + + // wait for first step of the job to finish + await() + .until(() -> { + myBatch2JobHelper.runMaintenancePass(); + String currentGatedStepId = myJobCoordinator.getInstance(jobId).getCurrentGatedStepId(); + return !"query-ids".equals(currentGatedStepId); + }); + + Encounter enc = new Encounter(); + enc.setStatus(Encounter.EncounterStatus.ARRIVED); + enc.getSubject().setReferenceElement(myTestHelper.getSourcePatientId()); + myEncounterDao.create(enc, mySrd).getId().toUnqualifiedVersionless(); + + myBatch2JobHelper.awaitJobFailure(jobId); + + + await().until(() -> { + Task taskAfterJobFailure = myTaskDao.read(task.getIdElement().toVersionless(), mySrd); + ourLog.info("Check if Task status is FAILED: {}", taskAfterJobFailure.getStatus()); + return Task.TaskStatus.FAILED.equals(taskAfterJobFailure.getStatus()); + }); + } @ParameterizedTest @CsvSource({ diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index 23bb46beddcd..05a46a8ab991 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -6,12 +6,10 @@ import jakarta.servlet.http.HttpServletResponse; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; -import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.Task; -import org.hl7.fhir.r4.model.Type; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -21,8 +19,6 @@ import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; import static ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper.EXPECTED_SMALL_BATCHES; -import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_202_ACCEPTED; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.HAPI_BATCH_JOB_ID_SYSTEM; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; import static org.assertj.core.api.Assertions.assertThat; @@ -76,16 +72,10 @@ void testReplaceReferences(boolean isAsync) throws IOException { } private JobInstance awaitJobCompletion(Task task) { - assertThat(task.getIdentifier()).hasSize(1) - .element(0) - .extracting(Identifier::getSystem) - .isEqualTo(HAPI_BATCH_JOB_ID_SYSTEM); - - String jobId = task.getIdentifierFirstRep().getValue(); + String jobId = myTestHelper.getJobIdFromTask(task); return myBatch2JobHelper.awaitJobCompletion(jobId); } - @ParameterizedTest @ValueSource(booleans = {false, true}) void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index 6a5dffa443db..ce9ddf6b262e 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR JPA Server Test Utilities + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.jpa.replacereferences; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceResultsJson; @@ -47,6 +66,7 @@ import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER; import static ca.uhn.fhir.rest.api.Constants.HEADER_PREFER_RESPOND_ASYNC; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.HAPI_BATCH_JOB_ID_SYSTEM; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -198,10 +218,14 @@ private Set getTargetEverythingResourceIds() { .collect(Collectors.toSet()); } - public Boolean taskCompleted(IdType theTaskId) { - Task updatedTask = myTaskDao.read(theTaskId, mySrd); - ourLog.info("Task {} status is {}", theTaskId, updatedTask.getStatus()); - return updatedTask.getStatus() == Task.TaskStatus.COMPLETED; + public String getJobIdFromTask(Task task) { + assertThat(task.getIdentifier()) + .hasSize(1) + .element(0) + .extracting(Identifier::getSystem) + .isEqualTo(HAPI_BATCH_JOB_ID_SYSTEM); + + return task.getIdentifierFirstRep().getValue(); } public Parameters callReplaceReferences(IGenericClient theFhirClient, boolean theIsAsync) { @@ -312,6 +336,7 @@ public static class PatientMergeInputParameters { public Patient resultPatient; public Boolean preview; public Boolean deleteSource; + public Integer batchSize; public Parameters asParametersResource() { Parameters inParams = new Parameters(); @@ -336,6 +361,9 @@ public Parameters asParametersResource() { if (deleteSource != null) { inParams.addParameter().setName("delete-source").setValue(new BooleanType(deleteSource)); } + if (batchSize != null) { + inParams.addParameter().setName("batch-size").setValue(new IntegerType(batchSize)); + } return inParams; } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java index 90b22da35b8e..a57310480fef 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java @@ -22,12 +22,15 @@ import ca.uhn.fhir.batch2.jobs.chunk.FhirIdListWorkChunkJson; import ca.uhn.fhir.batch2.jobs.replacereferences.*; import ca.uhn.fhir.batch2.model.JobDefinition; +import ca.uhn.fhir.batch2.util.Batch2TaskHelper; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; +import org.hl7.fhir.r4.model.Task; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -39,7 +42,8 @@ public class MergeAppCtx { public JobDefinition merge( ReplaceReferencesQueryIdsStep theMergeQueryIds, ReplaceReferenceUpdateStep theMergeUpdateStep, - MergeUpdateTaskReducerStep theMergeUpdateTaskReducerStep) { + MergeUpdateTaskReducerStep theMergeUpdateTaskReducerStep, + MergeErrorHandler mergeErrorHandler) { return JobDefinition.newBuilder() .setJobDefinitionId(JOB_MERGE) .setJobDescription("Merge Resources") @@ -61,19 +65,20 @@ public JobDefinition merge( "Waits for replace reference work to complete and updates Task.", ReplaceReferenceResultsJson.class, theMergeUpdateTaskReducerStep) + .errorHandler(mergeErrorHandler) .build(); } @Bean public ReplaceReferencesQueryIdsStep mergeQueryIdsStep( HapiTransactionService theHapiTransactionService, IBatch2DaoSvc theBatch2DaoSvc) { - return new ReplaceReferencesQueryIdsStep(theHapiTransactionService, theBatch2DaoSvc); + return new ReplaceReferencesQueryIdsStep<>(theHapiTransactionService, theBatch2DaoSvc); } @Bean public ReplaceReferenceUpdateStep mergeUpdateStep( FhirContext theFhirContext, ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc) { - return new ReplaceReferenceUpdateStep(theFhirContext, theReplaceReferencesPatchBundleSvc); + return new ReplaceReferenceUpdateStep<>(theFhirContext, theReplaceReferencesPatchBundleSvc); } @Bean @@ -81,4 +86,10 @@ public MergeUpdateTaskReducerStep mergeUpdateTaskStep( DaoRegistry theDaoRegistry, IHapiTransactionService theHapiTransactionService) { return new MergeUpdateTaskReducerStep(theDaoRegistry, theHapiTransactionService); } + + @Bean + public MergeErrorHandler mergeErrorHandler(DaoRegistry theDaoRegistry, Batch2TaskHelper theBatch2TaskHelper) { + IFhirResourceDao taskDao = theDaoRegistry.getResourceDao(Task.class); + return new MergeErrorHandler(theBatch2TaskHelper, taskDao); + } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeErrorHandler.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeErrorHandler.java new file mode 100644 index 000000000000..05b32274f4e7 --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeErrorHandler.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.batch2.jobs.merge; + +import ca.uhn.fhir.batch2.api.IJobCompletionHandler; +import ca.uhn.fhir.batch2.api.JobCompletionDetails; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; +import ca.uhn.fhir.batch2.util.Batch2TaskHelper; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import org.hl7.fhir.r4.model.Task; + +public class MergeErrorHandler implements IJobCompletionHandler { + + private final Batch2TaskHelper myBatch2TaskHelper; + private final IFhirResourceDao myTaskDao; + + public MergeErrorHandler(Batch2TaskHelper theBatch2TaskHelper, IFhirResourceDao theTaskDao) { + myBatch2TaskHelper = theBatch2TaskHelper; + myTaskDao = theTaskDao; + } + + @Override + public void jobComplete(JobCompletionDetails theDetails) { + + ReplaceReferencesJobParameters jobParameters = theDetails.getParameters(); + + SystemRequestDetails requestDetails = + SystemRequestDetails.forRequestPartitionId(jobParameters.getPartitionId()); + + myBatch2TaskHelper.updateTaskStatusOnJobCompletion(myTaskDao, requestDetails, theDetails); + } +} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java index 715a1799ad3b..ca4e4b208268 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java @@ -49,7 +49,6 @@ public RunOutcome run( throws JobExecutionFailedException { MergeJobParameters mergeJobParameters = theStepExecutionDetails.getParameters(); - SystemRequestDetails requestDetails = SystemRequestDetails.forRequestPartitionId(mergeJobParameters.getPartitionId()); diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java index eb2a49a365ac..95bcfc64ceff 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java @@ -20,8 +20,10 @@ package ca.uhn.fhir.batch2.util; import ca.uhn.fhir.batch2.api.IJobCoordinator; +import ca.uhn.fhir.batch2.api.JobCompletionDetails; import ca.uhn.fhir.batch2.jobs.parameters.BatchJobParametersWithTaskId; import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; +import ca.uhn.fhir.batch2.model.StatusEnum; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -51,4 +53,33 @@ public Task startJobAndCreateAssociatedTask( return task; } + + public void updateTaskStatusOnJobCompletion( + IFhirResourceDao theTaskDao, + RequestDetails theRequestDetails, + JobCompletionDetails theJobCompletionDetails) { + + BatchJobParametersWithTaskId jobParams = theJobCompletionDetails.getParameters(); + + StatusEnum jobStatus = theJobCompletionDetails.getInstance().getStatus(); + Task.TaskStatus taskStatus; + switch (jobStatus) { + case COMPLETED: + taskStatus = Task.TaskStatus.COMPLETED; + break; + case FAILED: + taskStatus = Task.TaskStatus.FAILED; + break; + case CANCELLED: + taskStatus = Task.TaskStatus.CANCELLED; + break; + default: + throw new IllegalStateException(String.format( + "Cannot handle job status '%s'. COMPLETED, FAILED or CANCELLED were expected", jobStatus)); + } + + Task task = theTaskDao.read(jobParams.getTaskId().asIdDt(), theRequestDetails); + task.setStatus(taskStatus); + theTaskDao.update(task, theRequestDetails); + } } From 38dee6ee1c32778d4865293d013376912efb5e16 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 18 Dec 2024 19:15:05 -0500 Subject: [PATCH 094/148] fixed --- .../coordinator/ReductionStepExecutorServiceImpl.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepExecutorServiceImpl.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepExecutorServiceImpl.java index d8ea1eefe55e..3e059a3342ef 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepExecutorServiceImpl.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepExecutorServiceImpl.java @@ -33,6 +33,7 @@ import ca.uhn.fhir.batch2.model.StatusEnum; import ca.uhn.fhir.batch2.model.WorkChunk; import ca.uhn.fhir.batch2.model.WorkChunkStatusEnum; +import ca.uhn.fhir.batch2.progress.JobInstanceStatusUpdater; import ca.uhn.fhir.batch2.util.BatchJobOpenTelemetryUtils; import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.model.sched.HapiJob; @@ -56,6 +57,7 @@ import org.springframework.transaction.annotation.Propagation; import java.util.Collections; +import java.util.Date; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.Map; @@ -85,6 +87,7 @@ public class ReductionStepExecutorServiceImpl implements IReductionStepExecutorS private final Semaphore myCurrentlyExecuting = new Semaphore(1); private final AtomicReference myCurrentlyFinalizingInstanceId = new AtomicReference<>(); private final JobDefinitionRegistry myJobDefinitionRegistry; + private final JobInstanceStatusUpdater myJobInstanceStatusUpdater; private Timer myHeartbeatTimer; /** @@ -97,6 +100,7 @@ public ReductionStepExecutorServiceImpl( myJobPersistence = theJobPersistence; myTransactionService = theTransactionService; myJobDefinitionRegistry = theJobDefinitionRegistry; + myJobInstanceStatusUpdater = new JobInstanceStatusUpdater(theJobDefinitionRegistry); myReducerExecutor = Executors.newSingleThreadExecutor(new CustomizableThreadFactory("batch2-reducer")); } @@ -231,8 +235,10 @@ ReductionStepChunkProcessingResponse executeReductionStep( ourLog.error("Job completion failed for Job {}", instance.getInstanceId(), ex); executeInTransactionWithSynchronization(() -> { + myJobPersistence.updateInstance(instance.getInstanceId(), theInstance -> { - theInstance.setStatus(StatusEnum.FAILED); + theInstance.setEndTime(new Date()); + myJobInstanceStatusUpdater.updateInstanceStatus(theInstance, StatusEnum.FAILED); return true; }); return null; From 487a928f328dce20c03e23f989d8a1423fb63539 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 18 Dec 2024 19:15:19 -0500 Subject: [PATCH 095/148] fixed --- .../batch2/coordinator/ReductionStepExecutorServiceImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepExecutorServiceImpl.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepExecutorServiceImpl.java index 3e059a3342ef..f4eba370003d 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepExecutorServiceImpl.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/coordinator/ReductionStepExecutorServiceImpl.java @@ -235,7 +235,6 @@ ReductionStepChunkProcessingResponse executeReductionStep( ourLog.error("Job completion failed for Job {}", instance.getInstanceId(), ex); executeInTransactionWithSynchronization(() -> { - myJobPersistence.updateInstance(instance.getInstanceId(), theInstance -> { theInstance.setEndTime(new Date()); myJobInstanceStatusUpdater.updateInstanceStatus(theInstance, StatusEnum.FAILED); From 9043b108a9c1ca51b9514ec31cf6c9b60262f6a8 Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Thu, 19 Dec 2024 10:35:42 -0500 Subject: [PATCH 096/148] added error handler to replace references job --- .../provider/merge/ResourceMergeService.java | 3 ++ .../jpa/provider/merge/MergeBatchTest.java | 23 ++++++++ .../jpa/provider/r4/PatientMergeR4Test.java | 9 +--- .../ReplaceReferencesBatchTest.java | 25 +++++++++ .../fhir/batch2/jobs/merge/MergeAppCtx.java | 9 ++-- .../batch2/jobs/merge/MergeErrorHandler.java | 31 ----------- .../ReplaceReferencesAppCtx.java | 14 ++++- .../ReplaceReferencesErrorHandler.java | 54 +++++++++++++++++++ 8 files changed, 125 insertions(+), 43 deletions(-) delete mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeErrorHandler.java create mode 100644 hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesErrorHandler.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 974fe105a1b4..88ec004289b2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -66,6 +66,9 @@ import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_500_INTERNAL_ERROR; +/** + * Service for the FHIR merge operation. Currently only supports merging Patient resources. + */ public class ResourceMergeService { private static final Logger ourLog = LoggerFactory.getLogger(ResourceMergeService.class); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java index 4302a294f973..bf4b0a5ec0b8 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java @@ -16,6 +16,7 @@ import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Task; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.springframework.beans.factory.annotation.Autowired; @@ -23,6 +24,8 @@ import java.util.List; import static ca.uhn.fhir.batch2.jobs.merge.MergeAppCtx.JOB_MERGE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; public class MergeBatchTest extends BaseJpaR4Test { @@ -84,6 +87,26 @@ public void testHappyPath(boolean theDeleteSource, boolean theWithResultResource myTestHelper.getExpectedIdentifiersForTargetAfterMerge(theWithResultResource)); } + @Test + void testMergeJob_JobFails_ErrorHandlerSetsAssociatedTaskStatusToFailed() { + IIdType taskId = createTask(); + + MergeJobParameters jobParams = new MergeJobParameters(); + //use a source that does not exist to force the job to fail + jobParams.setSourceId(new FhirIdJson("Patient", "doesnotexist")); + jobParams.setTargetId(new FhirIdJson(myTestHelper.getTargetPatientId())); + jobParams.setTaskId(taskId); + + JobInstanceStartRequest request = new JobInstanceStartRequest(JOB_MERGE, jobParams); + Batch2JobStartResponse jobStartResponse = myJobCoordinator.startInstance(mySrd, request); + myBatch2JobHelper.awaitJobFailure(jobStartResponse); + + await().until(() -> { + myBatch2JobHelper.runMaintenancePass(); + return myTaskDao.read(taskId, mySrd).getStatus().equals(Task.TaskStatus.FAILED); + }); + } + private IIdType createTask() { Task task = new Task(); task.setStatus(Task.TaskStatus.INPROGRESS); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index b99ecabb7790..2f0c68a766c7 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -11,7 +11,6 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import jakarta.annotation.Nonnull; import jakarta.servlet.http.HttpServletResponse; -import org.apache.jena.base.Sys; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Encounter; @@ -33,7 +32,6 @@ import org.springframework.beans.factory.annotation.Autowired; import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; @@ -259,11 +257,8 @@ void testMerge_SourceResourceCannotBeDeletedBecauseAnotherResourceReferencingSou myBatch2JobHelper.awaitJobFailure(jobId); - await().until(() -> { - Task taskAfterJobFailure = myTaskDao.read(task.getIdElement().toVersionless(), mySrd); - ourLog.info("Check if Task status is FAILED: {}", taskAfterJobFailure.getStatus()); - return Task.TaskStatus.FAILED.equals(taskAfterJobFailure.getStatus()); - }); + Task taskAfterJobFailure = myTaskDao.read(task.getIdElement().toVersionless(), mySrd); + assertThat(taskAfterJobFailure.getStatus()).isEqualTo(Task.TaskStatus.FAILED); } @ParameterizedTest diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java index 0af294796dd7..18737a6bfd47 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java @@ -2,6 +2,7 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; +import ca.uhn.fhir.batch2.jobs.merge.MergeJobParameters; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceResultsJson; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; import ca.uhn.fhir.batch2.model.JobInstance; @@ -23,7 +24,10 @@ import java.util.List; +import static ca.uhn.fhir.batch2.jobs.merge.MergeAppCtx.JOB_MERGE; import static ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx.JOB_REPLACE_REFERENCES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; public class ReplaceReferencesBatchTest extends BaseJpaR4Test { @@ -70,6 +74,27 @@ public void testHappyPath() { myTestHelper.assertAllReferencesUpdated(); } + + @Test + void testReplaceReferencesJob_JobFails_ErrorHandlerSetsAssociatedTaskStatusToFailed() { + IIdType taskId = createReplaceReferencesTask(); + + ReplaceReferencesJobParameters jobParams = new ReplaceReferencesJobParameters(); + jobParams.setSourceId(new FhirIdJson(myTestHelper.getSourcePatientId())); + //use a target that does not exist to force the job to fail + jobParams.setTargetId(new FhirIdJson("Patient", "doesnotexist")); + jobParams.setTaskId(taskId); + + JobInstanceStartRequest request = new JobInstanceStartRequest(JOB_REPLACE_REFERENCES, jobParams); + Batch2JobStartResponse jobStartResponse = myJobCoordinator.startInstance(mySrd, request); + myBatch2JobHelper.awaitJobFailure(jobStartResponse); + + await().until(() -> { + myBatch2JobHelper.runMaintenancePass(); + return myTaskDao.read(taskId, mySrd).getStatus().equals(Task.TaskStatus.FAILED); + }); + } + private IIdType createReplaceReferencesTask() { Task task = new Task(); task.setStatus(Task.TaskStatus.INPROGRESS); diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java index a57310480fef..ccd9bc57667e 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java @@ -43,7 +43,7 @@ public JobDefinition merge( ReplaceReferencesQueryIdsStep theMergeQueryIds, ReplaceReferenceUpdateStep theMergeUpdateStep, MergeUpdateTaskReducerStep theMergeUpdateTaskReducerStep, - MergeErrorHandler mergeErrorHandler) { + ReplaceReferencesErrorHandler theMergeErrorHandler) { return JobDefinition.newBuilder() .setJobDefinitionId(JOB_MERGE) .setJobDescription("Merge Resources") @@ -65,7 +65,7 @@ public JobDefinition merge( "Waits for replace reference work to complete and updates Task.", ReplaceReferenceResultsJson.class, theMergeUpdateTaskReducerStep) - .errorHandler(mergeErrorHandler) + .errorHandler(theMergeErrorHandler) .build(); } @@ -88,8 +88,9 @@ public MergeUpdateTaskReducerStep mergeUpdateTaskStep( } @Bean - public MergeErrorHandler mergeErrorHandler(DaoRegistry theDaoRegistry, Batch2TaskHelper theBatch2TaskHelper) { + public ReplaceReferencesErrorHandler mergeErorHandler( + DaoRegistry theDaoRegistry, Batch2TaskHelper theBatch2TaskHelper) { IFhirResourceDao taskDao = theDaoRegistry.getResourceDao(Task.class); - return new MergeErrorHandler(theBatch2TaskHelper, taskDao); + return new ReplaceReferencesErrorHandler<>(theBatch2TaskHelper, taskDao); } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeErrorHandler.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeErrorHandler.java deleted file mode 100644 index 05b32274f4e7..000000000000 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeErrorHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -package ca.uhn.fhir.batch2.jobs.merge; - -import ca.uhn.fhir.batch2.api.IJobCompletionHandler; -import ca.uhn.fhir.batch2.api.JobCompletionDetails; -import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; -import ca.uhn.fhir.batch2.util.Batch2TaskHelper; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; -import ca.uhn.fhir.rest.api.server.SystemRequestDetails; -import org.hl7.fhir.r4.model.Task; - -public class MergeErrorHandler implements IJobCompletionHandler { - - private final Batch2TaskHelper myBatch2TaskHelper; - private final IFhirResourceDao myTaskDao; - - public MergeErrorHandler(Batch2TaskHelper theBatch2TaskHelper, IFhirResourceDao theTaskDao) { - myBatch2TaskHelper = theBatch2TaskHelper; - myTaskDao = theTaskDao; - } - - @Override - public void jobComplete(JobCompletionDetails theDetails) { - - ReplaceReferencesJobParameters jobParameters = theDetails.getParameters(); - - SystemRequestDetails requestDetails = - SystemRequestDetails.forRequestPartitionId(jobParameters.getPartitionId()); - - myBatch2TaskHelper.updateTaskStatusOnJobCompletion(myTaskDao, requestDetails, theDetails); - } -} diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java index 51ca58276e46..9fd55181d1f4 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesAppCtx.java @@ -21,11 +21,14 @@ import ca.uhn.fhir.batch2.jobs.chunk.FhirIdListWorkChunkJson; import ca.uhn.fhir.batch2.model.JobDefinition; +import ca.uhn.fhir.batch2.util.Batch2TaskHelper; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.svc.IBatch2DaoSvc; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; +import org.hl7.fhir.r4.model.Task; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -38,7 +41,8 @@ public JobDefinition replaceReferencesJobDefinit ReplaceReferencesQueryIdsStep theReplaceReferencesQueryIds, ReplaceReferenceUpdateStep theReplaceReferenceUpdateStep, ReplaceReferenceUpdateTaskReducerStep - theReplaceReferenceUpdateTaskReducerStep) { + theReplaceReferenceUpdateTaskReducerStep, + ReplaceReferencesErrorHandler theReplaceReferencesErrorHandler) { return JobDefinition.newBuilder() .setJobDefinitionId(JOB_REPLACE_REFERENCES) .setJobDescription("Replace References") @@ -60,6 +64,7 @@ public JobDefinition replaceReferencesJobDefinit "Waits for replace reference work to complete and updates Task.", ReplaceReferenceResultsJson.class, theReplaceReferenceUpdateTaskReducerStep) + .errorHandler(theReplaceReferencesErrorHandler) .build(); } @@ -80,4 +85,11 @@ public ReplaceReferenceUpdateTaskReducerStep rep DaoRegistry theDaoRegistry) { return new ReplaceReferenceUpdateTaskReducerStep<>(theDaoRegistry); } + + @Bean + public ReplaceReferencesErrorHandler replaceReferencesErrorHandler( + DaoRegistry theDaoRegistry, Batch2TaskHelper theBatch2TaskHelper) { + IFhirResourceDao taskDao = theDaoRegistry.getResourceDao(Task.class); + return new ReplaceReferencesErrorHandler<>(theBatch2TaskHelper, taskDao); + } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesErrorHandler.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesErrorHandler.java new file mode 100644 index 000000000000..76dd8643e835 --- /dev/null +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesErrorHandler.java @@ -0,0 +1,54 @@ +/*- + * #%L + * hapi-fhir-storage-batch2-jobs + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.batch2.jobs.replacereferences; + +import ca.uhn.fhir.batch2.api.IJobCompletionHandler; +import ca.uhn.fhir.batch2.api.JobCompletionDetails; +import ca.uhn.fhir.batch2.util.Batch2TaskHelper; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import org.hl7.fhir.r4.model.Task; + +/** + * This class is the error handler for ReplaceReferences and Merge jobs. + * It updates the status of the associated task. + */ +public class ReplaceReferencesErrorHandler + implements IJobCompletionHandler { + + private final Batch2TaskHelper myBatch2TaskHelper; + private final IFhirResourceDao myTaskDao; + + public ReplaceReferencesErrorHandler(Batch2TaskHelper theBatch2TaskHelper, IFhirResourceDao theTaskDao) { + myBatch2TaskHelper = theBatch2TaskHelper; + myTaskDao = theTaskDao; + } + + @Override + public void jobComplete(JobCompletionDetails theDetails) { + + PT jobParameters = theDetails.getParameters(); + + SystemRequestDetails requestDetails = + SystemRequestDetails.forRequestPartitionId(jobParameters.getPartitionId()); + + myBatch2TaskHelper.updateTaskStatusOnJobCompletion(myTaskDao, requestDetails, theDetails); + } +} From 187feb1c186871e390cd9e18f5d9a33c618f97d5 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 13:48:08 -0500 Subject: [PATCH 097/148] review --- .../main/java/ca/uhn/fhir/util/StopLimitAccumulator.java | 6 +++--- .../jpa/provider/merge/MergeOperationInputParameters.java | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java index d15d4ec1549d..f003214d03ff 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java @@ -36,13 +36,13 @@ private StopLimitAccumulator(List theList, boolean theIsTruncated) { isTruncated = theIsTruncated; } - public static StopLimitAccumulator fromStreamAndLimit(@Nonnull Stream thePidStream, long theLimit) { + public static StopLimitAccumulator fromStreamAndLimit(@Nonnull Stream theItemStream, long theLimit) { assert theLimit > 0; AtomicBoolean isBeyondLimit = new AtomicBoolean(false); List accumulator = new ArrayList<>(); - thePidStream - .limit(theLimit + 1) // Fetch one extra item to see if there are more items past our limit + theItemStream + .limit(theLimit + 1) // Fetch one extra item to see if there are any more items past our limit .forEach(item -> { if (accumulator.size() < theLimit) { accumulator.add(item); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParameters.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParameters.java index cbc3915de079..85c5e5fd0016 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParameters.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParameters.java @@ -25,6 +25,9 @@ import java.util.List; +/** + * See Patient $merge spec + */ public abstract class MergeOperationInputParameters { private List mySourceResourceIdentifiers; From 7f30354ac21d67fe77289e6da13577e53b8dc4f0 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 13:57:08 -0500 Subject: [PATCH 098/148] javadoc --- .../uhn/fhir/jpa/dao/data/IResourceLinkDao.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java index 05342e8695d7..ce545c154205 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java @@ -49,12 +49,28 @@ public interface IResourceLinkDao extends JpaRepository, IHa @Query("SELECT t FROM ResourceLink t LEFT JOIN FETCH t.myTargetResource tr WHERE t.myId in :pids") List findByPidAndFetchTargetDetails(@Param("pids") List thePids); + /** + * Stream Resource Ids of all resources that have a reference to the provided resource id + * + * @param theTargetResourceType the resource type part of the id + * @param theTargetResourceFhirId the value part of the id + * @return + */ + @Query( "SELECT DISTINCT new ca.uhn.fhir.model.primitive.IdDt(t.mySourceResourceType, t.mySourceResource.myFhirId) FROM ResourceLink t WHERE t.myTargetResourceType = :resourceType AND t.myTargetResource.myFhirId = :resourceFhirId") Stream streamSourceIdsForTargetFhirId( @Param("resourceType") String theTargetResourceType, @Param("resourceFhirId") String theTargetResourceFhirId); + /** + * Count the number of resources that have a reference to the provided resource id + * + * @param theTargetResourceType the resource type part of the id + * @param theTargetResourceFhirId the value part of the id + * @return + */ + @Query( "SELECT COUNT(DISTINCT t.mySourceResourcePid) FROM ResourceLink t WHERE t.myTargetResourceType = :resourceType AND t.myTargetResource.myFhirId = :resourceFhirId") Integer countResourcesTargetingFhirTypeAndFhirId( From 542b469e787de4ec1b0b51bc6abcbaae594344ed Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 14:06:43 -0500 Subject: [PATCH 099/148] organize imports --- .../fhir/util/StopLimitAccumulatorTest.java | 5 +- .../fhir/jpa/dao/data/IResourceLinkDao.java | 2 - .../provider/merge/MergeOperationOutcome.java | 3 + .../PatientMergeOperationInputParameters.java | 3 + .../provider/merge/ResourceMergeService.java | 389 +++++++++--------- .../jpa/provider/merge/MergeBatchTest.java | 1 - .../ReplaceReferencesBatchTest.java | 7 - .../fhir/batch2/jobs/merge/MergeAppCtx.java | 6 +- 8 files changed, 210 insertions(+), 206 deletions(-) diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/StopLimitAccumulatorTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/StopLimitAccumulatorTest.java index 825571336969..3f826424e3f1 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/StopLimitAccumulatorTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/StopLimitAccumulatorTest.java @@ -5,7 +5,10 @@ import java.util.List; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class StopLimitAccumulatorTest { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java index ce545c154205..cd4980b32b8e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceLinkDao.java @@ -56,7 +56,6 @@ public interface IResourceLinkDao extends JpaRepository, IHa * @param theTargetResourceFhirId the value part of the id * @return */ - @Query( "SELECT DISTINCT new ca.uhn.fhir.model.primitive.IdDt(t.mySourceResourceType, t.mySourceResource.myFhirId) FROM ResourceLink t WHERE t.myTargetResourceType = :resourceType AND t.myTargetResource.myFhirId = :resourceFhirId") Stream streamSourceIdsForTargetFhirId( @@ -70,7 +69,6 @@ Stream streamSourceIdsForTargetFhirId( * @param theTargetResourceFhirId the value part of the id * @return */ - @Query( "SELECT COUNT(DISTINCT t.mySourceResourcePid) FROM ResourceLink t WHERE t.myTargetResourceType = :resourceType AND t.myTargetResource.myFhirId = :resourceFhirId") Integer countResourcesTargetingFhirTypeAndFhirId( diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationOutcome.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationOutcome.java index a30d929c87a4..4953b667e6ec 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationOutcome.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationOutcome.java @@ -22,6 +22,9 @@ import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; +/** + * See Patient $merge spec + */ public class MergeOperationOutcome { private IBaseOperationOutcome myOperationOutcome; private int myHttpStatusCode; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java index c480d1034677..9144503b975e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java @@ -25,6 +25,9 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER; +/** + * See Patient $merge spec + */ public class PatientMergeOperationInputParameters extends MergeOperationInputParameters { public PatientMergeOperationInputParameters(int theBatchSize) { super(theBatchSize); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 88ec004289b2..1522533d2130 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -67,7 +67,7 @@ import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_500_INTERNAL_ERROR; /** - * Service for the FHIR merge operation. Currently only supports merging Patient resources. + * Service for the FHIR $merge operation. Currently only supports Patient/$merge. The plan is to expand to other resource types. */ public class ResourceMergeService { private static final Logger ourLog = LoggerFactory.getLogger(ResourceMergeService.class); @@ -83,12 +83,12 @@ public class ResourceMergeService { private final Batch2TaskHelper myBatch2TaskHelper; public ResourceMergeService( - DaoRegistry theDaoRegistry, - IReplaceReferencesSvc theReplaceReferencesSvc, - IHapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator, - Batch2TaskHelper theBatch2TaskHelper) { + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + IHapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper) { myPatientDao = theDaoRegistry.getResourceDao(Patient.class); myTaskDao = theDaoRegistry.getResourceDao(Task.class); @@ -102,14 +102,16 @@ public ResourceMergeService( } /** - * Implementation of the $merge operation for resources + * Perform the $merge operation. If the number of resources to be changed exceeds the provided batch size, + * then switch to async mode. See the Patient $merge spec + * for details on what the difference is between synchronous and asynchronous mode. * * @param theMergeOperationParameters the merge operation parameters * @param theRequestDetails the request details * @return the merge outcome containing OperationOutcome and HTTP status code */ public MergeOperationOutcome merge( - MergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + MergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); @@ -132,9 +134,9 @@ public MergeOperationOutcome merge( } private void validateAndMerge( - MergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { ValidationResult validationResult = validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); @@ -147,17 +149,16 @@ private void validateAndMerge( if (theMergeOperationParameters.getPreview()) { handlePreview( - sourceResource, targetResource, theMergeOperationParameters, theRequestDetails, theMergeOutcome); - return; + sourceResource, targetResource, theMergeOperationParameters, theRequestDetails, theMergeOutcome); + } else { + doMerge(theMergeOperationParameters, sourceResource, targetResource, theRequestDetails, theMergeOutcome); } - - doMerge(theMergeOperationParameters, sourceResource, targetResource, theRequestDetails, theMergeOutcome); } private ValidationResult validate( - MergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { ValidationResult validationResult = new ValidationResult(); IBaseOperationOutcome operationOutcome = theMergeOutcome.getOperationOutcome(); @@ -170,7 +171,7 @@ private ValidationResult validate( // cast to Patient, since we only support merging Patient resources for now Patient sourceResource = - (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); + (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (sourceResource == null) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); @@ -182,7 +183,7 @@ private ValidationResult validate( // cast to Patient, since we only support merging Patient resources for now Patient targetResource = - (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); + (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (targetResource == null) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); @@ -199,7 +200,7 @@ private ValidationResult validate( } if (!validateResultResourceIfExists( - theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { + theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); return validationResult; } @@ -209,19 +210,19 @@ private ValidationResult validate( } private void handlePreview( - Patient theSourceResource, - Patient theTargetResource, - MergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + Patient theSourceResource, + Patient theTargetResource, + MergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); + theSourceResource.getIdElement().toVersionless(), theRequestDetails); // in preview mode, we should also return how the target would look like Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); Patient targetPatientAsIfUpdated = myMergeHelper.prepareTargetPatientForUpdate( - theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); + theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); // adding +2 because the source and the target resources themselved would be updated as well @@ -231,72 +232,72 @@ private void handlePreview( } private void doMerge( - MergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( - theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); + theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); if (theRequestDetails.isPreferAsync()) { // client prefers async processing, do async doMergeAsync( + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); + } else { + // count the number of refs, if it is larger than batch size then process async, otherwise process sync + Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( + theSourceResource.getIdElement().toVersionless(), theRequestDetails); + if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { + doMergeAsync( theMergeOperationParameters, theSourceResource, theTargetResource, theRequestDetails, theMergeOutcome, partitionId); - } else { - // count the number of refs, if it is larger than batch size then process async, otherwise process sync - Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); - if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { - doMergeAsync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); } else { doMergeSync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } } } private void doMergeSync( - MergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId partitionId) { + MergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId partitionId) { ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest( - theSourceResource.getIdElement(), - theTargetResource.getIdElement(), - theMergeOperationParameters.getBatchSize(), - partitionId); + theSourceResource.getIdElement(), + theTargetResource.getIdElement(), + theMergeOperationParameters.getBatchSize(), + partitionId); replaceReferenceRequest.setForceSync(true); myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); Patient updatedTarget = myMergeHelper.updateMergedResourcesAfterReferencesReplaced( - myHapiTransactionService, - theSourceResource, - theTargetResource, - (Patient) theMergeOperationParameters.getResultResource(), - theMergeOperationParameters.getDeleteSource(), - theRequestDetails); + myHapiTransactionService, + theSourceResource, + theTargetResource, + (Patient) theMergeOperationParameters.getResultResource(), + theMergeOperationParameters.getDeleteSource(), + theRequestDetails); theMergeOutcome.setUpdatedTargetResource(updatedTarget); String detailsText = "Merge operation completed successfully."; @@ -304,29 +305,29 @@ private void doMergeSync( } private void doMergeAsync( - MergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId partitionId) { + MergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId partitionId) { MergeJobParameters mergeJobParameters = new MergeJobParameters(); if (theMergeOperationParameters.getResultResource() != null) { mergeJobParameters.setResultResource(myFhirContext - .newJsonParser() - .encodeResourceToString(theMergeOperationParameters.getResultResource())); + .newJsonParser() + .encodeResourceToString(theMergeOperationParameters.getResultResource())); } mergeJobParameters.setDeleteSource(theMergeOperationParameters.getDeleteSource()); mergeJobParameters.setBatchSize(theMergeOperationParameters.getBatchSize()); mergeJobParameters.setSourceId( - new FhirIdJson(theSourceResource.getIdElement().toVersionless())); + new FhirIdJson(theSourceResource.getIdElement().toVersionless())); mergeJobParameters.setTargetId( - new FhirIdJson(theTargetResource.getIdElement().toVersionless())); + new FhirIdJson(theTargetResource.getIdElement().toVersionless())); mergeJobParameters.setPartitionId(partitionId); Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( - myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); + myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); task.getMeta().setVersionId(null); @@ -334,15 +335,15 @@ private void doMergeAsync( theMergeOutcome.setHttpStatusCode(STATUS_HTTP_202_ACCEPTED); String detailsText = "Merge request is accepted, and will be processed asynchronously. See" - + " task resource returned in this response for details."; + + " task resource returned in this response for details."; addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); } private boolean validateResultResourceIfExists( - MergeOperationInputParameters theMergeOperationParameters, - Patient theResolvedTargetResource, - Patient theResolvedSourceResource, - IBaseOperationOutcome theOperationOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + Patient theResolvedTargetResource, + Patient theResolvedSourceResource, + IBaseOperationOutcome theOperationOutcome) { if (theMergeOperationParameters.getResultResource() == null) { // result resource is not provided, no further validation is needed @@ -356,21 +357,21 @@ private boolean validateResultResourceIfExists( // validate the result resource's id as same as the target resource if (!theResolvedTargetResource.getIdElement().toVersionless().equals(theResultResource.getIdElement())) { String msg = String.format( - "'%s' must have the same versionless id as the actual resolved target resource. " - + "The actual resolved target resource's id is: '%s'", - theMergeOperationParameters.getResultResourceParameterName(), - theResolvedTargetResource.getIdElement().toVersionless().getValue()); + "'%s' must have the same versionless id as the actual resolved target resource. " + + "The actual resolved target resource's id is: '%s'", + theMergeOperationParameters.getResultResourceParameterName(), + theResolvedTargetResource.getIdElement().toVersionless().getValue()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); isValid = false; } // validate the result resource contains the identifiers provided in the target identifiers param if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { + && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { String msg = String.format( - "'%s' must have all the identifiers provided in %s", - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "'%s' must have all the identifiers provided in %s", + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); isValid = false; } @@ -380,11 +381,11 @@ private boolean validateResultResourceIfExists( // if the source resource is being deleted, the result resource must not have a replaces link to the source // resource if (!validateResultResourceReplacesLinkToSourceResource( - theResultResource, - theResolvedSourceResource, - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getDeleteSource(), - theOperationOutcome)) { + theResultResource, + theResolvedSourceResource, + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getDeleteSource(), + theOperationOutcome)) { isValid = false; } @@ -396,9 +397,9 @@ private boolean hasAllIdentifiers(Patient theResource, List List identifiersInResource = theResource.getIdentifier(); for (CanonicalIdentifier identifier : theIdentifiers) { boolean identifierFound = identifiersInResource.stream() - .anyMatch(i -> i.getSystem() - .equals(identifier.getSystemElement().getValueAsString()) - && i.getValue().equals(identifier.getValueElement().getValueAsString())); + .anyMatch(i -> i.getSystem() + .equals(identifier.getSystemElement().getValueAsString()) + && i.getValue().equals(identifier.getValueElement().getValueAsString())); if (!identifierFound) { return false; @@ -408,45 +409,45 @@ private boolean hasAllIdentifiers(Patient theResource, List } private List getLinksToResource( - Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { + Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { List links = getLinksOfTypeWithNonNullReference(theResource, theLinkType); return links.stream() - .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) - .collect(Collectors.toList()); + .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) + .collect(Collectors.toList()); } private boolean validateResultResourceReplacesLinkToSourceResource( - Patient theResultResource, - Patient theResolvedSourceResource, - String theResultResourceParameterName, - boolean theDeleteSource, - IBaseOperationOutcome theOperationOutcome) { + Patient theResultResource, + Patient theResolvedSourceResource, + String theResultResourceParameterName, + boolean theDeleteSource, + IBaseOperationOutcome theOperationOutcome) { // the result resource must have the replaces link set to the source resource List replacesLinkToSourceResource = getLinksToResource( - theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); + theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); if (theDeleteSource) { if (!replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must not have a 'replaces' link to the source resource " - + "when the source resource will be deleted, as the link may prevent deleting the source " - + "resource.", - theResultResourceParameterName); + "'%s' must not have a 'replaces' link to the source resource " + + "when the source resource will be deleted, as the link may prevent deleting the source " + + "resource.", + theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } } else { if (replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); + "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } if (replacesLinkToSourceResource.size() > 1) { String msg = String.format( - "'%s' has multiple 'replaces' links to the source resource. There should be only one.", - theResultResourceParameterName); + "'%s' has multiple 'replaces' links to the source resource. There should be only one.", + theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } @@ -467,7 +468,7 @@ protected List getLinksOfTypeWithNonNullReference(Patient theResource } private boolean validateSourceAndTargetAreSuitableForMerge( - Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { + Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { if (theSourceResource.getId().equalsIgnoreCase(theTargetResource.getId())) { String msg = "Source and target resources are the same resource."; @@ -483,25 +484,25 @@ private boolean validateSourceAndTargetAreSuitableForMerge( } List replacedByLinksInTarget = - getLinksOfTypeWithNonNullReference(theTargetResource, Patient.LinkType.REPLACEDBY); + getLinksOfTypeWithNonNullReference(theTargetResource, Patient.LinkType.REPLACEDBY); if (!replacedByLinksInTarget.isEmpty()) { String ref = replacedByLinksInTarget.get(0).getReference(); String msg = String.format( - "Target resource was previously replaced by a resource with reference '%s', it " - + "is not a suitable target for merging.", - ref); + "Target resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable target for merging.", + ref); addErrorToOperationOutcome(outcome, msg, "invalid"); return false; } List replacedByLinksInSource = - getLinksOfTypeWithNonNullReference(theSourceResource, Patient.LinkType.REPLACEDBY); + getLinksOfTypeWithNonNullReference(theSourceResource, Patient.LinkType.REPLACEDBY); if (!replacedByLinksInSource.isEmpty()) { String ref = replacedByLinksInSource.get(0).getReference(); String msg = String.format( - "Source resource was previously replaced by a resource with reference '%s', it " - + "is not a suitable source for merging.", - ref); + "Source resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable source for merging.", + ref); addErrorToOperationOutcome(outcome, msg, "invalid"); return false; } @@ -517,59 +518,59 @@ private boolean validateSourceAndTargetAreSuitableForMerge( * @return true if the parameters are valid, false otherwise */ private boolean validateMergeOperationParameters( - MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { List errorMessages = new ArrayList<>(); if (!theMergeOperationParameters.hasAtLeastOneSourceIdentifier() - && theMergeOperationParameters.getSourceResource() == null) { + && theMergeOperationParameters.getSourceResource() == null) { String msg = String.format( - "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", - theMergeOperationParameters.getSourceResourceParameterName(), - theMergeOperationParameters.getSourceIdentifiersParameterName()); + "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); errorMessages.add(msg); } // Spec has conflicting information about this case if (theMergeOperationParameters.hasAtLeastOneSourceIdentifier() - && theMergeOperationParameters.getSourceResource() != null) { + && theMergeOperationParameters.getSourceResource() != null) { String msg = String.format( - "Source resource must be provided either by '%s' or by '%s', not both.", - theMergeOperationParameters.getSourceResourceParameterName(), - theMergeOperationParameters.getSourceIdentifiersParameterName()); + "Source resource must be provided either by '%s' or by '%s', not both.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); errorMessages.add(msg); } if (!theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && theMergeOperationParameters.getTargetResource() == null) { + && theMergeOperationParameters.getTargetResource() == null) { String msg = String.format( - "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", - theMergeOperationParameters.getTargetResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); errorMessages.add(msg); } // Spec has conflicting information about this case if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && theMergeOperationParameters.getTargetResource() != null) { + && theMergeOperationParameters.getTargetResource() != null) { String msg = String.format( - "Target resource must be provided either by '%s' or by '%s', not both.", - theMergeOperationParameters.getTargetResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "Target resource must be provided either by '%s' or by '%s', not both.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); errorMessages.add(msg); } Reference sourceRef = (Reference) theMergeOperationParameters.getSourceResource(); if (sourceRef != null && !sourceRef.hasReference()) { String msg = String.format( - "Reference specified in '%s' parameter does not have a reference element.", - theMergeOperationParameters.getSourceResourceParameterName()); + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getSourceResourceParameterName()); errorMessages.add(msg); } Reference targetRef = (Reference) theMergeOperationParameters.getTargetResource(); if (targetRef != null && !targetRef.hasReference()) { String msg = String.format( - "Reference specified in '%s' parameter does not have a reference element.", - theMergeOperationParameters.getTargetResourceParameterName()); + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getTargetResourceParameterName()); errorMessages.add(msg); } @@ -586,43 +587,43 @@ private boolean validateMergeOperationParameters( } private IBaseResource resolveSourceResource( - MergeOperationInputParameters theOperationParameters, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { return resolveResource( - theOperationParameters.getSourceResource(), - theOperationParameters.getSourceIdentifiers(), - theRequestDetails, - theOutcome, - theOperationParameters.getSourceResourceParameterName(), - theOperationParameters.getSourceIdentifiersParameterName()); + theOperationParameters.getSourceResource(), + theOperationParameters.getSourceIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getSourceResourceParameterName(), + theOperationParameters.getSourceIdentifiersParameterName()); } private IBaseResource resolveTargetResource( - MergeOperationInputParameters theOperationParameters, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { return resolveResource( - theOperationParameters.getTargetResource(), - theOperationParameters.getTargetIdentifiers(), - theRequestDetails, - theOutcome, - theOperationParameters.getTargetResourceParameterName(), - theOperationParameters.getTargetIdentifiersParameterName()); + theOperationParameters.getTargetResource(), + theOperationParameters.getTargetIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getTargetResourceParameterName(), + theOperationParameters.getTargetIdentifiersParameterName()); } private IBaseResource resolveResourceByIdentifiers( - List theIdentifiers, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationParameterName) { + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { SearchParameterMap searchParameterMap = new SearchParameterMap(); TokenAndListParam tokenAndListParam = new TokenAndListParam(); for (CanonicalIdentifier identifier : theIdentifiers) { TokenParam tokenParam = new TokenParam( - identifier.getSystemElement().getValueAsString(), - identifier.getValueElement().getValueAsString()); + identifier.getSystemElement().getValueAsString(), + identifier.getValueElement().getValueAsString()); tokenAndListParam.addAnd(tokenParam); } searchParameterMap.add("identifier", tokenAndListParam); @@ -632,13 +633,13 @@ private IBaseResource resolveResourceByIdentifiers( List resources = bundle.getAllResources(); if (resources.isEmpty()) { String msg = String.format( - "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); + "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "not-found"); return null; } if (resources.size() > 1) { String msg = String.format( - "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); + "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "multiple-matches"); return null; } @@ -647,10 +648,10 @@ private IBaseResource resolveResourceByIdentifiers( } private IBaseResource resolveResourceByReference( - IBaseReference theReference, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationParameterName) { + IBaseReference theReference, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { // TODO Emre: why does IBaseReference not have getIdentifier or hasReference methods? // casting it to r4.Reference for now Reference r4ref = (Reference) theReference; @@ -661,19 +662,19 @@ private IBaseResource resolveResourceByReference( resource = myPatientDao.read(theResourceId.toVersionless(), theRequestDetails); } catch (ResourceNotFoundException e) { String msg = String.format( - "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); + "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "not-found"); return null; } if (theResourceId.hasVersionIdPart() - && !theResourceId - .getVersionIdPart() - .equals(resource.getIdElement().getVersionIdPart())) { + && !theResourceId + .getVersionIdPart() + .equals(resource.getIdElement().getVersionIdPart())) { String msg = String.format( - "The reference in '%s' parameter has a version specified, " - + "but it is not the latest version of the resource", - theOperationParameterName); + "The reference in '%s' parameter has a version specified, " + + "but it is not the latest version of the resource", + theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "conflict"); return null; } @@ -682,25 +683,25 @@ private IBaseResource resolveResourceByReference( } private IBaseResource resolveResource( - IBaseReference theReference, - List theIdentifiers, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationReferenceParameterName, - String theOperationIdentifiersParameterName) { + IBaseReference theReference, + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationReferenceParameterName, + String theOperationIdentifiersParameterName) { if (theReference != null) { return resolveResourceByReference( - theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); + theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); } return resolveResourceByIdentifiers( - theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); + theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); } private void addInfoToOperationOutcome( - IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { + IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { IBase issue = - OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); OperationOutcomeUtil.addDetailsToIssue(myFhirContext, issue, null, null, theDetailsText); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java index bf4b0a5ec0b8..196fb407462c 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeBatchTest.java @@ -24,7 +24,6 @@ import java.util.List; import static ca.uhn.fhir.batch2.jobs.merge.MergeAppCtx.JOB_MERGE; -import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; public class MergeBatchTest extends BaseJpaR4Test { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java index 18737a6bfd47..6779dfef9ac8 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesBatchTest.java @@ -2,8 +2,6 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; -import ca.uhn.fhir.batch2.jobs.merge.MergeJobParameters; -import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceResultsJson; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; @@ -12,9 +10,7 @@ import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; import ca.uhn.fhir.jpa.test.BaseJpaR4Test; import ca.uhn.fhir.jpa.test.Batch2JobHelper; -import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; -import ca.uhn.fhir.util.JsonUtil; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Task; @@ -24,11 +20,8 @@ import java.util.List; -import static ca.uhn.fhir.batch2.jobs.merge.MergeAppCtx.JOB_MERGE; import static ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx.JOB_REPLACE_REFERENCES; -import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -import static org.junit.jupiter.api.Assertions.assertEquals; public class ReplaceReferencesBatchTest extends BaseJpaR4Test { diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java index ccd9bc57667e..9bb003174c7f 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java @@ -20,7 +20,11 @@ package ca.uhn.fhir.batch2.jobs.merge; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdListWorkChunkJson; -import ca.uhn.fhir.batch2.jobs.replacereferences.*; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencePatchOutcomeJson; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceResultsJson; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferenceUpdateStep; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesErrorHandler; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesQueryIdsStep; import ca.uhn.fhir.batch2.model.JobDefinition; import ca.uhn.fhir.batch2.util.Batch2TaskHelper; import ca.uhn.fhir.context.FhirContext; From 6e47d8770a026edadddfcf283a5d10a8faf2e5bb Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 14:16:10 -0500 Subject: [PATCH 100/148] make ValidationResult immutable --- .../provider/merge/ResourceMergeService.java | 421 +++++++++--------- 1 file changed, 212 insertions(+), 209 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 1522533d2130..e3b8d0bc2038 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -83,12 +83,12 @@ public class ResourceMergeService { private final Batch2TaskHelper myBatch2TaskHelper; public ResourceMergeService( - DaoRegistry theDaoRegistry, - IReplaceReferencesSvc theReplaceReferencesSvc, - IHapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator, - Batch2TaskHelper theBatch2TaskHelper) { + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + IHapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper) { myPatientDao = theDaoRegistry.getResourceDao(Patient.class); myTaskDao = theDaoRegistry.getResourceDao(Task.class); @@ -111,7 +111,7 @@ public ResourceMergeService( * @return the merge outcome containing OperationOutcome and HTTP status code */ public MergeOperationOutcome merge( - MergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + MergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); @@ -134,9 +134,9 @@ public MergeOperationOutcome merge( } private void validateAndMerge( - MergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { ValidationResult validationResult = validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); @@ -149,80 +149,69 @@ private void validateAndMerge( if (theMergeOperationParameters.getPreview()) { handlePreview( - sourceResource, targetResource, theMergeOperationParameters, theRequestDetails, theMergeOutcome); + sourceResource, targetResource, theMergeOperationParameters, theRequestDetails, theMergeOutcome); } else { doMerge(theMergeOperationParameters, sourceResource, targetResource, theRequestDetails, theMergeOutcome); } } private ValidationResult validate( - MergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { - ValidationResult validationResult = new ValidationResult(); IBaseOperationOutcome operationOutcome = theMergeOutcome.getOperationOutcome(); if (!validateMergeOperationParameters(theMergeOperationParameters, operationOutcome)) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); - validationResult.isValid = false; - return validationResult; + return ValidationResult.invalidResult(); } // cast to Patient, since we only support merging Patient resources for now Patient sourceResource = - (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); + (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (sourceResource == null) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); - validationResult.isValid = false; - return validationResult; + return ValidationResult.invalidResult(); } - validationResult.sourceResource = sourceResource; - // cast to Patient, since we only support merging Patient resources for now Patient targetResource = - (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); + (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (targetResource == null) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); - validationResult.isValid = false; - return validationResult; + return ValidationResult.invalidResult(); } - validationResult.targetResource = targetResource; - if (!validateSourceAndTargetAreSuitableForMerge(sourceResource, targetResource, operationOutcome)) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); - validationResult.isValid = false; - return validationResult; + return ValidationResult.invalidResult(); } if (!validateResultResourceIfExists( - theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { + theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); - return validationResult; + return ValidationResult.invalidResult(); } - - validationResult.isValid = true; - return validationResult; + return ValidationResult.validResult(sourceResource, targetResource); } private void handlePreview( - Patient theSourceResource, - Patient theTargetResource, - MergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + Patient theSourceResource, + Patient theTargetResource, + MergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); + theSourceResource.getIdElement().toVersionless(), theRequestDetails); // in preview mode, we should also return how the target would look like Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); Patient targetPatientAsIfUpdated = myMergeHelper.prepareTargetPatientForUpdate( - theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); + theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); // adding +2 because the source and the target resources themselved would be updated as well @@ -232,72 +221,72 @@ private void handlePreview( } private void doMerge( - MergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( - theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); + theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); if (theRequestDetails.isPreferAsync()) { // client prefers async processing, do async doMergeAsync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); - } else { - // count the number of refs, if it is larger than batch size then process async, otherwise process sync - Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); - if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { - doMergeAsync( theMergeOperationParameters, theSourceResource, theTargetResource, theRequestDetails, theMergeOutcome, partitionId); + } else { + // count the number of refs, if it is larger than batch size then process async, otherwise process sync + Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( + theSourceResource.getIdElement().toVersionless(), theRequestDetails); + if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { + doMergeAsync( + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } else { doMergeSync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } } } private void doMergeSync( - MergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId partitionId) { + MergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId partitionId) { ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest( - theSourceResource.getIdElement(), - theTargetResource.getIdElement(), - theMergeOperationParameters.getBatchSize(), - partitionId); + theSourceResource.getIdElement(), + theTargetResource.getIdElement(), + theMergeOperationParameters.getBatchSize(), + partitionId); replaceReferenceRequest.setForceSync(true); myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); Patient updatedTarget = myMergeHelper.updateMergedResourcesAfterReferencesReplaced( - myHapiTransactionService, - theSourceResource, - theTargetResource, - (Patient) theMergeOperationParameters.getResultResource(), - theMergeOperationParameters.getDeleteSource(), - theRequestDetails); + myHapiTransactionService, + theSourceResource, + theTargetResource, + (Patient) theMergeOperationParameters.getResultResource(), + theMergeOperationParameters.getDeleteSource(), + theRequestDetails); theMergeOutcome.setUpdatedTargetResource(updatedTarget); String detailsText = "Merge operation completed successfully."; @@ -305,29 +294,29 @@ private void doMergeSync( } private void doMergeAsync( - MergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId partitionId) { + MergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId partitionId) { MergeJobParameters mergeJobParameters = new MergeJobParameters(); if (theMergeOperationParameters.getResultResource() != null) { mergeJobParameters.setResultResource(myFhirContext - .newJsonParser() - .encodeResourceToString(theMergeOperationParameters.getResultResource())); + .newJsonParser() + .encodeResourceToString(theMergeOperationParameters.getResultResource())); } mergeJobParameters.setDeleteSource(theMergeOperationParameters.getDeleteSource()); mergeJobParameters.setBatchSize(theMergeOperationParameters.getBatchSize()); mergeJobParameters.setSourceId( - new FhirIdJson(theSourceResource.getIdElement().toVersionless())); + new FhirIdJson(theSourceResource.getIdElement().toVersionless())); mergeJobParameters.setTargetId( - new FhirIdJson(theTargetResource.getIdElement().toVersionless())); + new FhirIdJson(theTargetResource.getIdElement().toVersionless())); mergeJobParameters.setPartitionId(partitionId); Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( - myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); + myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); task.getMeta().setVersionId(null); @@ -335,15 +324,15 @@ private void doMergeAsync( theMergeOutcome.setHttpStatusCode(STATUS_HTTP_202_ACCEPTED); String detailsText = "Merge request is accepted, and will be processed asynchronously. See" - + " task resource returned in this response for details."; + + " task resource returned in this response for details."; addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); } private boolean validateResultResourceIfExists( - MergeOperationInputParameters theMergeOperationParameters, - Patient theResolvedTargetResource, - Patient theResolvedSourceResource, - IBaseOperationOutcome theOperationOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + Patient theResolvedTargetResource, + Patient theResolvedSourceResource, + IBaseOperationOutcome theOperationOutcome) { if (theMergeOperationParameters.getResultResource() == null) { // result resource is not provided, no further validation is needed @@ -357,21 +346,21 @@ private boolean validateResultResourceIfExists( // validate the result resource's id as same as the target resource if (!theResolvedTargetResource.getIdElement().toVersionless().equals(theResultResource.getIdElement())) { String msg = String.format( - "'%s' must have the same versionless id as the actual resolved target resource. " - + "The actual resolved target resource's id is: '%s'", - theMergeOperationParameters.getResultResourceParameterName(), - theResolvedTargetResource.getIdElement().toVersionless().getValue()); + "'%s' must have the same versionless id as the actual resolved target resource. " + + "The actual resolved target resource's id is: '%s'", + theMergeOperationParameters.getResultResourceParameterName(), + theResolvedTargetResource.getIdElement().toVersionless().getValue()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); isValid = false; } // validate the result resource contains the identifiers provided in the target identifiers param if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { + && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { String msg = String.format( - "'%s' must have all the identifiers provided in %s", - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "'%s' must have all the identifiers provided in %s", + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); isValid = false; } @@ -381,11 +370,11 @@ private boolean validateResultResourceIfExists( // if the source resource is being deleted, the result resource must not have a replaces link to the source // resource if (!validateResultResourceReplacesLinkToSourceResource( - theResultResource, - theResolvedSourceResource, - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getDeleteSource(), - theOperationOutcome)) { + theResultResource, + theResolvedSourceResource, + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getDeleteSource(), + theOperationOutcome)) { isValid = false; } @@ -397,9 +386,9 @@ private boolean hasAllIdentifiers(Patient theResource, List List identifiersInResource = theResource.getIdentifier(); for (CanonicalIdentifier identifier : theIdentifiers) { boolean identifierFound = identifiersInResource.stream() - .anyMatch(i -> i.getSystem() - .equals(identifier.getSystemElement().getValueAsString()) - && i.getValue().equals(identifier.getValueElement().getValueAsString())); + .anyMatch(i -> i.getSystem() + .equals(identifier.getSystemElement().getValueAsString()) + && i.getValue().equals(identifier.getValueElement().getValueAsString())); if (!identifierFound) { return false; @@ -409,45 +398,45 @@ private boolean hasAllIdentifiers(Patient theResource, List } private List getLinksToResource( - Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { + Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { List links = getLinksOfTypeWithNonNullReference(theResource, theLinkType); return links.stream() - .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) - .collect(Collectors.toList()); + .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) + .collect(Collectors.toList()); } private boolean validateResultResourceReplacesLinkToSourceResource( - Patient theResultResource, - Patient theResolvedSourceResource, - String theResultResourceParameterName, - boolean theDeleteSource, - IBaseOperationOutcome theOperationOutcome) { + Patient theResultResource, + Patient theResolvedSourceResource, + String theResultResourceParameterName, + boolean theDeleteSource, + IBaseOperationOutcome theOperationOutcome) { // the result resource must have the replaces link set to the source resource List replacesLinkToSourceResource = getLinksToResource( - theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); + theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); if (theDeleteSource) { if (!replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must not have a 'replaces' link to the source resource " - + "when the source resource will be deleted, as the link may prevent deleting the source " - + "resource.", - theResultResourceParameterName); + "'%s' must not have a 'replaces' link to the source resource " + + "when the source resource will be deleted, as the link may prevent deleting the source " + + "resource.", + theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } } else { if (replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); + "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } if (replacesLinkToSourceResource.size() > 1) { String msg = String.format( - "'%s' has multiple 'replaces' links to the source resource. There should be only one.", - theResultResourceParameterName); + "'%s' has multiple 'replaces' links to the source resource. There should be only one.", + theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } @@ -468,7 +457,7 @@ protected List getLinksOfTypeWithNonNullReference(Patient theResource } private boolean validateSourceAndTargetAreSuitableForMerge( - Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { + Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { if (theSourceResource.getId().equalsIgnoreCase(theTargetResource.getId())) { String msg = "Source and target resources are the same resource."; @@ -484,25 +473,25 @@ private boolean validateSourceAndTargetAreSuitableForMerge( } List replacedByLinksInTarget = - getLinksOfTypeWithNonNullReference(theTargetResource, Patient.LinkType.REPLACEDBY); + getLinksOfTypeWithNonNullReference(theTargetResource, Patient.LinkType.REPLACEDBY); if (!replacedByLinksInTarget.isEmpty()) { String ref = replacedByLinksInTarget.get(0).getReference(); String msg = String.format( - "Target resource was previously replaced by a resource with reference '%s', it " - + "is not a suitable target for merging.", - ref); + "Target resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable target for merging.", + ref); addErrorToOperationOutcome(outcome, msg, "invalid"); return false; } List replacedByLinksInSource = - getLinksOfTypeWithNonNullReference(theSourceResource, Patient.LinkType.REPLACEDBY); + getLinksOfTypeWithNonNullReference(theSourceResource, Patient.LinkType.REPLACEDBY); if (!replacedByLinksInSource.isEmpty()) { String ref = replacedByLinksInSource.get(0).getReference(); String msg = String.format( - "Source resource was previously replaced by a resource with reference '%s', it " - + "is not a suitable source for merging.", - ref); + "Source resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable source for merging.", + ref); addErrorToOperationOutcome(outcome, msg, "invalid"); return false; } @@ -518,59 +507,59 @@ private boolean validateSourceAndTargetAreSuitableForMerge( * @return true if the parameters are valid, false otherwise */ private boolean validateMergeOperationParameters( - MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { List errorMessages = new ArrayList<>(); if (!theMergeOperationParameters.hasAtLeastOneSourceIdentifier() - && theMergeOperationParameters.getSourceResource() == null) { + && theMergeOperationParameters.getSourceResource() == null) { String msg = String.format( - "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", - theMergeOperationParameters.getSourceResourceParameterName(), - theMergeOperationParameters.getSourceIdentifiersParameterName()); + "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); errorMessages.add(msg); } // Spec has conflicting information about this case if (theMergeOperationParameters.hasAtLeastOneSourceIdentifier() - && theMergeOperationParameters.getSourceResource() != null) { + && theMergeOperationParameters.getSourceResource() != null) { String msg = String.format( - "Source resource must be provided either by '%s' or by '%s', not both.", - theMergeOperationParameters.getSourceResourceParameterName(), - theMergeOperationParameters.getSourceIdentifiersParameterName()); + "Source resource must be provided either by '%s' or by '%s', not both.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); errorMessages.add(msg); } if (!theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && theMergeOperationParameters.getTargetResource() == null) { + && theMergeOperationParameters.getTargetResource() == null) { String msg = String.format( - "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", - theMergeOperationParameters.getTargetResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); errorMessages.add(msg); } // Spec has conflicting information about this case if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && theMergeOperationParameters.getTargetResource() != null) { + && theMergeOperationParameters.getTargetResource() != null) { String msg = String.format( - "Target resource must be provided either by '%s' or by '%s', not both.", - theMergeOperationParameters.getTargetResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "Target resource must be provided either by '%s' or by '%s', not both.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); errorMessages.add(msg); } Reference sourceRef = (Reference) theMergeOperationParameters.getSourceResource(); if (sourceRef != null && !sourceRef.hasReference()) { String msg = String.format( - "Reference specified in '%s' parameter does not have a reference element.", - theMergeOperationParameters.getSourceResourceParameterName()); + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getSourceResourceParameterName()); errorMessages.add(msg); } Reference targetRef = (Reference) theMergeOperationParameters.getTargetResource(); if (targetRef != null && !targetRef.hasReference()) { String msg = String.format( - "Reference specified in '%s' parameter does not have a reference element.", - theMergeOperationParameters.getTargetResourceParameterName()); + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getTargetResourceParameterName()); errorMessages.add(msg); } @@ -587,43 +576,43 @@ private boolean validateMergeOperationParameters( } private IBaseResource resolveSourceResource( - MergeOperationInputParameters theOperationParameters, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { return resolveResource( - theOperationParameters.getSourceResource(), - theOperationParameters.getSourceIdentifiers(), - theRequestDetails, - theOutcome, - theOperationParameters.getSourceResourceParameterName(), - theOperationParameters.getSourceIdentifiersParameterName()); + theOperationParameters.getSourceResource(), + theOperationParameters.getSourceIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getSourceResourceParameterName(), + theOperationParameters.getSourceIdentifiersParameterName()); } private IBaseResource resolveTargetResource( - MergeOperationInputParameters theOperationParameters, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { return resolveResource( - theOperationParameters.getTargetResource(), - theOperationParameters.getTargetIdentifiers(), - theRequestDetails, - theOutcome, - theOperationParameters.getTargetResourceParameterName(), - theOperationParameters.getTargetIdentifiersParameterName()); + theOperationParameters.getTargetResource(), + theOperationParameters.getTargetIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getTargetResourceParameterName(), + theOperationParameters.getTargetIdentifiersParameterName()); } private IBaseResource resolveResourceByIdentifiers( - List theIdentifiers, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationParameterName) { + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { SearchParameterMap searchParameterMap = new SearchParameterMap(); TokenAndListParam tokenAndListParam = new TokenAndListParam(); for (CanonicalIdentifier identifier : theIdentifiers) { TokenParam tokenParam = new TokenParam( - identifier.getSystemElement().getValueAsString(), - identifier.getValueElement().getValueAsString()); + identifier.getSystemElement().getValueAsString(), + identifier.getValueElement().getValueAsString()); tokenAndListParam.addAnd(tokenParam); } searchParameterMap.add("identifier", tokenAndListParam); @@ -633,13 +622,13 @@ private IBaseResource resolveResourceByIdentifiers( List resources = bundle.getAllResources(); if (resources.isEmpty()) { String msg = String.format( - "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); + "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "not-found"); return null; } if (resources.size() > 1) { String msg = String.format( - "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); + "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "multiple-matches"); return null; } @@ -648,10 +637,10 @@ private IBaseResource resolveResourceByIdentifiers( } private IBaseResource resolveResourceByReference( - IBaseReference theReference, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationParameterName) { + IBaseReference theReference, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { // TODO Emre: why does IBaseReference not have getIdentifier or hasReference methods? // casting it to r4.Reference for now Reference r4ref = (Reference) theReference; @@ -662,19 +651,19 @@ private IBaseResource resolveResourceByReference( resource = myPatientDao.read(theResourceId.toVersionless(), theRequestDetails); } catch (ResourceNotFoundException e) { String msg = String.format( - "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); + "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "not-found"); return null; } if (theResourceId.hasVersionIdPart() - && !theResourceId - .getVersionIdPart() - .equals(resource.getIdElement().getVersionIdPart())) { + && !theResourceId + .getVersionIdPart() + .equals(resource.getIdElement().getVersionIdPart())) { String msg = String.format( - "The reference in '%s' parameter has a version specified, " - + "but it is not the latest version of the resource", - theOperationParameterName); + "The reference in '%s' parameter has a version specified, " + + "but it is not the latest version of the resource", + theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "conflict"); return null; } @@ -683,25 +672,25 @@ private IBaseResource resolveResourceByReference( } private IBaseResource resolveResource( - IBaseReference theReference, - List theIdentifiers, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationReferenceParameterName, - String theOperationIdentifiersParameterName) { + IBaseReference theReference, + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationReferenceParameterName, + String theOperationIdentifiersParameterName) { if (theReference != null) { return resolveResourceByReference( - theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); + theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); } return resolveResourceByIdentifiers( - theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); + theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); } private void addInfoToOperationOutcome( - IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { + IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { IBase issue = - OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); OperationOutcomeUtil.addDetailsToIssue(myFhirContext, issue, null, null, theDetailsText); } @@ -710,8 +699,22 @@ private void addErrorToOperationOutcome(IBaseOperationOutcome theOutcome, String } private static class ValidationResult { - protected Patient sourceResource; - protected Patient targetResource; - protected boolean isValid; + protected final Patient sourceResource; + protected final Patient targetResource; + protected final boolean isValid; + + private ValidationResult(Patient theSourceResource, Patient theTargetResource, boolean theIsValid) { + sourceResource = theSourceResource; + targetResource = theTargetResource; + isValid = theIsValid; + } + + public static ValidationResult invalidResult() { + return new ValidationResult(null, null, false); + } + + public static ValidationResult validResult(Patient theSourceResource, Patient theTargetResource) { + return new ValidationResult(theSourceResource, theTargetResource, true); + } } } From cdb3239c8464e5dc4db28d608ffe291968a32db8 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 14:20:03 -0500 Subject: [PATCH 101/148] make ValidationResult immutable --- .../provider/merge/ResourceMergeService.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index e3b8d0bc2038..a96083d205c7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -140,18 +140,16 @@ private void validateAndMerge( ValidationResult validationResult = validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); - if (!validationResult.isValid) { - return; - } - - Patient sourceResource = validationResult.sourceResource; - Patient targetResource = validationResult.targetResource; + if (validationResult.isValid) { + Patient sourceResource = validationResult.sourceResource; + Patient targetResource = validationResult.targetResource; - if (theMergeOperationParameters.getPreview()) { - handlePreview( + if (theMergeOperationParameters.getPreview()) { + handlePreview( sourceResource, targetResource, theMergeOperationParameters, theRequestDetails, theMergeOutcome); - } else { - doMerge(theMergeOperationParameters, sourceResource, targetResource, theRequestDetails, theMergeOutcome); + } else { + doMerge(theMergeOperationParameters, sourceResource, targetResource, theRequestDetails, theMergeOutcome); + } } } @@ -208,13 +206,13 @@ private void handlePreview( Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( theSourceResource.getIdElement().toVersionless(), theRequestDetails); - // in preview mode, we should also return how the target would look like + // in preview mode, we should also return what the target would look like Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); Patient targetPatientAsIfUpdated = myMergeHelper.prepareTargetPatientForUpdate( theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); - // adding +2 because the source and the target resources themselved would be updated as well + // adding +2 because the source and the target resources would be updated as well String diagnosticsMsg = String.format("Merge would update %d resources", referencingResourceCount + 2); String detailsText = "Preview only merge operation - no issues detected"; addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), diagnosticsMsg, detailsText); From bece0bbbf5dd4a73c7dcbc08afdc0b5ba4d678d7 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 14:25:38 -0500 Subject: [PATCH 102/148] rename ReplaceReferencesRequest --- .../jpa/provider/IReplaceReferencesSvc.java | 4 +- .../fhir/jpa/provider/JpaSystemProvider.java | 8 ++-- .../provider/ReplaceReferencesSvcImpl.java | 40 +++++++++---------- .../provider/merge/ResourceMergeService.java | 23 ++++++++--- .../merge/ResourceMergeServiceTest.java | 4 +- .../ReplaceReferenceUpdateStep.java | 4 +- .../ReplaceReferencesJobParameters.java | 8 ++-- .../ReplaceReferencesPatchBundleSvc.java | 14 +++---- ...est.java => ReplaceReferencesRequest.java} | 4 +- 9 files changed, 60 insertions(+), 49 deletions(-) rename hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/{ReplaceReferenceRequest.java => ReplaceReferencesRequest.java} (97%) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java index 603f9627dbcc..eee3879547b0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java @@ -19,7 +19,7 @@ */ package ca.uhn.fhir.jpa.provider; -import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; +import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; import ca.uhn.fhir.rest.api.server.RequestDetails; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IIdType; @@ -30,7 +30,7 @@ public interface IReplaceReferencesSvc { IBaseParameters replaceReferences( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails); + ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails); Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index e8d2ff408b4e..fae225c79060 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -27,7 +27,7 @@ import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; +import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.Transaction; @@ -177,10 +177,10 @@ public IBaseParameters replaceReferences( IdDt targetId = new IdDt(theTargetId); RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( theRequestDetails, ReadPartitionIdRequestDetails.forRead(targetId)); - ReplaceReferenceRequest replaceReferenceRequest = - new ReplaceReferenceRequest(sourceId, targetId, batchSize, partitionId); + ReplaceReferencesRequest replaceReferencesRequest = + new ReplaceReferencesRequest(sourceId, targetId, batchSize, partitionId); IBaseParameters retval = - getReplaceReferencesSvc().replaceReferences(replaceReferenceRequest, theRequestDetails); + getReplaceReferencesSvc().replaceReferences(replaceReferencesRequest, theRequestDetails); if (ParametersUtil.getNamedParameter(getContext(), retval, OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) .isPresent()) { HttpServletResponse response = theRequestDetails.getServletResponse(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index d0757577a52b..26e5930e6638 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -26,7 +26,7 @@ import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; +import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.util.StopLimitAccumulator; @@ -74,15 +74,15 @@ public ReplaceReferencesSvcImpl( @Override public IBaseParameters replaceReferences( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { - theReplaceReferenceRequest.validateOrThrowInvalidParameterException(); + ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) { + theReplaceReferencesRequest.validateOrThrowInvalidParameterException(); if (theRequestDetails.isPreferAsync()) { - return replaceReferencesPreferAsync(theReplaceReferenceRequest, theRequestDetails); - } else if (theReplaceReferenceRequest.isForceSync()) { - return replaceReferencesForceSync(theReplaceReferenceRequest, theRequestDetails); + return replaceReferencesPreferAsync(theReplaceReferencesRequest, theRequestDetails); + } else if (theReplaceReferencesRequest.isForceSync()) { + return replaceReferencesForceSync(theReplaceReferencesRequest, theRequestDetails); } else { - return replaceReferencesPreferSync(theReplaceReferenceRequest, theRequestDetails); + return replaceReferencesPreferSync(theReplaceReferencesRequest, theRequestDetails); } } @@ -95,14 +95,14 @@ public Integer countResourcesReferencingResource(IIdType theResourceId, RequestD } private IBaseParameters replaceReferencesPreferAsync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) { Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( myDaoRegistry.getResourceDao(Task.class), theRequestDetails, myJobCoordinator, JOB_REPLACE_REFERENCES, - new ReplaceReferencesJobParameters(theReplaceReferenceRequest)); + new ReplaceReferencesJobParameters(theReplaceReferencesRequest)); Parameters retval = new Parameters(); task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); @@ -118,20 +118,20 @@ private IBaseParameters replaceReferencesPreferAsync( */ @Nonnull private IBaseParameters replaceReferencesPreferSync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) { // TODO KHS get partition from request StopLimitAccumulator accumulator = myHapiTransactionService .withRequest(theRequestDetails) - .execute(() -> getAllPidsWithLimit(theReplaceReferenceRequest)); + .execute(() -> getAllPidsWithLimit(theReplaceReferencesRequest)); if (accumulator.isTruncated()) { ourLog.warn("Too many results. Switching to asynchronous reference replacement."); - return replaceReferencesPreferAsync(theReplaceReferenceRequest, theRequestDetails); + return replaceReferencesPreferAsync(theReplaceReferencesRequest, theRequestDetails); } Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources( - theReplaceReferenceRequest, accumulator.getItemList(), theRequestDetails); + theReplaceReferencesRequest, accumulator.getItemList(), theRequestDetails); Parameters retval = new Parameters(); retval.addParameter() @@ -146,20 +146,20 @@ private IBaseParameters replaceReferencesPreferSync( */ @Nonnull private IBaseParameters replaceReferencesForceSync( - ReplaceReferenceRequest theReplaceReferenceRequest, RequestDetails theRequestDetails) { + ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) { // TODO KHS get partition from request List allIds = myHapiTransactionService .withRequest(theRequestDetails) .execute(() -> { Stream idStream = myResourceLinkDao.streamSourceIdsForTargetFhirId( - theReplaceReferenceRequest.sourceId.getResourceType(), - theReplaceReferenceRequest.sourceId.getIdPart()); + theReplaceReferencesRequest.sourceId.getResourceType(), + theReplaceReferencesRequest.sourceId.getIdPart()); return idStream.collect(Collectors.toList()); }); Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources( - theReplaceReferenceRequest, allIds, theRequestDetails); + theReplaceReferencesRequest, allIds, theRequestDetails); Parameters retval = new Parameters(); retval.addParameter() @@ -169,12 +169,12 @@ private IBaseParameters replaceReferencesForceSync( } private @Nonnull StopLimitAccumulator getAllPidsWithLimit( - ReplaceReferenceRequest theReplaceReferenceRequest) { + ReplaceReferencesRequest theReplaceReferencesRequest) { Stream idStream = myResourceLinkDao.streamSourceIdsForTargetFhirId( - theReplaceReferenceRequest.sourceId.getResourceType(), theReplaceReferenceRequest.sourceId.getIdPart()); + theReplaceReferencesRequest.sourceId.getResourceType(), theReplaceReferencesRequest.sourceId.getIdPart()); StopLimitAccumulator accumulator = - StopLimitAccumulator.fromStreamAndLimit(idStream, theReplaceReferenceRequest.batchSize); + StopLimitAccumulator.fromStreamAndLimit(idStream, theReplaceReferencesRequest.batchSize); return accumulator; } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index a96083d205c7..eacbe4629505 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -33,7 +33,7 @@ import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; +import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.TokenAndListParam; @@ -146,9 +146,18 @@ private void validateAndMerge( if (theMergeOperationParameters.getPreview()) { handlePreview( - sourceResource, targetResource, theMergeOperationParameters, theRequestDetails, theMergeOutcome); + sourceResource, + targetResource, + theMergeOperationParameters, + theRequestDetails, + theMergeOutcome); } else { - doMerge(theMergeOperationParameters, sourceResource, targetResource, theRequestDetails, theMergeOutcome); + doMerge( + theMergeOperationParameters, + sourceResource, + targetResource, + theRequestDetails, + theMergeOutcome); } } } @@ -242,6 +251,8 @@ private void doMerge( Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( theSourceResource.getIdElement().toVersionless(), theRequestDetails); if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { + ourLog.info("{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", + numberOfRefs, theMergeOperationParameters.getBatchSize()); doMergeAsync( theMergeOperationParameters, theSourceResource, @@ -269,14 +280,14 @@ private void doMergeSync( MergeOperationOutcome theMergeOutcome, RequestPartitionId partitionId) { - ReplaceReferenceRequest replaceReferenceRequest = new ReplaceReferenceRequest( + ReplaceReferencesRequest replaceReferencesRequest = new ReplaceReferencesRequest( theSourceResource.getIdElement(), theTargetResource.getIdElement(), theMergeOperationParameters.getBatchSize(), partitionId); - replaceReferenceRequest.setForceSync(true); + replaceReferencesRequest.setForceSync(true); - myReplaceReferencesSvc.replaceReferences(replaceReferenceRequest, theRequestDetails); + myReplaceReferencesSvc.replaceReferences(replaceReferencesRequest, theRequestDetails); Patient updatedTarget = myMergeHelper.updateMergedResourcesAfterReferencesReplaced( myHapiTransactionService, diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java index 61cf4d301562..70c86cbfa9d0 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java @@ -14,7 +14,7 @@ import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.model.api.IQueryParameterType; -import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; +import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; @@ -1413,7 +1413,7 @@ private void setupReplaceReferencesForSuccessForSync() { // set the count to less that the page size for sync processing when(myReplaceReferencesSvcMock.countResourcesReferencingResource(new IdType(SOURCE_PATIENT_TEST_ID), myRequestDetailsMock)).thenReturn(PAGE_SIZE - 1); - when(myReplaceReferencesSvcMock.replaceReferences(isA(ReplaceReferenceRequest.class), + when(myReplaceReferencesSvcMock.replaceReferences(isA(ReplaceReferencesRequest.class), eq(myRequestDetailsMock))).thenReturn(new Parameters()); } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java index e146967e37bb..4f31906882a1 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java @@ -28,7 +28,7 @@ import ca.uhn.fhir.batch2.jobs.chunk.FhirIdListWorkChunkJson; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; +import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import jakarta.annotation.Nonnull; @@ -57,7 +57,7 @@ public RunOutcome run( throws JobExecutionFailedException { ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); - ReplaceReferenceRequest replaceReferencesRequest = params.asReplaceReferencesRequest(); + ReplaceReferencesRequest replaceReferencesRequest = params.asReplaceReferencesRequest(); List fhirIds = theStepExecutionDetails.getData().getFhirIds().stream() .map(FhirIdJson::asIdDt) .collect(Collectors.toList()); diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java index 1d8e9458a867..f4b63e1039f5 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java @@ -22,7 +22,7 @@ import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; import ca.uhn.fhir.batch2.jobs.parameters.BatchJobParametersWithTaskId; import ca.uhn.fhir.interceptor.model.RequestPartitionId; -import ca.uhn.fhir.replacereferences.ReplaceReferenceRequest; +import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; import com.fasterxml.jackson.annotation.JsonProperty; import static ca.uhn.fhir.jpa.api.config.JpaStorageSettings.DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE; @@ -47,7 +47,7 @@ public class ReplaceReferencesJobParameters extends BatchJobParametersWithTaskId public ReplaceReferencesJobParameters() {} - public ReplaceReferencesJobParameters(ReplaceReferenceRequest theRequest) { + public ReplaceReferencesJobParameters(ReplaceReferencesRequest theRequest) { mySourceId = new FhirIdJson(theRequest.sourceId); myTargetId = new FhirIdJson(theRequest.targetId); myBatchSize = theRequest.batchSize; @@ -89,7 +89,7 @@ public void setPartitionId(RequestPartitionId thePartitionId) { myPartitionId = thePartitionId; } - public ReplaceReferenceRequest asReplaceReferencesRequest() { - return new ReplaceReferenceRequest(mySourceId.asIdDt(), myTargetId.asIdDt(), myBatchSize, myPartitionId); + public ReplaceReferencesRequest asReplaceReferencesRequest() { + return new ReplaceReferencesRequest(mySourceId.asIdDt(), myTargetId.asIdDt(), myBatchSize, myPartitionId); } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java index dd5e30e3889b..92781bc64ec9 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java @@ -58,10 +58,10 @@ public ReplaceReferencesPatchBundleSvc(DaoRegistry theDaoRegistry) { } public Bundle patchReferencingResources( - ReplaceReferenceRequest theReplaceReferenceRequest, + ReplaceReferencesRequest theReplaceReferencesRequest, List theResourceIds, RequestDetails theRequestDetails) { - Bundle patchBundle = buildPatchBundle(theReplaceReferenceRequest, theResourceIds, theRequestDetails); + Bundle patchBundle = buildPatchBundle(theReplaceReferencesRequest, theResourceIds, theRequestDetails); IFhirSystemDao systemDao = myDaoRegistry.getSystemDao(); Bundle result = systemDao.transaction(theRequestDetails, patchBundle); // TODO KHS shouldn't transaction response bundles already have ids? @@ -70,7 +70,7 @@ public Bundle patchReferencingResources( } private Bundle buildPatchBundle( - ReplaceReferenceRequest theReplaceReferenceRequest, + ReplaceReferencesRequest theReplaceReferencesRequest, List theResourceIds, RequestDetails theRequestDetails) { BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); @@ -78,7 +78,7 @@ private Bundle buildPatchBundle( theResourceIds.forEach(referencingResourceId -> { IFhirResourceDao dao = myDaoRegistry.getResourceDao(referencingResourceId.getResourceType()); IBaseResource resource = dao.read(referencingResourceId, theRequestDetails); - Parameters patchParams = buildPatchParams(theReplaceReferenceRequest, resource); + Parameters patchParams = buildPatchParams(theReplaceReferencesRequest, resource); IIdType resourceId = resource.getIdElement(); bundleBuilder.addTransactionFhirPatchEntry(resourceId, patchParams); }); @@ -86,16 +86,16 @@ private Bundle buildPatchBundle( } private @Nonnull Parameters buildPatchParams( - ReplaceReferenceRequest theReplaceReferenceRequest, IBaseResource referencingResource) { + ReplaceReferencesRequest theReplaceReferencesRequest, IBaseResource referencingResource) { Parameters params = new Parameters(); myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() .filter(refInfo -> matches( refInfo, - theReplaceReferenceRequest.sourceId)) // We only care about references to our source resource + theReplaceReferencesRequest.sourceId)) // We only care about references to our source resource .map(refInfo -> createReplaceReferencePatchOperation( referencingResource.fhirType() + "." + refInfo.getName(), - new Reference(theReplaceReferenceRequest.targetId.getValueAsString()))) + new Reference(theReplaceReferencesRequest.targetId.getValueAsString()))) .forEach(params::addParameter); // Add each operation to parameters return params; } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java similarity index 97% rename from hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java rename to hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java index 2b444179361a..6e5401d3b242 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferenceRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java @@ -30,7 +30,7 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID; import static org.apache.commons.lang3.StringUtils.isBlank; -public class ReplaceReferenceRequest { +public class ReplaceReferencesRequest { @Nonnull public final IIdType sourceId; @@ -43,7 +43,7 @@ public class ReplaceReferenceRequest { private boolean myForceSync = false; - public ReplaceReferenceRequest( + public ReplaceReferencesRequest( @Nonnull IIdType theSourceId, @Nonnull IIdType theTargetId, int theBatchSize, From 617936aa7649007f55a75ba6b6a0e366831ba4d0 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 14:25:50 -0500 Subject: [PATCH 103/148] rename ReplaceReferencesRequest --- .../jpa/provider/IReplaceReferencesSvc.java | 2 +- .../jpa/provider/ReplaceReferencesSvcImpl.java | 17 +++++++++-------- .../provider/merge/ResourceMergeService.java | 6 ++++-- .../ReplaceReferenceUpdateStep.java | 2 +- .../ReplaceReferencesPatchBundleSvc.java | 2 +- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java index eee3879547b0..4e9fbd256c3f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java @@ -30,7 +30,7 @@ public interface IReplaceReferencesSvc { IBaseParameters replaceReferences( - ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails); + ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails); Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 26e5930e6638..5795b4b5086b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -26,8 +26,8 @@ import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; +import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.util.StopLimitAccumulator; import jakarta.annotation.Nonnull; @@ -74,7 +74,7 @@ public ReplaceReferencesSvcImpl( @Override public IBaseParameters replaceReferences( - ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) { + ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) { theReplaceReferencesRequest.validateOrThrowInvalidParameterException(); if (theRequestDetails.isPreferAsync()) { @@ -95,7 +95,7 @@ public Integer countResourcesReferencingResource(IIdType theResourceId, RequestD } private IBaseParameters replaceReferencesPreferAsync( - ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) { + ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) { Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( myDaoRegistry.getResourceDao(Task.class), @@ -118,7 +118,7 @@ private IBaseParameters replaceReferencesPreferAsync( */ @Nonnull private IBaseParameters replaceReferencesPreferSync( - ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) { + ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) { // TODO KHS get partition from request StopLimitAccumulator accumulator = myHapiTransactionService @@ -131,7 +131,7 @@ private IBaseParameters replaceReferencesPreferSync( } Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources( - theReplaceReferencesRequest, accumulator.getItemList(), theRequestDetails); + theReplaceReferencesRequest, accumulator.getItemList(), theRequestDetails); Parameters retval = new Parameters(); retval.addParameter() @@ -146,7 +146,7 @@ private IBaseParameters replaceReferencesPreferSync( */ @Nonnull private IBaseParameters replaceReferencesForceSync( - ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) { + ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) { // TODO KHS get partition from request List allIds = myHapiTransactionService @@ -159,7 +159,7 @@ private IBaseParameters replaceReferencesForceSync( }); Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources( - theReplaceReferencesRequest, allIds, theRequestDetails); + theReplaceReferencesRequest, allIds, theRequestDetails); Parameters retval = new Parameters(); retval.addParameter() @@ -172,7 +172,8 @@ private IBaseParameters replaceReferencesForceSync( ReplaceReferencesRequest theReplaceReferencesRequest) { Stream idStream = myResourceLinkDao.streamSourceIdsForTargetFhirId( - theReplaceReferencesRequest.sourceId.getResourceType(), theReplaceReferencesRequest.sourceId.getIdPart()); + theReplaceReferencesRequest.sourceId.getResourceType(), + theReplaceReferencesRequest.sourceId.getIdPart()); StopLimitAccumulator accumulator = StopLimitAccumulator.fromStreamAndLimit(idStream, theReplaceReferencesRequest.batchSize); return accumulator; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index eacbe4629505..4bd05129c20a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -251,8 +251,10 @@ private void doMerge( Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( theSourceResource.getIdElement().toVersionless(), theRequestDetails); if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { - ourLog.info("{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", - numberOfRefs, theMergeOperationParameters.getBatchSize()); + ourLog.info( + "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", + numberOfRefs, + theMergeOperationParameters.getBatchSize()); doMergeAsync( theMergeOperationParameters, theSourceResource, diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java index 4f31906882a1..36fbd7969d1f 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateStep.java @@ -28,8 +28,8 @@ import ca.uhn.fhir.batch2.jobs.chunk.FhirIdListWorkChunkJson; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; +import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import jakarta.annotation.Nonnull; import org.hl7.fhir.r4.model.Bundle; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java index 92781bc64ec9..4eb9391e238d 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java @@ -86,7 +86,7 @@ private Bundle buildPatchBundle( } private @Nonnull Parameters buildPatchParams( - ReplaceReferencesRequest theReplaceReferencesRequest, IBaseResource referencingResource) { + ReplaceReferencesRequest theReplaceReferencesRequest, IBaseResource referencingResource) { Parameters params = new Parameters(); myFhirContext.newTerser().getAllResourceReferences(referencingResource).stream() From 21379210844237cfb924a131c349847ba29849bb Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 14:28:27 -0500 Subject: [PATCH 104/148] rename MergeResourceHelper --- .../jpa/provider/merge/ResourceMergeService.java | 12 +++++++----- .../{MergeHelper.java => MergeResourceHelper.java} | 4 ++-- .../jobs/merge/MergeUpdateTaskReducerStep.java | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) rename hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/{MergeHelper.java => MergeResourceHelper.java} (98%) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 4bd05129c20a..1f948372a40b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -21,7 +21,7 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; -import ca.uhn.fhir.batch2.jobs.merge.MergeHelper; +import ca.uhn.fhir.batch2.jobs.merge.MergeResourceHelper; import ca.uhn.fhir.batch2.jobs.merge.MergeJobParameters; import ca.uhn.fhir.batch2.util.Batch2TaskHelper; import ca.uhn.fhir.context.FhirContext; @@ -79,7 +79,7 @@ public class ResourceMergeService { private final IRequestPartitionHelperSvc myRequestPartitionHelperSvc; private final IFhirResourceDao myTaskDao; private final IJobCoordinator myJobCoordinator; - private final MergeHelper myMergeHelper; + private final MergeResourceHelper myMergeResourceHelper; private final Batch2TaskHelper myBatch2TaskHelper; public ResourceMergeService( @@ -98,7 +98,7 @@ public ResourceMergeService( myBatch2TaskHelper = theBatch2TaskHelper; myFhirContext = myPatientDao.getContext(); myHapiTransactionService = theHapiTransactionService; - myMergeHelper = new MergeHelper(myPatientDao); + myMergeResourceHelper = new MergeResourceHelper(myPatientDao); } /** @@ -217,7 +217,7 @@ private void handlePreview( // in preview mode, we should also return what the target would look like Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); - Patient targetPatientAsIfUpdated = myMergeHelper.prepareTargetPatientForUpdate( + Patient targetPatientAsIfUpdated = myMergeResourceHelper.prepareTargetPatientForUpdate( theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); @@ -287,11 +287,13 @@ private void doMergeSync( theTargetResource.getIdElement(), theMergeOperationParameters.getBatchSize(), partitionId); + + // We don't want replace references to flip to async mode once we've already made the decision to go sync here. replaceReferencesRequest.setForceSync(true); myReplaceReferencesSvc.replaceReferences(replaceReferencesRequest, theRequestDetails); - Patient updatedTarget = myMergeHelper.updateMergedResourcesAfterReferencesReplaced( + Patient updatedTarget = myMergeResourceHelper.updateMergedResourcesAfterReferencesReplaced( myHapiTransactionService, theSourceResource, theTargetResource, diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeHelper.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeResourceHelper.java similarity index 98% rename from hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeHelper.java rename to hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeResourceHelper.java index 5be18aefd813..672d9d543c66 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeHelper.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeResourceHelper.java @@ -36,11 +36,11 @@ * This class contains code that is used to update source and target resources after the references are replaced. * This is the common functionality that is used in sync case and in the async case as the reduction step. */ -public class MergeHelper { +public class MergeResourceHelper { private final IFhirResourceDao myPatientDao; - public MergeHelper(IFhirResourceDao theDao) { + public MergeResourceHelper(IFhirResourceDao theDao) { myPatientDao = theDao; } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java index ca4e4b208268..67928769d254 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeUpdateTaskReducerStep.java @@ -60,7 +60,7 @@ public RunOutcome run( IFhirResourceDao patientDao = myDaoRegistry.getResourceDao(Patient.class); - MergeHelper helper = new MergeHelper(patientDao); + MergeResourceHelper helper = new MergeResourceHelper(patientDao); helper.updateMergedResourcesAfterReferencesReplaced( myHapiTransactionService, From 52bff4dde03a0b81d09a1cabc6f4a12d061bb136 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 14:30:29 -0500 Subject: [PATCH 105/148] rename MergeResourceHelper --- .../ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 1f948372a40b..e875d3860b18 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -21,8 +21,8 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; -import ca.uhn.fhir.batch2.jobs.merge.MergeResourceHelper; import ca.uhn.fhir.batch2.jobs.merge.MergeJobParameters; +import ca.uhn.fhir.batch2.jobs.merge.MergeResourceHelper; import ca.uhn.fhir.batch2.util.Batch2TaskHelper; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; From 34b8ac2ac1a64cac3a3c40d3a90505422f821f72 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 14:38:40 -0500 Subject: [PATCH 106/148] javadoc --- .../ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java | 4 ++-- .../main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index e875d3860b18..9627dd9e9afc 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -312,7 +312,7 @@ private void doMergeAsync( Patient theTargetResource, RequestDetails theRequestDetails, MergeOperationOutcome theMergeOutcome, - RequestPartitionId partitionId) { + RequestPartitionId thePartitionId) { MergeJobParameters mergeJobParameters = new MergeJobParameters(); if (theMergeOperationParameters.getResultResource() != null) { @@ -326,7 +326,7 @@ private void doMergeAsync( new FhirIdJson(theSourceResource.getIdElement().toVersionless())); mergeJobParameters.setTargetId( new FhirIdJson(theTargetResource.getIdElement().toVersionless())); - mergeJobParameters.setPartitionId(partitionId); + mergeJobParameters.setPartitionId(thePartitionId); Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java index 95bcfc64ceff..a690cc796ec3 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java @@ -31,6 +31,11 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.HAPI_BATCH_JOB_ID_SYSTEM; +/** + * Start a job and create a Task resource that tracks the status of the job. This is designed in a way that + * it could be used by any Batch 2 job. + */ + public class Batch2TaskHelper { public Task startJobAndCreateAssociatedTask( From 6386362e02d0bc9dce8bad1314169409655d0e13 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 15:13:49 -0500 Subject: [PATCH 107/148] split merge validation service out from merge service --- .../uhn/hapi/fhir/docs/validation/examples.md | 2 +- .../provider/merge/MergeValidationResult.java | 23 + .../merge/MergeValidationService.java | 444 +++++++++++++++++ .../provider/merge/ResourceMergeService.java | 455 +----------------- .../fhir/batch2/util/Batch2TaskHelper.java | 1 - 5 files changed, 475 insertions(+), 450 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/examples.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/examples.md index fe0d1dd89560..d39fba7cb660 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/examples.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/examples.md @@ -39,5 +39,5 @@ FhirValidator validator = myFhirCtx.newValidator(); validator.registerValidatorModule(instanceValidator); // Validate theResource -ValidationResult validationResult = validator.validateWithResult(theResource); +ValidationResult mergeValidationResult = validator.validateWithResult(theResource); ``` diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java new file mode 100644 index 000000000000..6e0278bbf59b --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java @@ -0,0 +1,23 @@ +package ca.uhn.fhir.jpa.provider.merge; + +import org.hl7.fhir.r4.model.Patient; + +class MergeValidationResult { + protected final Patient sourceResource; + protected final Patient targetResource; + protected final boolean isValid; + + private MergeValidationResult(Patient theSourceResource, Patient theTargetResource, boolean theIsValid) { + sourceResource = theSourceResource; + targetResource = theTargetResource; + isValid = theIsValid; + } + + public static MergeValidationResult invalidResult() { + return new MergeValidationResult(null, null, false); + } + + public static MergeValidationResult validResult(Patient theSourceResource, Patient theTargetResource) { + return new MergeValidationResult(theSourceResource, theTargetResource, true); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java new file mode 100644 index 000000000000..b993d2e92419 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java @@ -0,0 +1,444 @@ +package ca.uhn.fhir.jpa.provider.merge; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.param.TokenAndListParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.util.CanonicalIdentifier; +import ca.uhn.fhir.util.OperationOutcomeUtil; +import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Reference; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_400_BAD_REQUEST; +import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY; + +public class MergeValidationService { + private final FhirContext myFhirContext; + private final IFhirResourceDao myPatientDao; + + public MergeValidationService(FhirContext theFhirContext, DaoRegistry theDaoRegistry) { + myFhirContext = theFhirContext; + myPatientDao = theDaoRegistry.getResourceDao(Patient.class); + } + + MergeValidationResult validate( + MergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { + + IBaseOperationOutcome operationOutcome = theMergeOutcome.getOperationOutcome(); + + if (!validateMergeOperationParameters(theMergeOperationParameters, operationOutcome)) { + theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); + return MergeValidationResult.invalidResult(); + } + + // cast to Patient, since we only support merging Patient resources for now + Patient sourceResource = + (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); + + if (sourceResource == null) { + theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); + return MergeValidationResult.invalidResult(); + } + + // cast to Patient, since we only support merging Patient resources for now + Patient targetResource = + (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); + + if (targetResource == null) { + theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); + return MergeValidationResult.invalidResult(); + } + + if (!validateSourceAndTargetAreSuitableForMerge(sourceResource, targetResource, operationOutcome)) { + theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); + return MergeValidationResult.invalidResult(); + } + + if (!validateResultResourceIfExists( + theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { + theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); + return MergeValidationResult.invalidResult(); + } + return MergeValidationResult.validResult(sourceResource, targetResource); + } + + private boolean validateResultResourceIfExists( + MergeOperationInputParameters theMergeOperationParameters, + Patient theResolvedTargetResource, + Patient theResolvedSourceResource, + IBaseOperationOutcome theOperationOutcome) { + + if (theMergeOperationParameters.getResultResource() == null) { + // result resource is not provided, no further validation is needed + return true; + } + + boolean retval = true; + + Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); + + // validate the result resource's id as same as the target resource + if (!theResolvedTargetResource.getIdElement().toVersionless().equals(theResultResource.getIdElement())) { + String msg = String.format( + "'%s' must have the same versionless id as the actual resolved target resource. " + + "The actual resolved target resource's id is: '%s'", + theMergeOperationParameters.getResultResourceParameterName(), + theResolvedTargetResource.getIdElement().toVersionless().getValue()); + addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); + retval = false; + } + + // validate the result resource contains the identifiers provided in the target identifiers param + if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() + && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { + String msg = String.format( + "'%s' must have all the identifiers provided in %s", + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); + addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); + retval = false; + } + + // if the source resource is not being deleted, the result resource must have a replaces link to the source + // resource + // if the source resource is being deleted, the result resource must not have a replaces link to the source + // resource + if (!validateResultResourceReplacesLinkToSourceResource( + theResultResource, + theResolvedSourceResource, + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getDeleteSource(), + theOperationOutcome)) { + retval = false; + } + + return retval; + } + + private void addErrorToOperationOutcome(IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theCode) { + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "error", theDiagnosticMsg, null, theCode); + } + + private boolean hasAllIdentifiers(Patient theResource, List theIdentifiers) { + + List identifiersInResource = theResource.getIdentifier(); + for (CanonicalIdentifier identifier : theIdentifiers) { + boolean identifierFound = identifiersInResource.stream() + .anyMatch(i -> i.getSystem() + .equals(identifier.getSystemElement().getValueAsString()) + && i.getValue().equals(identifier.getValueElement().getValueAsString())); + + if (!identifierFound) { + return false; + } + } + return true; + } + + private boolean validateResultResourceReplacesLinkToSourceResource( + Patient theResultResource, + Patient theResolvedSourceResource, + String theResultResourceParameterName, + boolean theDeleteSource, + IBaseOperationOutcome theOperationOutcome) { + // the result resource must have the replaces link set to the source resource + List replacesLinkToSourceResource = getLinksToResource( + theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); + + if (theDeleteSource) { + if (!replacesLinkToSourceResource.isEmpty()) { + String msg = String.format( + "'%s' must not have a 'replaces' link to the source resource " + + "when the source resource will be deleted, as the link may prevent deleting the source " + + "resource.", + theResultResourceParameterName); + addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); + return false; + } + } else { + if (replacesLinkToSourceResource.isEmpty()) { + String msg = String.format( + "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); + addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); + return false; + } + + if (replacesLinkToSourceResource.size() > 1) { + String msg = String.format( + "'%s' has multiple 'replaces' links to the source resource. There should be only one.", + theResultResourceParameterName); + addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); + return false; + } + } + return true; + } + + private List getLinksToResource( + Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { + List links = getLinksOfTypeWithNonNullReference(theResource, theLinkType); + return links.stream() + .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) + .collect(Collectors.toList()); + } + + protected List getLinksOfTypeWithNonNullReference(Patient theResource, Patient.LinkType theLinkType) { + List links = new ArrayList<>(); + if (theResource.hasLink()) { + for (Patient.PatientLinkComponent link : theResource.getLink()) { + if (theLinkType.equals(link.getType()) && link.hasOther()) { + links.add(link.getOther()); + } + } + } + return links; + } + + private boolean validateSourceAndTargetAreSuitableForMerge( + Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { + + if (theSourceResource.getId().equalsIgnoreCase(theTargetResource.getId())) { + String msg = "Source and target resources are the same resource."; + // What is the right code to use in these cases? + addErrorToOperationOutcome(outcome, msg, "invalid"); + return false; + } + + if (theTargetResource.hasActive() && !theTargetResource.getActive()) { + String msg = "Target resource is not active, it must be active to be the target of a merge operation."; + addErrorToOperationOutcome(outcome, msg, "invalid"); + return false; + } + + List replacedByLinksInTarget = + getLinksOfTypeWithNonNullReference(theTargetResource, Patient.LinkType.REPLACEDBY); + if (!replacedByLinksInTarget.isEmpty()) { + String ref = replacedByLinksInTarget.get(0).getReference(); + String msg = String.format( + "Target resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable target for merging.", + ref); + addErrorToOperationOutcome(outcome, msg, "invalid"); + return false; + } + + List replacedByLinksInSource = + getLinksOfTypeWithNonNullReference(theSourceResource, Patient.LinkType.REPLACEDBY); + if (!replacedByLinksInSource.isEmpty()) { + String ref = replacedByLinksInSource.get(0).getReference(); + String msg = String.format( + "Source resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable source for merging.", + ref); + addErrorToOperationOutcome(outcome, msg, "invalid"); + return false; + } + + return true; + } + + /** + * Validates the merge operation parameters and adds validation errors to the outcome + * + * @param theMergeOperationParameters the merge operation parameters + * @param theOutcome the outcome to add validation errors to + * @return true if the parameters are valid, false otherwise + */ + private boolean validateMergeOperationParameters( + MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { + List errorMessages = new ArrayList<>(); + if (!theMergeOperationParameters.hasAtLeastOneSourceIdentifier() + && theMergeOperationParameters.getSourceResource() == null) { + String msg = String.format( + "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); + errorMessages.add(msg); + } + + // Spec has conflicting information about this case + if (theMergeOperationParameters.hasAtLeastOneSourceIdentifier() + && theMergeOperationParameters.getSourceResource() != null) { + String msg = String.format( + "Source resource must be provided either by '%s' or by '%s', not both.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); + errorMessages.add(msg); + } + + if (!theMergeOperationParameters.hasAtLeastOneTargetIdentifier() + && theMergeOperationParameters.getTargetResource() == null) { + String msg = String.format( + "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); + errorMessages.add(msg); + } + + // Spec has conflicting information about this case + if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() + && theMergeOperationParameters.getTargetResource() != null) { + String msg = String.format( + "Target resource must be provided either by '%s' or by '%s', not both.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); + errorMessages.add(msg); + } + + Reference sourceRef = (Reference) theMergeOperationParameters.getSourceResource(); + if (sourceRef != null && !sourceRef.hasReference()) { + String msg = String.format( + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getSourceResourceParameterName()); + errorMessages.add(msg); + } + + Reference targetRef = (Reference) theMergeOperationParameters.getTargetResource(); + if (targetRef != null && !targetRef.hasReference()) { + String msg = String.format( + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getTargetResourceParameterName()); + errorMessages.add(msg); + } + + if (!errorMessages.isEmpty()) { + for (String validationError : errorMessages) { + addErrorToOperationOutcome(theOutcome, validationError, "required"); + } + // there are validation errors + return false; + } + + // no validation errors + return true; + } + + private IBaseResource resolveSourceResource( + MergeOperationInputParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { + return resolveResource( + theOperationParameters.getSourceResource(), + theOperationParameters.getSourceIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getSourceResourceParameterName(), + theOperationParameters.getSourceIdentifiersParameterName()); + } + + private IBaseResource resolveTargetResource( + MergeOperationInputParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { + return resolveResource( + theOperationParameters.getTargetResource(), + theOperationParameters.getTargetIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getTargetResourceParameterName(), + theOperationParameters.getTargetIdentifiersParameterName()); + } + + private IBaseResource resolveResource( + IBaseReference theReference, + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationReferenceParameterName, + String theOperationIdentifiersParameterName) { + if (theReference != null) { + return resolveResourceByReference( + theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); + } + + return resolveResourceByIdentifiers( + theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); + } + + private IBaseResource resolveResourceByIdentifiers( + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { + + SearchParameterMap searchParameterMap = new SearchParameterMap(); + TokenAndListParam tokenAndListParam = new TokenAndListParam(); + for (CanonicalIdentifier identifier : theIdentifiers) { + TokenParam tokenParam = new TokenParam( + identifier.getSystemElement().getValueAsString(), + identifier.getValueElement().getValueAsString()); + tokenAndListParam.addAnd(tokenParam); + } + searchParameterMap.add("identifier", tokenAndListParam); + searchParameterMap.setCount(2); + + IBundleProvider bundle = myPatientDao.search(searchParameterMap, theRequestDetails); + List resources = bundle.getAllResources(); + if (resources.isEmpty()) { + String msg = String.format( + "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); + addErrorToOperationOutcome(theOutcome, msg, "not-found"); + return null; + } + if (resources.size() > 1) { + String msg = String.format( + "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); + addErrorToOperationOutcome(theOutcome, msg, "multiple-matches"); + return null; + } + + return resources.get(0); + } + + private IBaseResource resolveResourceByReference( + IBaseReference theReference, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { + // TODO Emre: why does IBaseReference not have getIdentifier or hasReference methods? + // casting it to r4.Reference for now + Reference r4ref = (Reference) theReference; + + IIdType theResourceId = new IdType(r4ref.getReferenceElement().getValue()); + IBaseResource resource; + try { + resource = myPatientDao.read(theResourceId.toVersionless(), theRequestDetails); + } catch (ResourceNotFoundException e) { + String msg = String.format( + "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); + addErrorToOperationOutcome(theOutcome, msg, "not-found"); + return null; + } + + if (theResourceId.hasVersionIdPart() + && !theResourceId + .getVersionIdPart() + .equals(resource.getIdElement().getVersionIdPart())) { + String msg = String.format( + "The reference in '%s' parameter has a version specified, " + + "but it is not the latest version of the resource", + theOperationParameterName); + addErrorToOperationOutcome(theOutcome, msg, "conflict"); + return null; + } + + return resource; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 9627dd9e9afc..0d2a3729665d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -32,38 +32,20 @@ import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; -import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; -import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.param.TokenAndListParam; -import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; -import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; -import ca.uhn.fhir.util.CanonicalIdentifier; import ca.uhn.fhir.util.OperationOutcomeUtil; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; -import org.hl7.fhir.instance.model.api.IBaseReference; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - import static ca.uhn.fhir.batch2.jobs.merge.MergeAppCtx.JOB_MERGE; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_200_OK; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_202_ACCEPTED; -import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_400_BAD_REQUEST; -import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_500_INTERNAL_ERROR; /** @@ -81,6 +63,7 @@ public class ResourceMergeService { private final IJobCoordinator myJobCoordinator; private final MergeResourceHelper myMergeResourceHelper; private final Batch2TaskHelper myBatch2TaskHelper; + private final MergeValidationService myMergeValidationService; public ResourceMergeService( DaoRegistry theDaoRegistry, @@ -99,6 +82,7 @@ public ResourceMergeService( myFhirContext = myPatientDao.getContext(); myHapiTransactionService = theHapiTransactionService; myMergeResourceHelper = new MergeResourceHelper(myPatientDao); + myMergeValidationService = new MergeValidationService(myFhirContext, theDaoRegistry); } /** @@ -138,11 +122,12 @@ private void validateAndMerge( RequestDetails theRequestDetails, MergeOperationOutcome theMergeOutcome) { - ValidationResult validationResult = validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); + MergeValidationResult mergeValidationResult = + myMergeValidationService.validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); - if (validationResult.isValid) { - Patient sourceResource = validationResult.sourceResource; - Patient targetResource = validationResult.targetResource; + if (mergeValidationResult.isValid) { + Patient sourceResource = mergeValidationResult.sourceResource; + Patient targetResource = mergeValidationResult.targetResource; if (theMergeOperationParameters.getPreview()) { handlePreview( @@ -162,49 +147,6 @@ private void validateAndMerge( } } - private ValidationResult validate( - MergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { - - IBaseOperationOutcome operationOutcome = theMergeOutcome.getOperationOutcome(); - - if (!validateMergeOperationParameters(theMergeOperationParameters, operationOutcome)) { - theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); - return ValidationResult.invalidResult(); - } - - // cast to Patient, since we only support merging Patient resources for now - Patient sourceResource = - (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); - - if (sourceResource == null) { - theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); - return ValidationResult.invalidResult(); - } - - // cast to Patient, since we only support merging Patient resources for now - Patient targetResource = - (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); - - if (targetResource == null) { - theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); - return ValidationResult.invalidResult(); - } - - if (!validateSourceAndTargetAreSuitableForMerge(sourceResource, targetResource, operationOutcome)) { - theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); - return ValidationResult.invalidResult(); - } - - if (!validateResultResourceIfExists( - theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { - theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); - return ValidationResult.invalidResult(); - } - return ValidationResult.validResult(sourceResource, targetResource); - } - private void handlePreview( Patient theSourceResource, Patient theTargetResource, @@ -341,393 +283,10 @@ private void doMergeAsync( addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); } - private boolean validateResultResourceIfExists( - MergeOperationInputParameters theMergeOperationParameters, - Patient theResolvedTargetResource, - Patient theResolvedSourceResource, - IBaseOperationOutcome theOperationOutcome) { - - if (theMergeOperationParameters.getResultResource() == null) { - // result resource is not provided, no further validation is needed - return true; - } - - boolean isValid = true; - - Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); - - // validate the result resource's id as same as the target resource - if (!theResolvedTargetResource.getIdElement().toVersionless().equals(theResultResource.getIdElement())) { - String msg = String.format( - "'%s' must have the same versionless id as the actual resolved target resource. " - + "The actual resolved target resource's id is: '%s'", - theMergeOperationParameters.getResultResourceParameterName(), - theResolvedTargetResource.getIdElement().toVersionless().getValue()); - addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); - isValid = false; - } - - // validate the result resource contains the identifiers provided in the target identifiers param - if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { - String msg = String.format( - "'%s' must have all the identifiers provided in %s", - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); - addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); - isValid = false; - } - - // if the source resource is not being deleted, the result resource must have a replaces link to the source - // resource - // if the source resource is being deleted, the result resource must not have a replaces link to the source - // resource - if (!validateResultResourceReplacesLinkToSourceResource( - theResultResource, - theResolvedSourceResource, - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getDeleteSource(), - theOperationOutcome)) { - isValid = false; - } - - return isValid; - } - - private boolean hasAllIdentifiers(Patient theResource, List theIdentifiers) { - - List identifiersInResource = theResource.getIdentifier(); - for (CanonicalIdentifier identifier : theIdentifiers) { - boolean identifierFound = identifiersInResource.stream() - .anyMatch(i -> i.getSystem() - .equals(identifier.getSystemElement().getValueAsString()) - && i.getValue().equals(identifier.getValueElement().getValueAsString())); - - if (!identifierFound) { - return false; - } - } - return true; - } - - private List getLinksToResource( - Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { - List links = getLinksOfTypeWithNonNullReference(theResource, theLinkType); - return links.stream() - .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) - .collect(Collectors.toList()); - } - - private boolean validateResultResourceReplacesLinkToSourceResource( - Patient theResultResource, - Patient theResolvedSourceResource, - String theResultResourceParameterName, - boolean theDeleteSource, - IBaseOperationOutcome theOperationOutcome) { - // the result resource must have the replaces link set to the source resource - List replacesLinkToSourceResource = getLinksToResource( - theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); - - if (theDeleteSource) { - if (!replacesLinkToSourceResource.isEmpty()) { - String msg = String.format( - "'%s' must not have a 'replaces' link to the source resource " - + "when the source resource will be deleted, as the link may prevent deleting the source " - + "resource.", - theResultResourceParameterName); - addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); - return false; - } - } else { - if (replacesLinkToSourceResource.isEmpty()) { - String msg = String.format( - "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); - addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); - return false; - } - - if (replacesLinkToSourceResource.size() > 1) { - String msg = String.format( - "'%s' has multiple 'replaces' links to the source resource. There should be only one.", - theResultResourceParameterName); - addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); - return false; - } - } - return true; - } - - protected List getLinksOfTypeWithNonNullReference(Patient theResource, Patient.LinkType theLinkType) { - List links = new ArrayList<>(); - if (theResource.hasLink()) { - for (Patient.PatientLinkComponent link : theResource.getLink()) { - if (theLinkType.equals(link.getType()) && link.hasOther()) { - links.add(link.getOther()); - } - } - } - return links; - } - - private boolean validateSourceAndTargetAreSuitableForMerge( - Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { - - if (theSourceResource.getId().equalsIgnoreCase(theTargetResource.getId())) { - String msg = "Source and target resources are the same resource."; - // What is the right code to use in these cases? - addErrorToOperationOutcome(outcome, msg, "invalid"); - return false; - } - - if (theTargetResource.hasActive() && !theTargetResource.getActive()) { - String msg = "Target resource is not active, it must be active to be the target of a merge operation."; - addErrorToOperationOutcome(outcome, msg, "invalid"); - return false; - } - - List replacedByLinksInTarget = - getLinksOfTypeWithNonNullReference(theTargetResource, Patient.LinkType.REPLACEDBY); - if (!replacedByLinksInTarget.isEmpty()) { - String ref = replacedByLinksInTarget.get(0).getReference(); - String msg = String.format( - "Target resource was previously replaced by a resource with reference '%s', it " - + "is not a suitable target for merging.", - ref); - addErrorToOperationOutcome(outcome, msg, "invalid"); - return false; - } - - List replacedByLinksInSource = - getLinksOfTypeWithNonNullReference(theSourceResource, Patient.LinkType.REPLACEDBY); - if (!replacedByLinksInSource.isEmpty()) { - String ref = replacedByLinksInSource.get(0).getReference(); - String msg = String.format( - "Source resource was previously replaced by a resource with reference '%s', it " - + "is not a suitable source for merging.", - ref); - addErrorToOperationOutcome(outcome, msg, "invalid"); - return false; - } - - return true; - } - - /** - * Validates the merge operation parameters and adds validation errors to the outcome - * - * @param theMergeOperationParameters the merge operation parameters - * @param theOutcome the outcome to add validation errors to - * @return true if the parameters are valid, false otherwise - */ - private boolean validateMergeOperationParameters( - MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { - List errorMessages = new ArrayList<>(); - if (!theMergeOperationParameters.hasAtLeastOneSourceIdentifier() - && theMergeOperationParameters.getSourceResource() == null) { - String msg = String.format( - "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", - theMergeOperationParameters.getSourceResourceParameterName(), - theMergeOperationParameters.getSourceIdentifiersParameterName()); - errorMessages.add(msg); - } - - // Spec has conflicting information about this case - if (theMergeOperationParameters.hasAtLeastOneSourceIdentifier() - && theMergeOperationParameters.getSourceResource() != null) { - String msg = String.format( - "Source resource must be provided either by '%s' or by '%s', not both.", - theMergeOperationParameters.getSourceResourceParameterName(), - theMergeOperationParameters.getSourceIdentifiersParameterName()); - errorMessages.add(msg); - } - - if (!theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && theMergeOperationParameters.getTargetResource() == null) { - String msg = String.format( - "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", - theMergeOperationParameters.getTargetResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); - errorMessages.add(msg); - } - - // Spec has conflicting information about this case - if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && theMergeOperationParameters.getTargetResource() != null) { - String msg = String.format( - "Target resource must be provided either by '%s' or by '%s', not both.", - theMergeOperationParameters.getTargetResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); - errorMessages.add(msg); - } - - Reference sourceRef = (Reference) theMergeOperationParameters.getSourceResource(); - if (sourceRef != null && !sourceRef.hasReference()) { - String msg = String.format( - "Reference specified in '%s' parameter does not have a reference element.", - theMergeOperationParameters.getSourceResourceParameterName()); - errorMessages.add(msg); - } - - Reference targetRef = (Reference) theMergeOperationParameters.getTargetResource(); - if (targetRef != null && !targetRef.hasReference()) { - String msg = String.format( - "Reference specified in '%s' parameter does not have a reference element.", - theMergeOperationParameters.getTargetResourceParameterName()); - errorMessages.add(msg); - } - - if (!errorMessages.isEmpty()) { - for (String validationError : errorMessages) { - addErrorToOperationOutcome(theOutcome, validationError, "required"); - } - // there are validation errors - return false; - } - - // no validation errors - return true; - } - - private IBaseResource resolveSourceResource( - MergeOperationInputParameters theOperationParameters, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome) { - return resolveResource( - theOperationParameters.getSourceResource(), - theOperationParameters.getSourceIdentifiers(), - theRequestDetails, - theOutcome, - theOperationParameters.getSourceResourceParameterName(), - theOperationParameters.getSourceIdentifiersParameterName()); - } - - private IBaseResource resolveTargetResource( - MergeOperationInputParameters theOperationParameters, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome) { - return resolveResource( - theOperationParameters.getTargetResource(), - theOperationParameters.getTargetIdentifiers(), - theRequestDetails, - theOutcome, - theOperationParameters.getTargetResourceParameterName(), - theOperationParameters.getTargetIdentifiersParameterName()); - } - - private IBaseResource resolveResourceByIdentifiers( - List theIdentifiers, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationParameterName) { - - SearchParameterMap searchParameterMap = new SearchParameterMap(); - TokenAndListParam tokenAndListParam = new TokenAndListParam(); - for (CanonicalIdentifier identifier : theIdentifiers) { - TokenParam tokenParam = new TokenParam( - identifier.getSystemElement().getValueAsString(), - identifier.getValueElement().getValueAsString()); - tokenAndListParam.addAnd(tokenParam); - } - searchParameterMap.add("identifier", tokenAndListParam); - searchParameterMap.setCount(2); - - IBundleProvider bundle = myPatientDao.search(searchParameterMap, theRequestDetails); - List resources = bundle.getAllResources(); - if (resources.isEmpty()) { - String msg = String.format( - "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); - addErrorToOperationOutcome(theOutcome, msg, "not-found"); - return null; - } - if (resources.size() > 1) { - String msg = String.format( - "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); - addErrorToOperationOutcome(theOutcome, msg, "multiple-matches"); - return null; - } - - return resources.get(0); - } - - private IBaseResource resolveResourceByReference( - IBaseReference theReference, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationParameterName) { - // TODO Emre: why does IBaseReference not have getIdentifier or hasReference methods? - // casting it to r4.Reference for now - Reference r4ref = (Reference) theReference; - - IIdType theResourceId = new IdType(r4ref.getReferenceElement().getValue()); - IBaseResource resource; - try { - resource = myPatientDao.read(theResourceId.toVersionless(), theRequestDetails); - } catch (ResourceNotFoundException e) { - String msg = String.format( - "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); - addErrorToOperationOutcome(theOutcome, msg, "not-found"); - return null; - } - - if (theResourceId.hasVersionIdPart() - && !theResourceId - .getVersionIdPart() - .equals(resource.getIdElement().getVersionIdPart())) { - String msg = String.format( - "The reference in '%s' parameter has a version specified, " - + "but it is not the latest version of the resource", - theOperationParameterName); - addErrorToOperationOutcome(theOutcome, msg, "conflict"); - return null; - } - - return resource; - } - - private IBaseResource resolveResource( - IBaseReference theReference, - List theIdentifiers, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationReferenceParameterName, - String theOperationIdentifiersParameterName) { - if (theReference != null) { - return resolveResourceByReference( - theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); - } - - return resolveResourceByIdentifiers( - theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); - } - private void addInfoToOperationOutcome( IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { IBase issue = OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); OperationOutcomeUtil.addDetailsToIssue(myFhirContext, issue, null, null, theDetailsText); } - - private void addErrorToOperationOutcome(IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theCode) { - OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "error", theDiagnosticMsg, null, theCode); - } - - private static class ValidationResult { - protected final Patient sourceResource; - protected final Patient targetResource; - protected final boolean isValid; - - private ValidationResult(Patient theSourceResource, Patient theTargetResource, boolean theIsValid) { - sourceResource = theSourceResource; - targetResource = theTargetResource; - isValid = theIsValid; - } - - public static ValidationResult invalidResult() { - return new ValidationResult(null, null, false); - } - - public static ValidationResult validResult(Patient theSourceResource, Patient theTargetResource) { - return new ValidationResult(theSourceResource, theTargetResource, true); - } - } } diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java index a690cc796ec3..f1073bf3b60c 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java @@ -35,7 +35,6 @@ * Start a job and create a Task resource that tracks the status of the job. This is designed in a way that * it could be used by any Batch 2 job. */ - public class Batch2TaskHelper { public Task startJobAndCreateAssociatedTask( From 9d79ffe9ae93364f77247d467f4d60f9fc04b2d7 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 15:15:59 -0500 Subject: [PATCH 108/148] split merge validation service out from merge service --- .../merge/MergeValidationService.java | 236 +++++++++--------- 1 file changed, 118 insertions(+), 118 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java index b993d2e92419..93b3e70ad160 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java @@ -37,9 +37,9 @@ public MergeValidationService(FhirContext theFhirContext, DaoRegistry theDaoRegi } MergeValidationResult validate( - MergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { IBaseOperationOutcome operationOutcome = theMergeOutcome.getOperationOutcome(); @@ -50,7 +50,7 @@ MergeValidationResult validate( // cast to Patient, since we only support merging Patient resources for now Patient sourceResource = - (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); + (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (sourceResource == null) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); @@ -59,7 +59,7 @@ MergeValidationResult validate( // cast to Patient, since we only support merging Patient resources for now Patient targetResource = - (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); + (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (targetResource == null) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); @@ -72,7 +72,7 @@ MergeValidationResult validate( } if (!validateResultResourceIfExists( - theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { + theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); return MergeValidationResult.invalidResult(); } @@ -80,10 +80,10 @@ MergeValidationResult validate( } private boolean validateResultResourceIfExists( - MergeOperationInputParameters theMergeOperationParameters, - Patient theResolvedTargetResource, - Patient theResolvedSourceResource, - IBaseOperationOutcome theOperationOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + Patient theResolvedTargetResource, + Patient theResolvedSourceResource, + IBaseOperationOutcome theOperationOutcome) { if (theMergeOperationParameters.getResultResource() == null) { // result resource is not provided, no further validation is needed @@ -97,21 +97,21 @@ private boolean validateResultResourceIfExists( // validate the result resource's id as same as the target resource if (!theResolvedTargetResource.getIdElement().toVersionless().equals(theResultResource.getIdElement())) { String msg = String.format( - "'%s' must have the same versionless id as the actual resolved target resource. " - + "The actual resolved target resource's id is: '%s'", - theMergeOperationParameters.getResultResourceParameterName(), - theResolvedTargetResource.getIdElement().toVersionless().getValue()); + "'%s' must have the same versionless id as the actual resolved target resource. " + + "The actual resolved target resource's id is: '%s'", + theMergeOperationParameters.getResultResourceParameterName(), + theResolvedTargetResource.getIdElement().toVersionless().getValue()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); retval = false; } // validate the result resource contains the identifiers provided in the target identifiers param if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { + && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { String msg = String.format( - "'%s' must have all the identifiers provided in %s", - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "'%s' must have all the identifiers provided in %s", + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); retval = false; } @@ -121,11 +121,11 @@ private boolean validateResultResourceIfExists( // if the source resource is being deleted, the result resource must not have a replaces link to the source // resource if (!validateResultResourceReplacesLinkToSourceResource( - theResultResource, - theResolvedSourceResource, - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getDeleteSource(), - theOperationOutcome)) { + theResultResource, + theResolvedSourceResource, + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getDeleteSource(), + theOperationOutcome)) { retval = false; } @@ -141,9 +141,9 @@ private boolean hasAllIdentifiers(Patient theResource, List List identifiersInResource = theResource.getIdentifier(); for (CanonicalIdentifier identifier : theIdentifiers) { boolean identifierFound = identifiersInResource.stream() - .anyMatch(i -> i.getSystem() - .equals(identifier.getSystemElement().getValueAsString()) - && i.getValue().equals(identifier.getValueElement().getValueAsString())); + .anyMatch(i -> i.getSystem() + .equals(identifier.getSystemElement().getValueAsString()) + && i.getValue().equals(identifier.getValueElement().getValueAsString())); if (!identifierFound) { return false; @@ -153,37 +153,37 @@ private boolean hasAllIdentifiers(Patient theResource, List } private boolean validateResultResourceReplacesLinkToSourceResource( - Patient theResultResource, - Patient theResolvedSourceResource, - String theResultResourceParameterName, - boolean theDeleteSource, - IBaseOperationOutcome theOperationOutcome) { + Patient theResultResource, + Patient theResolvedSourceResource, + String theResultResourceParameterName, + boolean theDeleteSource, + IBaseOperationOutcome theOperationOutcome) { // the result resource must have the replaces link set to the source resource List replacesLinkToSourceResource = getLinksToResource( - theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); + theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); if (theDeleteSource) { if (!replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must not have a 'replaces' link to the source resource " - + "when the source resource will be deleted, as the link may prevent deleting the source " - + "resource.", - theResultResourceParameterName); + "'%s' must not have a 'replaces' link to the source resource " + + "when the source resource will be deleted, as the link may prevent deleting the source " + + "resource.", + theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } } else { if (replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); + "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } if (replacesLinkToSourceResource.size() > 1) { String msg = String.format( - "'%s' has multiple 'replaces' links to the source resource. There should be only one.", - theResultResourceParameterName); + "'%s' has multiple 'replaces' links to the source resource. There should be only one.", + theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } @@ -192,14 +192,14 @@ private boolean validateResultResourceReplacesLinkToSourceResource( } private List getLinksToResource( - Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { + Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { List links = getLinksOfTypeWithNonNullReference(theResource, theLinkType); return links.stream() - .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) - .collect(Collectors.toList()); + .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) + .collect(Collectors.toList()); } - protected List getLinksOfTypeWithNonNullReference(Patient theResource, Patient.LinkType theLinkType) { + private List getLinksOfTypeWithNonNullReference(Patient theResource, Patient.LinkType theLinkType) { List links = new ArrayList<>(); if (theResource.hasLink()) { for (Patient.PatientLinkComponent link : theResource.getLink()) { @@ -212,7 +212,7 @@ protected List getLinksOfTypeWithNonNullReference(Patient theResource } private boolean validateSourceAndTargetAreSuitableForMerge( - Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { + Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { if (theSourceResource.getId().equalsIgnoreCase(theTargetResource.getId())) { String msg = "Source and target resources are the same resource."; @@ -228,25 +228,25 @@ private boolean validateSourceAndTargetAreSuitableForMerge( } List replacedByLinksInTarget = - getLinksOfTypeWithNonNullReference(theTargetResource, Patient.LinkType.REPLACEDBY); + getLinksOfTypeWithNonNullReference(theTargetResource, Patient.LinkType.REPLACEDBY); if (!replacedByLinksInTarget.isEmpty()) { String ref = replacedByLinksInTarget.get(0).getReference(); String msg = String.format( - "Target resource was previously replaced by a resource with reference '%s', it " - + "is not a suitable target for merging.", - ref); + "Target resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable target for merging.", + ref); addErrorToOperationOutcome(outcome, msg, "invalid"); return false; } List replacedByLinksInSource = - getLinksOfTypeWithNonNullReference(theSourceResource, Patient.LinkType.REPLACEDBY); + getLinksOfTypeWithNonNullReference(theSourceResource, Patient.LinkType.REPLACEDBY); if (!replacedByLinksInSource.isEmpty()) { String ref = replacedByLinksInSource.get(0).getReference(); String msg = String.format( - "Source resource was previously replaced by a resource with reference '%s', it " - + "is not a suitable source for merging.", - ref); + "Source resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable source for merging.", + ref); addErrorToOperationOutcome(outcome, msg, "invalid"); return false; } @@ -262,59 +262,59 @@ private boolean validateSourceAndTargetAreSuitableForMerge( * @return true if the parameters are valid, false otherwise */ private boolean validateMergeOperationParameters( - MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { List errorMessages = new ArrayList<>(); if (!theMergeOperationParameters.hasAtLeastOneSourceIdentifier() - && theMergeOperationParameters.getSourceResource() == null) { + && theMergeOperationParameters.getSourceResource() == null) { String msg = String.format( - "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", - theMergeOperationParameters.getSourceResourceParameterName(), - theMergeOperationParameters.getSourceIdentifiersParameterName()); + "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); errorMessages.add(msg); } // Spec has conflicting information about this case if (theMergeOperationParameters.hasAtLeastOneSourceIdentifier() - && theMergeOperationParameters.getSourceResource() != null) { + && theMergeOperationParameters.getSourceResource() != null) { String msg = String.format( - "Source resource must be provided either by '%s' or by '%s', not both.", - theMergeOperationParameters.getSourceResourceParameterName(), - theMergeOperationParameters.getSourceIdentifiersParameterName()); + "Source resource must be provided either by '%s' or by '%s', not both.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); errorMessages.add(msg); } if (!theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && theMergeOperationParameters.getTargetResource() == null) { + && theMergeOperationParameters.getTargetResource() == null) { String msg = String.format( - "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", - theMergeOperationParameters.getTargetResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); errorMessages.add(msg); } // Spec has conflicting information about this case if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && theMergeOperationParameters.getTargetResource() != null) { + && theMergeOperationParameters.getTargetResource() != null) { String msg = String.format( - "Target resource must be provided either by '%s' or by '%s', not both.", - theMergeOperationParameters.getTargetResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "Target resource must be provided either by '%s' or by '%s', not both.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); errorMessages.add(msg); } Reference sourceRef = (Reference) theMergeOperationParameters.getSourceResource(); if (sourceRef != null && !sourceRef.hasReference()) { String msg = String.format( - "Reference specified in '%s' parameter does not have a reference element.", - theMergeOperationParameters.getSourceResourceParameterName()); + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getSourceResourceParameterName()); errorMessages.add(msg); } Reference targetRef = (Reference) theMergeOperationParameters.getTargetResource(); if (targetRef != null && !targetRef.hasReference()) { String msg = String.format( - "Reference specified in '%s' parameter does not have a reference element.", - theMergeOperationParameters.getTargetResourceParameterName()); + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getTargetResourceParameterName()); errorMessages.add(msg); } @@ -331,59 +331,59 @@ private boolean validateMergeOperationParameters( } private IBaseResource resolveSourceResource( - MergeOperationInputParameters theOperationParameters, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { return resolveResource( - theOperationParameters.getSourceResource(), - theOperationParameters.getSourceIdentifiers(), - theRequestDetails, - theOutcome, - theOperationParameters.getSourceResourceParameterName(), - theOperationParameters.getSourceIdentifiersParameterName()); + theOperationParameters.getSourceResource(), + theOperationParameters.getSourceIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getSourceResourceParameterName(), + theOperationParameters.getSourceIdentifiersParameterName()); } private IBaseResource resolveTargetResource( - MergeOperationInputParameters theOperationParameters, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { return resolveResource( - theOperationParameters.getTargetResource(), - theOperationParameters.getTargetIdentifiers(), - theRequestDetails, - theOutcome, - theOperationParameters.getTargetResourceParameterName(), - theOperationParameters.getTargetIdentifiersParameterName()); + theOperationParameters.getTargetResource(), + theOperationParameters.getTargetIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getTargetResourceParameterName(), + theOperationParameters.getTargetIdentifiersParameterName()); } private IBaseResource resolveResource( - IBaseReference theReference, - List theIdentifiers, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationReferenceParameterName, - String theOperationIdentifiersParameterName) { + IBaseReference theReference, + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationReferenceParameterName, + String theOperationIdentifiersParameterName) { if (theReference != null) { return resolveResourceByReference( - theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); + theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); } return resolveResourceByIdentifiers( - theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); + theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); } private IBaseResource resolveResourceByIdentifiers( - List theIdentifiers, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationParameterName) { + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { SearchParameterMap searchParameterMap = new SearchParameterMap(); TokenAndListParam tokenAndListParam = new TokenAndListParam(); for (CanonicalIdentifier identifier : theIdentifiers) { TokenParam tokenParam = new TokenParam( - identifier.getSystemElement().getValueAsString(), - identifier.getValueElement().getValueAsString()); + identifier.getSystemElement().getValueAsString(), + identifier.getValueElement().getValueAsString()); tokenAndListParam.addAnd(tokenParam); } searchParameterMap.add("identifier", tokenAndListParam); @@ -393,13 +393,13 @@ private IBaseResource resolveResourceByIdentifiers( List resources = bundle.getAllResources(); if (resources.isEmpty()) { String msg = String.format( - "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); + "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "not-found"); return null; } if (resources.size() > 1) { String msg = String.format( - "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); + "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "multiple-matches"); return null; } @@ -408,10 +408,10 @@ private IBaseResource resolveResourceByIdentifiers( } private IBaseResource resolveResourceByReference( - IBaseReference theReference, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationParameterName) { + IBaseReference theReference, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { // TODO Emre: why does IBaseReference not have getIdentifier or hasReference methods? // casting it to r4.Reference for now Reference r4ref = (Reference) theReference; @@ -422,19 +422,19 @@ private IBaseResource resolveResourceByReference( resource = myPatientDao.read(theResourceId.toVersionless(), theRequestDetails); } catch (ResourceNotFoundException e) { String msg = String.format( - "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); + "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "not-found"); return null; } if (theResourceId.hasVersionIdPart() - && !theResourceId - .getVersionIdPart() - .equals(resource.getIdElement().getVersionIdPart())) { + && !theResourceId + .getVersionIdPart() + .equals(resource.getIdElement().getVersionIdPart())) { String msg = String.format( - "The reference in '%s' parameter has a version specified, " - + "but it is not the latest version of the resource", - theOperationParameterName); + "The reference in '%s' parameter has a version specified, " + + "but it is not the latest version of the resource", + theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "conflict"); return null; } From e7bb29483038b8f51797bd63c1ae37425ba06cf3 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 15:17:57 -0500 Subject: [PATCH 109/148] split merge validation service out from merge service --- .../merge/MergeValidationService.java | 234 +++++++++--------- 1 file changed, 117 insertions(+), 117 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java index 93b3e70ad160..9c58152d0aa3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java @@ -37,9 +37,9 @@ public MergeValidationService(FhirContext theFhirContext, DaoRegistry theDaoRegi } MergeValidationResult validate( - MergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { IBaseOperationOutcome operationOutcome = theMergeOutcome.getOperationOutcome(); @@ -50,7 +50,7 @@ MergeValidationResult validate( // cast to Patient, since we only support merging Patient resources for now Patient sourceResource = - (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); + (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (sourceResource == null) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); @@ -59,7 +59,7 @@ MergeValidationResult validate( // cast to Patient, since we only support merging Patient resources for now Patient targetResource = - (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); + (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (targetResource == null) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); @@ -72,7 +72,7 @@ MergeValidationResult validate( } if (!validateResultResourceIfExists( - theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { + theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); return MergeValidationResult.invalidResult(); } @@ -80,10 +80,10 @@ MergeValidationResult validate( } private boolean validateResultResourceIfExists( - MergeOperationInputParameters theMergeOperationParameters, - Patient theResolvedTargetResource, - Patient theResolvedSourceResource, - IBaseOperationOutcome theOperationOutcome) { + MergeOperationInputParameters theMergeOperationParameters, + Patient theResolvedTargetResource, + Patient theResolvedSourceResource, + IBaseOperationOutcome theOperationOutcome) { if (theMergeOperationParameters.getResultResource() == null) { // result resource is not provided, no further validation is needed @@ -97,21 +97,21 @@ private boolean validateResultResourceIfExists( // validate the result resource's id as same as the target resource if (!theResolvedTargetResource.getIdElement().toVersionless().equals(theResultResource.getIdElement())) { String msg = String.format( - "'%s' must have the same versionless id as the actual resolved target resource. " - + "The actual resolved target resource's id is: '%s'", - theMergeOperationParameters.getResultResourceParameterName(), - theResolvedTargetResource.getIdElement().toVersionless().getValue()); + "'%s' must have the same versionless id as the actual resolved target resource. " + + "The actual resolved target resource's id is: '%s'", + theMergeOperationParameters.getResultResourceParameterName(), + theResolvedTargetResource.getIdElement().toVersionless().getValue()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); retval = false; } // validate the result resource contains the identifiers provided in the target identifiers param if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { + && !hasAllIdentifiers(theResultResource, theMergeOperationParameters.getTargetIdentifiers())) { String msg = String.format( - "'%s' must have all the identifiers provided in %s", - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "'%s' must have all the identifiers provided in %s", + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); retval = false; } @@ -121,11 +121,11 @@ private boolean validateResultResourceIfExists( // if the source resource is being deleted, the result resource must not have a replaces link to the source // resource if (!validateResultResourceReplacesLinkToSourceResource( - theResultResource, - theResolvedSourceResource, - theMergeOperationParameters.getResultResourceParameterName(), - theMergeOperationParameters.getDeleteSource(), - theOperationOutcome)) { + theResultResource, + theResolvedSourceResource, + theMergeOperationParameters.getResultResourceParameterName(), + theMergeOperationParameters.getDeleteSource(), + theOperationOutcome)) { retval = false; } @@ -141,9 +141,9 @@ private boolean hasAllIdentifiers(Patient theResource, List List identifiersInResource = theResource.getIdentifier(); for (CanonicalIdentifier identifier : theIdentifiers) { boolean identifierFound = identifiersInResource.stream() - .anyMatch(i -> i.getSystem() - .equals(identifier.getSystemElement().getValueAsString()) - && i.getValue().equals(identifier.getValueElement().getValueAsString())); + .anyMatch(i -> i.getSystem() + .equals(identifier.getSystemElement().getValueAsString()) + && i.getValue().equals(identifier.getValueElement().getValueAsString())); if (!identifierFound) { return false; @@ -153,37 +153,37 @@ private boolean hasAllIdentifiers(Patient theResource, List } private boolean validateResultResourceReplacesLinkToSourceResource( - Patient theResultResource, - Patient theResolvedSourceResource, - String theResultResourceParameterName, - boolean theDeleteSource, - IBaseOperationOutcome theOperationOutcome) { + Patient theResultResource, + Patient theResolvedSourceResource, + String theResultResourceParameterName, + boolean theDeleteSource, + IBaseOperationOutcome theOperationOutcome) { // the result resource must have the replaces link set to the source resource List replacesLinkToSourceResource = getLinksToResource( - theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); + theResultResource, Patient.LinkType.REPLACES, theResolvedSourceResource.getIdElement()); if (theDeleteSource) { if (!replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must not have a 'replaces' link to the source resource " - + "when the source resource will be deleted, as the link may prevent deleting the source " - + "resource.", - theResultResourceParameterName); + "'%s' must not have a 'replaces' link to the source resource " + + "when the source resource will be deleted, as the link may prevent deleting the source " + + "resource.", + theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } } else { if (replacesLinkToSourceResource.isEmpty()) { String msg = String.format( - "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); + "'%s' must have a 'replaces' link to the source resource.", theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } if (replacesLinkToSourceResource.size() > 1) { String msg = String.format( - "'%s' has multiple 'replaces' links to the source resource. There should be only one.", - theResultResourceParameterName); + "'%s' has multiple 'replaces' links to the source resource. There should be only one.", + theResultResourceParameterName); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); return false; } @@ -192,11 +192,11 @@ private boolean validateResultResourceReplacesLinkToSourceResource( } private List getLinksToResource( - Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { + Patient theResource, Patient.LinkType theLinkType, IIdType theResourceId) { List links = getLinksOfTypeWithNonNullReference(theResource, theLinkType); return links.stream() - .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) - .collect(Collectors.toList()); + .filter(r -> theResourceId.toVersionless().getValue().equals(r.getReference())) + .collect(Collectors.toList()); } private List getLinksOfTypeWithNonNullReference(Patient theResource, Patient.LinkType theLinkType) { @@ -212,7 +212,7 @@ private List getLinksOfTypeWithNonNullReference(Patient theResource, } private boolean validateSourceAndTargetAreSuitableForMerge( - Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { + Patient theSourceResource, Patient theTargetResource, IBaseOperationOutcome outcome) { if (theSourceResource.getId().equalsIgnoreCase(theTargetResource.getId())) { String msg = "Source and target resources are the same resource."; @@ -228,25 +228,25 @@ private boolean validateSourceAndTargetAreSuitableForMerge( } List replacedByLinksInTarget = - getLinksOfTypeWithNonNullReference(theTargetResource, Patient.LinkType.REPLACEDBY); + getLinksOfTypeWithNonNullReference(theTargetResource, Patient.LinkType.REPLACEDBY); if (!replacedByLinksInTarget.isEmpty()) { String ref = replacedByLinksInTarget.get(0).getReference(); String msg = String.format( - "Target resource was previously replaced by a resource with reference '%s', it " - + "is not a suitable target for merging.", - ref); + "Target resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable target for merging.", + ref); addErrorToOperationOutcome(outcome, msg, "invalid"); return false; } List replacedByLinksInSource = - getLinksOfTypeWithNonNullReference(theSourceResource, Patient.LinkType.REPLACEDBY); + getLinksOfTypeWithNonNullReference(theSourceResource, Patient.LinkType.REPLACEDBY); if (!replacedByLinksInSource.isEmpty()) { String ref = replacedByLinksInSource.get(0).getReference(); String msg = String.format( - "Source resource was previously replaced by a resource with reference '%s', it " - + "is not a suitable source for merging.", - ref); + "Source resource was previously replaced by a resource with reference '%s', it " + + "is not a suitable source for merging.", + ref); addErrorToOperationOutcome(outcome, msg, "invalid"); return false; } @@ -262,59 +262,59 @@ private boolean validateSourceAndTargetAreSuitableForMerge( * @return true if the parameters are valid, false otherwise */ private boolean validateMergeOperationParameters( - MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { List errorMessages = new ArrayList<>(); if (!theMergeOperationParameters.hasAtLeastOneSourceIdentifier() - && theMergeOperationParameters.getSourceResource() == null) { + && theMergeOperationParameters.getSourceResource() == null) { String msg = String.format( - "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", - theMergeOperationParameters.getSourceResourceParameterName(), - theMergeOperationParameters.getSourceIdentifiersParameterName()); + "There are no source resource parameters provided, include either a '%s', or a '%s' parameter.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); errorMessages.add(msg); } // Spec has conflicting information about this case if (theMergeOperationParameters.hasAtLeastOneSourceIdentifier() - && theMergeOperationParameters.getSourceResource() != null) { + && theMergeOperationParameters.getSourceResource() != null) { String msg = String.format( - "Source resource must be provided either by '%s' or by '%s', not both.", - theMergeOperationParameters.getSourceResourceParameterName(), - theMergeOperationParameters.getSourceIdentifiersParameterName()); + "Source resource must be provided either by '%s' or by '%s', not both.", + theMergeOperationParameters.getSourceResourceParameterName(), + theMergeOperationParameters.getSourceIdentifiersParameterName()); errorMessages.add(msg); } if (!theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && theMergeOperationParameters.getTargetResource() == null) { + && theMergeOperationParameters.getTargetResource() == null) { String msg = String.format( - "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", - theMergeOperationParameters.getTargetResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "There are no target resource parameters provided, include either a '%s', or a '%s' parameter.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); errorMessages.add(msg); } // Spec has conflicting information about this case if (theMergeOperationParameters.hasAtLeastOneTargetIdentifier() - && theMergeOperationParameters.getTargetResource() != null) { + && theMergeOperationParameters.getTargetResource() != null) { String msg = String.format( - "Target resource must be provided either by '%s' or by '%s', not both.", - theMergeOperationParameters.getTargetResourceParameterName(), - theMergeOperationParameters.getTargetIdentifiersParameterName()); + "Target resource must be provided either by '%s' or by '%s', not both.", + theMergeOperationParameters.getTargetResourceParameterName(), + theMergeOperationParameters.getTargetIdentifiersParameterName()); errorMessages.add(msg); } Reference sourceRef = (Reference) theMergeOperationParameters.getSourceResource(); if (sourceRef != null && !sourceRef.hasReference()) { String msg = String.format( - "Reference specified in '%s' parameter does not have a reference element.", - theMergeOperationParameters.getSourceResourceParameterName()); + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getSourceResourceParameterName()); errorMessages.add(msg); } Reference targetRef = (Reference) theMergeOperationParameters.getTargetResource(); if (targetRef != null && !targetRef.hasReference()) { String msg = String.format( - "Reference specified in '%s' parameter does not have a reference element.", - theMergeOperationParameters.getTargetResourceParameterName()); + "Reference specified in '%s' parameter does not have a reference element.", + theMergeOperationParameters.getTargetResourceParameterName()); errorMessages.add(msg); } @@ -331,59 +331,59 @@ private boolean validateMergeOperationParameters( } private IBaseResource resolveSourceResource( - MergeOperationInputParameters theOperationParameters, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { return resolveResource( - theOperationParameters.getSourceResource(), - theOperationParameters.getSourceIdentifiers(), - theRequestDetails, - theOutcome, - theOperationParameters.getSourceResourceParameterName(), - theOperationParameters.getSourceIdentifiersParameterName()); + theOperationParameters.getSourceResource(), + theOperationParameters.getSourceIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getSourceResourceParameterName(), + theOperationParameters.getSourceIdentifiersParameterName()); } private IBaseResource resolveTargetResource( - MergeOperationInputParameters theOperationParameters, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome) { + MergeOperationInputParameters theOperationParameters, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome) { return resolveResource( - theOperationParameters.getTargetResource(), - theOperationParameters.getTargetIdentifiers(), - theRequestDetails, - theOutcome, - theOperationParameters.getTargetResourceParameterName(), - theOperationParameters.getTargetIdentifiersParameterName()); + theOperationParameters.getTargetResource(), + theOperationParameters.getTargetIdentifiers(), + theRequestDetails, + theOutcome, + theOperationParameters.getTargetResourceParameterName(), + theOperationParameters.getTargetIdentifiersParameterName()); } private IBaseResource resolveResource( - IBaseReference theReference, - List theIdentifiers, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationReferenceParameterName, - String theOperationIdentifiersParameterName) { + IBaseReference theReference, + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationReferenceParameterName, + String theOperationIdentifiersParameterName) { if (theReference != null) { return resolveResourceByReference( - theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); + theReference, theRequestDetails, theOutcome, theOperationReferenceParameterName); } return resolveResourceByIdentifiers( - theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); + theIdentifiers, theRequestDetails, theOutcome, theOperationIdentifiersParameterName); } private IBaseResource resolveResourceByIdentifiers( - List theIdentifiers, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationParameterName) { + List theIdentifiers, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { SearchParameterMap searchParameterMap = new SearchParameterMap(); TokenAndListParam tokenAndListParam = new TokenAndListParam(); for (CanonicalIdentifier identifier : theIdentifiers) { TokenParam tokenParam = new TokenParam( - identifier.getSystemElement().getValueAsString(), - identifier.getValueElement().getValueAsString()); + identifier.getSystemElement().getValueAsString(), + identifier.getValueElement().getValueAsString()); tokenAndListParam.addAnd(tokenParam); } searchParameterMap.add("identifier", tokenAndListParam); @@ -393,13 +393,13 @@ private IBaseResource resolveResourceByIdentifiers( List resources = bundle.getAllResources(); if (resources.isEmpty()) { String msg = String.format( - "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); + "No resources found matching the identifier(s) specified in '%s'", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "not-found"); return null; } if (resources.size() > 1) { String msg = String.format( - "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); + "Multiple resources found matching the identifier(s) specified in '%s'", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "multiple-matches"); return null; } @@ -408,10 +408,10 @@ private IBaseResource resolveResourceByIdentifiers( } private IBaseResource resolveResourceByReference( - IBaseReference theReference, - RequestDetails theRequestDetails, - IBaseOperationOutcome theOutcome, - String theOperationParameterName) { + IBaseReference theReference, + RequestDetails theRequestDetails, + IBaseOperationOutcome theOutcome, + String theOperationParameterName) { // TODO Emre: why does IBaseReference not have getIdentifier or hasReference methods? // casting it to r4.Reference for now Reference r4ref = (Reference) theReference; @@ -422,19 +422,19 @@ private IBaseResource resolveResourceByReference( resource = myPatientDao.read(theResourceId.toVersionless(), theRequestDetails); } catch (ResourceNotFoundException e) { String msg = String.format( - "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); + "Resource not found for the reference specified in '%s' parameter", theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "not-found"); return null; } if (theResourceId.hasVersionIdPart() - && !theResourceId - .getVersionIdPart() - .equals(resource.getIdElement().getVersionIdPart())) { + && !theResourceId + .getVersionIdPart() + .equals(resource.getIdElement().getVersionIdPart())) { String msg = String.format( - "The reference in '%s' parameter has a version specified, " - + "but it is not the latest version of the resource", - theOperationParameterName); + "The reference in '%s' parameter has a version specified, " + + "but it is not the latest version of the resource", + theOperationParameterName); addErrorToOperationOutcome(theOutcome, msg, "conflict"); return null; } From 57f14e0396419968c63a0f7db7756264c03ea57d Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 19:05:13 -0500 Subject: [PATCH 110/148] review feedback --- .../BaseJpaResourceProviderPatient.java | 4 +- .../jpa/provider/IReplaceReferencesSvc.java | 9 +- .../fhir/jpa/provider/JpaSystemProvider.java | 101 ++++++++++-------- .../provider/ReplaceReferencesSvcImpl.java | 10 +- .../jpa/provider/r4/PatientMergeR4Test.java | 3 + .../provider/r4/ReplaceReferencesR4Test.java | 2 + .../src/test/resources/logback-test.xml | 2 +- .../ReplaceReferencesTestHelper.java | 3 + ...ReplaceReferenceUpdateTaskReducerStep.java | 9 +- .../ReplaceReferencesRequest.java | 9 +- 10 files changed, 85 insertions(+), 67 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 4ed9918a699c..584604c46893 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -293,9 +293,9 @@ public IBaseParameters patientMerge( startRequest(theServletRequest); - int batchSize = myStorageSettings.getTransactionWriteBatchSizeFromOperationParameter(theBatchSize); - try { + int batchSize = myStorageSettings.getTransactionWriteBatchSizeFromOperationParameter(theBatchSize); + MergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( theSourcePatientIdentifier, theTargetPatientIdentifier, diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java index 4e9fbd256c3f..fc96b377af27 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/IReplaceReferencesSvc.java @@ -25,12 +25,19 @@ import org.hl7.fhir.instance.model.api.IIdType; /** - * Contract for service which replaces references + * Find all references to a source resource and replace them with references to the provided target */ public interface IReplaceReferencesSvc { + /** + * Find all references to a source resource and replace them with references to the provided target + */ IBaseParameters replaceReferences( ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails); + /** + * To support $merge preview mode, provide a count of how many references would be updated if replaceReferences + * was called + */ Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index fae225c79060..342343753be1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -33,6 +33,7 @@ import ca.uhn.fhir.rest.annotation.Transaction; import ca.uhn.fhir.rest.annotation.TransactionParam; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ParametersUtil; @@ -41,9 +42,9 @@ import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.model.StringType; import org.springframework.beans.factory.annotation.Autowired; -import java.security.InvalidParameterException; import java.util.Collections; import java.util.Map; import java.util.TreeMap; @@ -60,18 +61,18 @@ public final class JpaSystemProvider extends BaseJpaSystemProvider private RequestPartitionHelperSvc myRequestPartitionHelperSvc; @Description( - "Marks all currently existing resources of a given type, or all resources of all types, for reindexing.") + "Marks all currently existing resources of a given type, or all resources of all types, for reindexing.") @Operation( - name = MARK_ALL_RESOURCES_FOR_REINDEXING, - idempotent = false, - returnParameters = {@OperationParam(name = "status")}) + name = MARK_ALL_RESOURCES_FOR_REINDEXING, + idempotent = false, + returnParameters = {@OperationParam(name = "status")}) /** * @deprecated * @see ReindexProvider#Reindex(List, IPrimitiveType, RequestDetails) */ @Deprecated public IBaseResource markAllResourcesForReindexing( - @OperationParam(name = "type", min = 0, max = 1, typeName = "code") IPrimitiveType theType) { + @OperationParam(name = "type", min = 0, max = 1, typeName = "code") IPrimitiveType theType) { if (theType != null && isNotBlank(theType.getValueAsString())) { getResourceReindexingSvc().markAllResourcesForReindexing(theType.getValueAsString()); @@ -89,9 +90,9 @@ public IBaseResource markAllResourcesForReindexing( @Description("Forces a single pass of the resource reindexing processor") @Operation( - name = PERFORM_REINDEXING_PASS, - idempotent = false, - returnParameters = {@OperationParam(name = "status")}) + name = PERFORM_REINDEXING_PASS, + idempotent = false, + returnParameters = {@OperationParam(name = "status")}) /** * @deprecated * @see ReindexProvider#Reindex(List, IPrimitiveType, RequestDetails) @@ -115,8 +116,8 @@ public IBaseResource performReindexingPass() { @Operation(name = JpaConstants.OPERATION_GET_RESOURCE_COUNTS, idempotent = true) @Description( - shortDefinition = - "Provides the number of resources currently stored on the server, broken down by resource type") + shortDefinition = + "Provides the number of resources currently stored on the server, broken down by resource type") public IBaseParameters getResourceCounts() { IBaseParameters retVal = ParametersUtil.newInstance(getContext()); @@ -125,23 +126,23 @@ public IBaseParameters getResourceCounts() { counts = new TreeMap<>(counts); for (Map.Entry nextEntry : counts.entrySet()) { ParametersUtil.addParameterToParametersInteger( - getContext(), - retVal, - nextEntry.getKey(), - nextEntry.getValue().intValue()); + getContext(), + retVal, + nextEntry.getKey(), + nextEntry.getValue().intValue()); } return retVal; } @Operation( - name = ProviderConstants.OPERATION_META, - idempotent = true, - returnParameters = {@OperationParam(name = "return", typeName = "Meta")}) + name = ProviderConstants.OPERATION_META, + idempotent = true, + returnParameters = {@OperationParam(name = "return", typeName = "Meta")}) public IBaseParameters meta(RequestDetails theRequestDetails) { IBaseParameters retVal = ParametersUtil.newInstance(getContext()); ParametersUtil.addParameterToParameters( - getContext(), retVal, "return", getDao().metaGetOperation(theRequestDetails)); + getContext(), retVal, "return", getDao().metaGetOperation(theRequestDetails)); return retVal; } @@ -159,45 +160,51 @@ public IBaseBundle transaction(RequestDetails theRequestDetails, @TransactionPar @Operation(name = ProviderConstants.OPERATION_REPLACE_REFERENCES, global = true) @Description( - value = - "This operation searches for all references matching the provided id and updates them to references to the provided newReferenceTargetId.", - shortDefinition = "Repoints referencing resources to another resources instance") + value = + "This operation searches for all references matching the provided id and updates them to references to the provided target-reference-id.", + shortDefinition = "Repoints referencing resources to another resources instance") public IBaseParameters replaceReferences( - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID) - String theSourceId, - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID) - String theTargetId, - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_BATCH_SIZE, typeName = "unsignedInt") - IPrimitiveType theBatchSize, - ServletRequestDetails theRequestDetails) { - validateReplaceReferencesParams(theSourceId, theTargetId); - int batchSize = myStorageSettings.getTransactionWriteBatchSizeFromOperationParameter(theBatchSize); - - IdDt sourceId = new IdDt(theSourceId); - IdDt targetId = new IdDt(theTargetId); - RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( - theRequestDetails, ReadPartitionIdRequestDetails.forRead(targetId)); - ReplaceReferencesRequest replaceReferencesRequest = + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID, min = 1, typeName = "string") + String theSourceId, + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID, min = 1, typeName = "string") + String theTargetId, + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_BATCH_SIZE, typeName = "unsignedInt") + IPrimitiveType theBatchSize, + ServletRequestDetails theServletRequest) { + startRequest(theServletRequest); + + try { + validateReplaceReferencesParams(theSourceId, theTargetId); + int batchSize = myStorageSettings.getTransactionWriteBatchSizeFromOperationParameter(theBatchSize); + + IdDt sourceId = new IdDt(theSourceId); + IdDt targetId = new IdDt(theTargetId); + RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( + theServletRequest, ReadPartitionIdRequestDetails.forRead(targetId)); + ReplaceReferencesRequest replaceReferencesRequest = new ReplaceReferencesRequest(sourceId, targetId, batchSize, partitionId); - IBaseParameters retval = - getReplaceReferencesSvc().replaceReferences(replaceReferencesRequest, theRequestDetails); - if (ParametersUtil.getNamedParameter(getContext(), retval, OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) + IBaseParameters retval = + getReplaceReferencesSvc().replaceReferences(replaceReferencesRequest, theServletRequest); + if (ParametersUtil.getNamedParameter(getContext(), retval, OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) .isPresent()) { - HttpServletResponse response = theRequestDetails.getServletResponse(); - response.setStatus(HttpServletResponse.SC_ACCEPTED); + HttpServletResponse response = theServletRequest.getServletResponse(); + response.setStatus(HttpServletResponse.SC_ACCEPTED); + } + return retval; + } finally { + endRequest(theServletRequest); } - return retval; } private static void validateReplaceReferencesParams(String theSourceId, String theTargetId) { if (isBlank(theSourceId)) { - throw new InvalidParameterException(Msg.code(2583) + "Parameter '" - + OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID + "' is blank"); + throw new InvalidRequestException(Msg.code(2583) + "Parameter '" + + OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID + "' is blank"); } if (isBlank(theTargetId)) { - throw new InvalidParameterException(Msg.code(2584) + "Parameter '" - + OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID + "' is blank"); + throw new InvalidRequestException(Msg.code(2584) + "Parameter '" + + OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID + "' is blank"); } } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 5795b4b5086b..d32da70cce85 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -148,15 +148,11 @@ private IBaseParameters replaceReferencesPreferSync( private IBaseParameters replaceReferencesForceSync( ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) { - // TODO KHS get partition from request List allIds = myHapiTransactionService .withRequest(theRequestDetails) - .execute(() -> { - Stream idStream = myResourceLinkDao.streamSourceIdsForTargetFhirId( - theReplaceReferencesRequest.sourceId.getResourceType(), - theReplaceReferencesRequest.sourceId.getIdPart()); - return idStream.collect(Collectors.toList()); - }); + .execute(() -> myResourceLinkDao.streamSourceIdsForTargetFhirId( + theReplaceReferencesRequest.sourceId.getResourceType(), + theReplaceReferencesRequest.sourceId.getIdPart()).collect(Collectors.toList())); Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources( theReplaceReferencesRequest, allIds, theRequestDetails); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 2f0c68a766c7..de86447ae418 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -210,6 +210,9 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea // Assert Merged Patient Patient mergedPatient = (Patient) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_RESULT).getResource(); List identifiers = mergedPatient.getIdentifier(); + + // TODO ED We can also validate that result patient returned here has the same id as the target patient. + // And maybe in not preview case, we should also read the target patient from the db and assert it equals to the result returned. myTestHelper.assertIdentifiers(identifiers, expectedIdentifiersOnTargetAfterMerge); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index 05a46a8ab991..d5d8171dfb23 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -132,6 +132,8 @@ void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { myTestHelper.assertAllReferencesUpdated(); } + // TODO ED we should add some tests for the invalid request error cases (and assert 4xx status code) + @Override protected boolean verboseClientLogging() { return true; diff --git a/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml b/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml index ce20ecdc53ec..569c06704c48 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml +++ b/hapi-fhir-jpaserver-test-r4/src/test/resources/logback-test.xml @@ -6,7 +6,7 @@ - + diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index ce9ddf6b262e..d78859c5e970 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -292,6 +292,9 @@ public void assertNothingChanged() { assertThat(actual).doesNotContainAnyElementsOf(mySourceObsIds); assertThat(actual).contains(myTargetPatientId); assertThat(actual).contains(myTargetEnc1); + + // TODO ED should we also assert here that source still has the all references it had before the operation, + // that is in addition to the validation that target doesn't contain the references. } public PatientMergeInputParameters buildMultipleTargetMatchParameters( diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java index 1cb22c1c0d35..df31a3432943 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java @@ -28,6 +28,7 @@ import ca.uhn.fhir.batch2.model.ChunkOutcome; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import jakarta.annotation.Nonnull; import org.hl7.fhir.r4.model.Bundle; @@ -43,13 +44,13 @@ public class ReplaceReferenceUpdateTaskReducerStep myPatchOutputBundles = new ArrayList<>(); + private IFhirResourceDao myTaskDao; public ReplaceReferenceUpdateTaskReducerStep(DaoRegistry theDaoRegistry) { - myDaoRegistry = theDaoRegistry; myFhirContext = theDaoRegistry.getFhirContext(); + myTaskDao = theDaoRegistry.getResourceDao(Task.class); } @Nonnull @@ -72,7 +73,7 @@ public RunOutcome run( ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); SystemRequestDetails requestDetails = SystemRequestDetails.forRequestPartitionId(params.getPartitionId()); Task task = - myDaoRegistry.getResourceDao(Task.class).read(params.getTaskId().asIdDt(), requestDetails); + myTaskDao.read(params.getTaskId().asIdDt(), requestDetails); task.setStatus(Task.TaskStatus.COMPLETED); // TODO KHS this Task will probably be too large for large jobs. Revisit this model once we support Provenance @@ -88,7 +89,7 @@ public RunOutcome run( task.addContained(outputBundle); }); - myDaoRegistry.getResourceDao(Task.class).update(task, requestDetails); + myTaskDao.update(task, requestDetails); ReplaceReferenceResultsJson result = new ReplaceReferenceResultsJson(); result.setTaskId(params.getTaskId()); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java index 6e5401d3b242..f3744b0f1737 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java @@ -21,11 +21,10 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import jakarta.annotation.Nonnull; import org.hl7.fhir.instance.model.api.IIdType; -import java.security.InvalidParameterException; - import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -56,19 +55,19 @@ public ReplaceReferencesRequest( public void validateOrThrowInvalidParameterException() { if (isBlank(sourceId.getResourceType())) { - throw new InvalidParameterException( + throw new InvalidRequestException( Msg.code(2585) + "'" + OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID + "' must be a resource type qualified id"); } if (isBlank(targetId.getResourceType())) { - throw new InvalidParameterException( + throw new InvalidRequestException( Msg.code(2586) + "'" + OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID + "' must be a resource type qualified id"); } if (!targetId.getResourceType().equals(sourceId.getResourceType())) { - throw new InvalidParameterException( + throw new InvalidRequestException( Msg.code(2587) + "Source and target id parameters must be for the same resource type"); } } From 74852d244508fda7791ee9848bb0c240ed11091a Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Thu, 19 Dec 2024 19:05:25 -0500 Subject: [PATCH 111/148] review feedback --- .../fhir/jpa/provider/JpaSystemProvider.java | 75 ++++++++++--------- .../provider/ReplaceReferencesSvcImpl.java | 8 +- ...ReplaceReferenceUpdateTaskReducerStep.java | 3 +- 3 files changed, 46 insertions(+), 40 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index 342343753be1..f47a683c4f34 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -42,7 +42,6 @@ import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r4.model.StringType; import org.springframework.beans.factory.annotation.Autowired; import java.util.Collections; @@ -61,18 +60,18 @@ public final class JpaSystemProvider extends BaseJpaSystemProvider private RequestPartitionHelperSvc myRequestPartitionHelperSvc; @Description( - "Marks all currently existing resources of a given type, or all resources of all types, for reindexing.") + "Marks all currently existing resources of a given type, or all resources of all types, for reindexing.") @Operation( - name = MARK_ALL_RESOURCES_FOR_REINDEXING, - idempotent = false, - returnParameters = {@OperationParam(name = "status")}) + name = MARK_ALL_RESOURCES_FOR_REINDEXING, + idempotent = false, + returnParameters = {@OperationParam(name = "status")}) /** * @deprecated * @see ReindexProvider#Reindex(List, IPrimitiveType, RequestDetails) */ @Deprecated public IBaseResource markAllResourcesForReindexing( - @OperationParam(name = "type", min = 0, max = 1, typeName = "code") IPrimitiveType theType) { + @OperationParam(name = "type", min = 0, max = 1, typeName = "code") IPrimitiveType theType) { if (theType != null && isNotBlank(theType.getValueAsString())) { getResourceReindexingSvc().markAllResourcesForReindexing(theType.getValueAsString()); @@ -90,9 +89,9 @@ public IBaseResource markAllResourcesForReindexing( @Description("Forces a single pass of the resource reindexing processor") @Operation( - name = PERFORM_REINDEXING_PASS, - idempotent = false, - returnParameters = {@OperationParam(name = "status")}) + name = PERFORM_REINDEXING_PASS, + idempotent = false, + returnParameters = {@OperationParam(name = "status")}) /** * @deprecated * @see ReindexProvider#Reindex(List, IPrimitiveType, RequestDetails) @@ -116,8 +115,8 @@ public IBaseResource performReindexingPass() { @Operation(name = JpaConstants.OPERATION_GET_RESOURCE_COUNTS, idempotent = true) @Description( - shortDefinition = - "Provides the number of resources currently stored on the server, broken down by resource type") + shortDefinition = + "Provides the number of resources currently stored on the server, broken down by resource type") public IBaseParameters getResourceCounts() { IBaseParameters retVal = ParametersUtil.newInstance(getContext()); @@ -126,23 +125,23 @@ public IBaseParameters getResourceCounts() { counts = new TreeMap<>(counts); for (Map.Entry nextEntry : counts.entrySet()) { ParametersUtil.addParameterToParametersInteger( - getContext(), - retVal, - nextEntry.getKey(), - nextEntry.getValue().intValue()); + getContext(), + retVal, + nextEntry.getKey(), + nextEntry.getValue().intValue()); } return retVal; } @Operation( - name = ProviderConstants.OPERATION_META, - idempotent = true, - returnParameters = {@OperationParam(name = "return", typeName = "Meta")}) + name = ProviderConstants.OPERATION_META, + idempotent = true, + returnParameters = {@OperationParam(name = "return", typeName = "Meta")}) public IBaseParameters meta(RequestDetails theRequestDetails) { IBaseParameters retVal = ParametersUtil.newInstance(getContext()); ParametersUtil.addParameterToParameters( - getContext(), retVal, "return", getDao().metaGetOperation(theRequestDetails)); + getContext(), retVal, "return", getDao().metaGetOperation(theRequestDetails)); return retVal; } @@ -160,17 +159,23 @@ public IBaseBundle transaction(RequestDetails theRequestDetails, @TransactionPar @Operation(name = ProviderConstants.OPERATION_REPLACE_REFERENCES, global = true) @Description( - value = - "This operation searches for all references matching the provided id and updates them to references to the provided target-reference-id.", - shortDefinition = "Repoints referencing resources to another resources instance") + value = + "This operation searches for all references matching the provided id and updates them to references to the provided target-reference-id.", + shortDefinition = "Repoints referencing resources to another resources instance") public IBaseParameters replaceReferences( - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID, min = 1, typeName = "string") - String theSourceId, - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID, min = 1, typeName = "string") - String theTargetId, - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_BATCH_SIZE, typeName = "unsignedInt") - IPrimitiveType theBatchSize, - ServletRequestDetails theServletRequest) { + @OperationParam( + name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID, + min = 1, + typeName = "string") + String theSourceId, + @OperationParam( + name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID, + min = 1, + typeName = "string") + String theTargetId, + @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_BATCH_SIZE, typeName = "unsignedInt") + IPrimitiveType theBatchSize, + ServletRequestDetails theServletRequest) { startRequest(theServletRequest); try { @@ -180,13 +185,13 @@ public IBaseParameters replaceReferences( IdDt sourceId = new IdDt(theSourceId); IdDt targetId = new IdDt(theTargetId); RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( - theServletRequest, ReadPartitionIdRequestDetails.forRead(targetId)); + theServletRequest, ReadPartitionIdRequestDetails.forRead(targetId)); ReplaceReferencesRequest replaceReferencesRequest = - new ReplaceReferencesRequest(sourceId, targetId, batchSize, partitionId); + new ReplaceReferencesRequest(sourceId, targetId, batchSize, partitionId); IBaseParameters retval = - getReplaceReferencesSvc().replaceReferences(replaceReferencesRequest, theServletRequest); + getReplaceReferencesSvc().replaceReferences(replaceReferencesRequest, theServletRequest); if (ParametersUtil.getNamedParameter(getContext(), retval, OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) - .isPresent()) { + .isPresent()) { HttpServletResponse response = theServletRequest.getServletResponse(); response.setStatus(HttpServletResponse.SC_ACCEPTED); } @@ -199,12 +204,12 @@ public IBaseParameters replaceReferences( private static void validateReplaceReferencesParams(String theSourceId, String theTargetId) { if (isBlank(theSourceId)) { throw new InvalidRequestException(Msg.code(2583) + "Parameter '" - + OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID + "' is blank"); + + OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID + "' is blank"); } if (isBlank(theTargetId)) { throw new InvalidRequestException(Msg.code(2584) + "Parameter '" - + OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID + "' is blank"); + + OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID + "' is blank"); } } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index d32da70cce85..6495e2c31b43 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -150,9 +150,11 @@ private IBaseParameters replaceReferencesForceSync( List allIds = myHapiTransactionService .withRequest(theRequestDetails) - .execute(() -> myResourceLinkDao.streamSourceIdsForTargetFhirId( - theReplaceReferencesRequest.sourceId.getResourceType(), - theReplaceReferencesRequest.sourceId.getIdPart()).collect(Collectors.toList())); + .execute(() -> myResourceLinkDao + .streamSourceIdsForTargetFhirId( + theReplaceReferencesRequest.sourceId.getResourceType(), + theReplaceReferencesRequest.sourceId.getIdPart()) + .collect(Collectors.toList())); Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources( theReplaceReferencesRequest, allIds, theRequestDetails); diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java index df31a3432943..f1d45bdaa02f 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java @@ -72,8 +72,7 @@ public RunOutcome run( ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); SystemRequestDetails requestDetails = SystemRequestDetails.forRequestPartitionId(params.getPartitionId()); - Task task = - myTaskDao.read(params.getTaskId().asIdDt(), requestDetails); + Task task = myTaskDao.read(params.getTaskId().asIdDt(), requestDetails); task.setStatus(Task.TaskStatus.COMPLETED); // TODO KHS this Task will probably be too large for large jobs. Revisit this model once we support Provenance From 3b754ff3c50d76020449034e0664a1e10cd5676e Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 20 Dec 2024 11:09:32 -0500 Subject: [PATCH 112/148] rename param constants --- .../BaseJpaResourceProviderPatient.java | 16 +++++++-------- .../provider/ReplaceReferencesSvcImpl.java | 6 ++---- .../PatientMergeOperationInputParameters.java | 20 +++++++++---------- .../jpa/provider/r4/PatientMergeR4Test.java | 6 +++--- .../provider/r4/ReplaceReferencesR4Test.java | 5 ++--- .../rest/api/server/SystemRequestDetails.java | 1 + .../server/provider/ProviderConstants.java | 18 ++++++++--------- 7 files changed, 35 insertions(+), 37 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 584604c46893..4ca752d2f641 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -274,21 +274,21 @@ public IBaseParameters patientMerge( HttpServletRequest theServletRequest, HttpServletResponse theServletResponse, ServletRequestDetails theRequestDetails, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER) + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT_IDENTIFIER) List theSourcePatientIdentifier, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER) + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT_IDENTIFIER) List theTargetPatientIdentifier, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT, max = 1) + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT, max = 1) IBaseReference theSourcePatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_TARGET_PATIENT, max = 1) + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT, max = 1) IBaseReference theTargetPatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PREVIEW, typeName = "boolean", max = 1) + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_PREVIEW, typeName = "boolean", max = 1) IPrimitiveType thePreview, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_DELETE_SOURCE, typeName = "boolean", max = 1) + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_DELETE_SOURCE, typeName = "boolean", max = 1) IPrimitiveType theDeleteSource, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_RESULT_PATIENT, max = 1) + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_RESULT_PATIENT, max = 1) IBaseResource theResultPatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_BATCH_SIZE, typeName = "unsignedInt") + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_BATCH_SIZE, typeName = "unsignedInt") IPrimitiveType theBatchSize) { startRequest(theServletRequest); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 6495e2c31b43..5708ea43526e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -88,10 +88,8 @@ public IBaseParameters replaceReferences( @Override public Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails) { - return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { - return myResourceLinkDao.countResourcesTargetingFhirTypeAndFhirId( - theResourceId.getResourceType(), theResourceId.getIdPart()); - }); + return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> myResourceLinkDao.countResourcesTargetingFhirTypeAndFhirId( + theResourceId.getResourceType(), theResourceId.getIdPart())); } private IBaseParameters replaceReferencesPreferAsync( diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java index 9144503b975e..7362f0367b18 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java @@ -19,11 +19,11 @@ */ package ca.uhn.fhir.jpa.provider.merge; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_RESULT_PATIENT; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_PARAM_RESULT_PATIENT; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT_IDENTIFIER; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT_IDENTIFIER; /** * See Patient $merge spec @@ -35,26 +35,26 @@ public PatientMergeOperationInputParameters(int theBatchSize) { @Override public String getSourceResourceParameterName() { - return OPERATION_MERGE_SOURCE_PATIENT; + return OPERATION_MERGE_PARAM_SOURCE_PATIENT; } @Override public String getTargetResourceParameterName() { - return OPERATION_MERGE_TARGET_PATIENT; + return OPERATION_MERGE_PARAM_TARGET_PATIENT; } @Override public String getSourceIdentifiersParameterName() { - return OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER; + return OPERATION_MERGE_PARAM_SOURCE_PATIENT_IDENTIFIER; } @Override public String getTargetIdentifiersParameterName() { - return OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER; + return OPERATION_MERGE_PARAM_TARGET_PATIENT_IDENTIFIER; } @Override public String getResultResourceParameterName() { - return OPERATION_MERGE_RESULT_PATIENT; + return OPERATION_MERGE_PARAM_RESULT_PATIENT; } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index de86447ae418..ed988766bbb6 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -42,7 +42,7 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_RESULT_PATIENT; +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_PARAM_RESULT_PATIENT; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; @@ -127,8 +127,8 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea // Assert input Parameters input = (Parameters) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_INPUT).getResource(); if (withInputResultPatient) { // if the following assert fails, check that these two patients are identical - Patient p1 = (Patient) inParameters.getParameter(OPERATION_MERGE_RESULT_PATIENT).getResource(); - Patient p2 = (Patient) input.getParameter(OPERATION_MERGE_RESULT_PATIENT).getResource(); + Patient p1 = (Patient) inParameters.getParameter(OPERATION_MERGE_PARAM_RESULT_PATIENT).getResource(); + Patient p2 = (Patient) input.getParameter(OPERATION_MERGE_PARAM_RESULT_PATIENT).getResource(); ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(p1)); ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(p2)); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index d5d8171dfb23..710cd61177d9 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -14,7 +14,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import java.io.IOException; import java.util.List; import static ca.uhn.fhir.jpa.provider.ReplaceReferencesSvcImpl.RESOURCE_TYPES_SYSTEM; @@ -40,7 +39,7 @@ public void before() throws Exception { @ParameterizedTest @ValueSource(booleans = {false, true}) - void testReplaceReferences(boolean isAsync) throws IOException { + void testReplaceReferences(boolean isAsync) { // exec Parameters outParams = myTestHelper.callReplaceReferences(myClient, isAsync); @@ -78,7 +77,7 @@ private JobInstance awaitJobCompletion(Task task) { @ParameterizedTest @ValueSource(booleans = {false, true}) - void testReplaceReferencesSmallBatchSize(boolean isAsync) throws IOException { + void testReplaceReferencesSmallBatchSize(boolean isAsync) { // exec Parameters outParams = myTestHelper.callReplaceReferencesWithBatchSize(myClient, isAsync, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SystemRequestDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SystemRequestDetails.java index 12b2dd27448e..cb2d3f010e55 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SystemRequestDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/SystemRequestDetails.java @@ -78,6 +78,7 @@ public SystemRequestDetails(RequestDetails theDetails) { } } + // TODO KHS use this everywhere we create a srd with only one partition public static SystemRequestDetails forRequestPartitionId(RequestPartitionId thePartitionId) { SystemRequestDetails retVal = new SystemRequestDetails(); retVal.setRequestPartitionId(thePartitionId); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index 77306dab8970..c51059ebc50e 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -281,15 +281,15 @@ public class ProviderConstants { /** * Patient $merge operation parameters */ - public static final String OPERATION_MERGE_SOURCE_PATIENT = "source-patient"; - - public static final String OPERATION_MERGE_SOURCE_PATIENT_IDENTIFIER = "source-patient-identifier"; - public static final String OPERATION_MERGE_TARGET_PATIENT = "target-patient"; - public static final String OPERATION_MERGE_TARGET_PATIENT_IDENTIFIER = "target-patient-identifier"; - public static final String OPERATION_MERGE_RESULT_PATIENT = "result-patient"; - public static final String OPERATION_MERGE_BATCH_SIZE = "batch-size"; - public static final String OPERATION_MERGE_PREVIEW = "preview"; - public static final String OPERATION_MERGE_DELETE_SOURCE = "delete-source"; + public static final String OPERATION_MERGE_PARAM_SOURCE_PATIENT = "source-patient"; + + public static final String OPERATION_MERGE_PARAM_SOURCE_PATIENT_IDENTIFIER = "source-patient-identifier"; + public static final String OPERATION_MERGE_PARAM_TARGET_PATIENT = "target-patient"; + public static final String OPERATION_MERGE_PARAM_TARGET_PATIENT_IDENTIFIER = "target-patient-identifier"; + public static final String OPERATION_MERGE_PARAM_RESULT_PATIENT = "result-patient"; + public static final String OPERATION_MERGE_PARAM_BATCH_SIZE = "batch-size"; + public static final String OPERATION_MERGE_PARAM_PREVIEW = "preview"; + public static final String OPERATION_MERGE_PARAM_DELETE_SOURCE = "delete-source"; public static final String OPERATION_MERGE_OUTPUT_PARAM_INPUT = "input"; public static final String OPERATION_MERGE_OUTPUT_PARAM_OUTCOME = OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; public static final String OPERATION_MERGE_OUTPUT_PARAM_RESULT = "result"; From 38b76d2335667e0e7684346547a64a73feac9f95 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 20 Dec 2024 11:09:42 -0500 Subject: [PATCH 113/148] rename param constants --- .../ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 5708ea43526e..3d63504853f8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -88,8 +88,10 @@ public IBaseParameters replaceReferences( @Override public Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails) { - return myHapiTransactionService.withRequest(theRequestDetails).execute(() -> myResourceLinkDao.countResourcesTargetingFhirTypeAndFhirId( - theResourceId.getResourceType(), theResourceId.getIdPart())); + return myHapiTransactionService + .withRequest(theRequestDetails) + .execute(() -> myResourceLinkDao.countResourcesTargetingFhirTypeAndFhirId( + theResourceId.getResourceType(), theResourceId.getIdPart())); } private IBaseParameters replaceReferencesPreferAsync( From af57f583dd123056de91ac6cc79723bb23669588 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 20 Dec 2024 11:35:40 -0500 Subject: [PATCH 114/148] remove IdentifierUtil for visibility --- .../BaseJpaResourceProviderPatient.java | 5 +- .../ca/uhn/fhir/mdm/util/IdentifierUtil.java | 2 +- .../uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java | 5 ++ .../ReplaceReferencesPatchBundleSvc.java | 10 +++- .../ReplaceReferencesRequest.java | 4 +- .../ca/uhn/fhir/util/CanonicalIdentifier.java | 22 +++++++++ .../java/ca/uhn/fhir/util/IdentifierUtil.java | 49 ------------------- 7 files changed, 41 insertions(+), 56 deletions(-) delete mode 100644 hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/IdentifierUtil.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 4ca752d2f641..768ebe4bacb5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -47,7 +47,6 @@ import ca.uhn.fhir.rest.server.provider.ProviderConstants; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.CanonicalIdentifier; -import ca.uhn.fhir.util.IdentifierUtil; import ca.uhn.fhir.util.ParametersUtil; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -359,13 +358,13 @@ private MergeOperationInputParameters buildMergeOperationInputParameters( MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(theBatchSize); if (theSourcePatientIdentifier != null) { List sourceResourceIdentifiers = theSourcePatientIdentifier.stream() - .map(IdentifierUtil::identifierDtFromIdentifier) + .map(CanonicalIdentifier::fromIdentifier) .collect(Collectors.toList()); mergeOperationParameters.setSourceResourceIdentifiers(sourceResourceIdentifiers); } if (theTargetPatientIdentifier != null) { List targetResourceIdentifiers = theTargetPatientIdentifier.stream() - .map(IdentifierUtil::identifierDtFromIdentifier) + .map(CanonicalIdentifier::fromIdentifier) .collect(Collectors.toList()); mergeOperationParameters.setTargetResourceIdentifiers(targetResourceIdentifiers); } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/IdentifierUtil.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/IdentifierUtil.java index 8c2aea9a5cb2..dc3748815e63 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/IdentifierUtil.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/IdentifierUtil.java @@ -30,7 +30,7 @@ public final class IdentifierUtil { private IdentifierUtil() {} public static CanonicalIdentifier identifierDtFromIdentifier(IBase theIdentifier) { - return ca.uhn.fhir.util.IdentifierUtil.identifierDtFromIdentifier(theIdentifier); + return ca.uhn.fhir.util.CanonicalIdentifier.fromIdentifier(theIdentifier); } /** diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java index f1b425101575..64664af3c109 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java @@ -80,6 +80,11 @@ default IResourcePidStream fetchResourceIdStream( theStart, theEnd, 20000 /* ResourceIdListStep.DEFAULT_PAGE_SIZE */, theTargetPartitionId, theUrl)); } + /** + * Stream Resource Ids of all resources that have a reference to the provided resource id + * + * @param theTargetId the id of the resource we are searching for references to + */ default Stream streamSourceIdsThatReferenceTargetId(IIdType theTargetId) { throw new UnsupportedOperationException(); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java index 4eb9391e238d..fc2c8ba55929 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java @@ -57,6 +57,14 @@ public ReplaceReferencesPatchBundleSvc(DaoRegistry theDaoRegistry) { myFhirContext = theDaoRegistry.getFhirContext(); } + /** + * Build a bundle of PATCH entries that make the requested reference updates + * @param theReplaceReferencesRequest source and target for reference switch + * @param theResourceIds the ids of the resource to create the patch entries for (they will all have references to the source resource) + * @param theRequestDetails + * @return + */ + public Bundle patchReferencingResources( ReplaceReferencesRequest theReplaceReferencesRequest, List theResourceIds, @@ -64,7 +72,7 @@ public Bundle patchReferencingResources( Bundle patchBundle = buildPatchBundle(theReplaceReferencesRequest, theResourceIds, theRequestDetails); IFhirSystemDao systemDao = myDaoRegistry.getSystemDao(); Bundle result = systemDao.transaction(theRequestDetails, patchBundle); - // TODO KHS shouldn't transaction response bundles already have ids? + result.setId(UUID.randomUUID().toString()); return result; } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java index f3744b0f1737..d21ea598ef52 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java @@ -76,7 +76,7 @@ public boolean isForceSync() { return myForceSync; } - public void setForceSync(boolean forceSync) { - this.myForceSync = forceSync; + public void setForceSync(boolean theForceSync) { + this.myForceSync = theForceSync; } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/CanonicalIdentifier.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/CanonicalIdentifier.java index b48e6567fdc5..aeb657682e4a 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/CanonicalIdentifier.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/CanonicalIdentifier.java @@ -24,8 +24,10 @@ import ca.uhn.fhir.model.base.composite.BaseIdentifierDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.model.primitive.UriDt; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.hl7.fhir.instance.model.api.IBase; import java.util.List; @@ -92,4 +94,24 @@ public boolean equals(Object theO) { public int hashCode() { return new HashCodeBuilder(17, 37).append(mySystem).append(myValue).toHashCode(); } + + public static CanonicalIdentifier fromIdentifier(IBase theIdentifier) { + CanonicalIdentifier retval = new CanonicalIdentifier(); + + // TODO add other fields like "use" etc + if (theIdentifier instanceof org.hl7.fhir.dstu3.model.Identifier) { + org.hl7.fhir.dstu3.model.Identifier ident = (org.hl7.fhir.dstu3.model.Identifier) theIdentifier; + retval.setSystem(ident.getSystem()).setValue(ident.getValue()); + } else if (theIdentifier instanceof org.hl7.fhir.r4.model.Identifier) { + org.hl7.fhir.r4.model.Identifier ident = (org.hl7.fhir.r4.model.Identifier) theIdentifier; + retval.setSystem(ident.getSystem()).setValue(ident.getValue()); + } else if (theIdentifier instanceof org.hl7.fhir.r5.model.Identifier) { + org.hl7.fhir.r5.model.Identifier ident = (org.hl7.fhir.r5.model.Identifier) theIdentifier; + retval.setSystem(ident.getSystem()).setValue(ident.getValue()); + } else { + throw new InternalErrorException(Msg.code(1486) + "Expected 'Identifier' type but was '" + + theIdentifier.getClass().getName() + "'"); + } + return retval; + } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/IdentifierUtil.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/IdentifierUtil.java deleted file mode 100644 index 97a707773407..000000000000 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/IdentifierUtil.java +++ /dev/null @@ -1,49 +0,0 @@ -/*- - * #%L - * HAPI FHIR Storage api - * %% - * Copyright (C) 2014 - 2024 Smile CDR, Inc. - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ -package ca.uhn.fhir.util; - -import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import org.hl7.fhir.instance.model.api.IBase; - -public final class IdentifierUtil { - - private IdentifierUtil() {} - - public static CanonicalIdentifier identifierDtFromIdentifier(IBase theIdentifier) { - CanonicalIdentifier retval = new CanonicalIdentifier(); - - // TODO add other fields like "use" etc - if (theIdentifier instanceof org.hl7.fhir.dstu3.model.Identifier) { - org.hl7.fhir.dstu3.model.Identifier ident = (org.hl7.fhir.dstu3.model.Identifier) theIdentifier; - retval.setSystem(ident.getSystem()).setValue(ident.getValue()); - } else if (theIdentifier instanceof org.hl7.fhir.r4.model.Identifier) { - org.hl7.fhir.r4.model.Identifier ident = (org.hl7.fhir.r4.model.Identifier) theIdentifier; - retval.setSystem(ident.getSystem()).setValue(ident.getValue()); - } else if (theIdentifier instanceof org.hl7.fhir.r5.model.Identifier) { - org.hl7.fhir.r5.model.Identifier ident = (org.hl7.fhir.r5.model.Identifier) theIdentifier; - retval.setSystem(ident.getSystem()).setValue(ident.getValue()); - } else { - throw new InternalErrorException(Msg.code(1486) + "Expected 'Identifier' type but was '" - + theIdentifier.getClass().getName() + "'"); - } - return retval; - } -} From 61576bef774234dccb498abe5716cd65ad167151 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 20 Dec 2024 11:35:50 -0500 Subject: [PATCH 115/148] remove IdentifierUtil for visibility --- .../fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java | 1 - .../src/main/java/ca/uhn/fhir/util/CanonicalIdentifier.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java index fc2c8ba55929..e01057ced108 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesPatchBundleSvc.java @@ -64,7 +64,6 @@ public ReplaceReferencesPatchBundleSvc(DaoRegistry theDaoRegistry) { * @param theRequestDetails * @return */ - public Bundle patchReferencingResources( ReplaceReferencesRequest theReplaceReferencesRequest, List theResourceIds, diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/CanonicalIdentifier.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/CanonicalIdentifier.java index aeb657682e4a..19fd96c434d4 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/CanonicalIdentifier.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/util/CanonicalIdentifier.java @@ -110,7 +110,7 @@ public static CanonicalIdentifier fromIdentifier(IBase theIdentifier) { retval.setSystem(ident.getSystem()).setValue(ident.getValue()); } else { throw new InternalErrorException(Msg.code(1486) + "Expected 'Identifier' type but was '" - + theIdentifier.getClass().getName() + "'"); + + theIdentifier.getClass().getName() + "'"); } return retval; } From ded58e7b1765841d45fd067a8491db9db904e496 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 20 Dec 2024 11:45:53 -0500 Subject: [PATCH 116/148] final review of hapi side --- .../ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java | 2 +- .../ReplaceReferenceUpdateTaskReducerStep.java | 10 +++++----- .../ca/uhn/fhir/batch2/api/IReductionStepWorker.java | 5 ++++- .../java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdJson.java | 1 + 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java index 9bb003174c7f..fe24006edd9d 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeAppCtx.java @@ -92,7 +92,7 @@ public MergeUpdateTaskReducerStep mergeUpdateTaskStep( } @Bean - public ReplaceReferencesErrorHandler mergeErorHandler( + public ReplaceReferencesErrorHandler mergeErrorHandler( DaoRegistry theDaoRegistry, Batch2TaskHelper theBatch2TaskHelper) { IFhirResourceDao taskDao = theDaoRegistry.getResourceDao(Task.class); return new ReplaceReferencesErrorHandler<>(theBatch2TaskHelper, taskDao); diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java index f1d45bdaa02f..1cb22c1c0d35 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java @@ -28,7 +28,6 @@ import ca.uhn.fhir.batch2.model.ChunkOutcome; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import jakarta.annotation.Nonnull; import org.hl7.fhir.r4.model.Bundle; @@ -44,13 +43,13 @@ public class ReplaceReferenceUpdateTaskReducerStep myPatchOutputBundles = new ArrayList<>(); - private IFhirResourceDao myTaskDao; public ReplaceReferenceUpdateTaskReducerStep(DaoRegistry theDaoRegistry) { + myDaoRegistry = theDaoRegistry; myFhirContext = theDaoRegistry.getFhirContext(); - myTaskDao = theDaoRegistry.getResourceDao(Task.class); } @Nonnull @@ -72,7 +71,8 @@ public RunOutcome run( ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); SystemRequestDetails requestDetails = SystemRequestDetails.forRequestPartitionId(params.getPartitionId()); - Task task = myTaskDao.read(params.getTaskId().asIdDt(), requestDetails); + Task task = + myDaoRegistry.getResourceDao(Task.class).read(params.getTaskId().asIdDt(), requestDetails); task.setStatus(Task.TaskStatus.COMPLETED); // TODO KHS this Task will probably be too large for large jobs. Revisit this model once we support Provenance @@ -88,7 +88,7 @@ public RunOutcome run( task.addContained(outputBundle); }); - myTaskDao.update(task, requestDetails); + myDaoRegistry.getResourceDao(Task.class).update(task, requestDetails); ReplaceReferenceResultsJson result = new ReplaceReferenceResultsJson(); result.setTaskId(params.getTaskId()); diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/api/IReductionStepWorker.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/api/IReductionStepWorker.java index 6c5a7d14f553..4beada5a8cdb 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/api/IReductionStepWorker.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/api/IReductionStepWorker.java @@ -25,7 +25,7 @@ /** * Reduction step worker. Once all chunks from the previous step have completed, consume() will first be called on - * all chunks, and then funally run() will be called on this step. + * all chunks, and then finally run() will be called on this step. * @param Job Parameter Type * @param Input Parameter type (real input for step is ListResult of IT * @param Output Job Report Type @@ -33,6 +33,9 @@ public interface IReductionStepWorker extends IJobStepWorker { + // TODO KHS create an abstract superclass under this that enforces the one-at-a-time contract + // (this contract is currently baked into the implementations inconsistently) + /** * * If an exception is thrown, the workchunk will be marked as failed. diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdJson.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdJson.java index e608dc0ccefe..f379dbe08f48 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdJson.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/jobs/chunk/FhirIdJson.java @@ -34,6 +34,7 @@ public class FhirIdJson implements IModelJson { @JsonProperty("id") private String myFhirId; + // Jackson needs an empty constructor public FhirIdJson() {} public FhirIdJson(String theResourceType, String theFhirId) { From 40b8d75795460efd8ba89b16283f144c2eb3486c Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 20 Dec 2024 12:04:46 -0500 Subject: [PATCH 117/148] review feedback --- ...ReplaceReferenceUpdateTaskReducerStep.java | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java index 1cb22c1c0d35..04208a5483d4 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java @@ -28,6 +28,7 @@ import ca.uhn.fhir.batch2.model.ChunkOutcome; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import jakarta.annotation.Nonnull; import org.hl7.fhir.r4.model.Bundle; @@ -44,11 +45,13 @@ public class ReplaceReferenceUpdateTaskReducerStep myTaskDao; private List myPatchOutputBundles = new ArrayList<>(); public ReplaceReferenceUpdateTaskReducerStep(DaoRegistry theDaoRegistry) { myDaoRegistry = theDaoRegistry; + myTaskDao = myDaoRegistry.getResourceDao(Task.class); myFhirContext = theDaoRegistry.getFhirContext(); } @@ -69,35 +72,39 @@ public RunOutcome run( @Nonnull IJobDataSink theDataSink) throws JobExecutionFailedException { - ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); - SystemRequestDetails requestDetails = SystemRequestDetails.forRequestPartitionId(params.getPartitionId()); - Task task = - myDaoRegistry.getResourceDao(Task.class).read(params.getTaskId().asIdDt(), requestDetails); + try { + ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); + SystemRequestDetails requestDetails = SystemRequestDetails.forRequestPartitionId(params.getPartitionId()); + Task task = + myTaskDao.read(params.getTaskId().asIdDt(), requestDetails); - task.setStatus(Task.TaskStatus.COMPLETED); - // TODO KHS this Task will probably be too large for large jobs. Revisit this model once we support Provenance - // resources. - myPatchOutputBundles.forEach(outputBundle -> { - Task.TaskOutputComponent output = task.addOutput(); - Coding coding = output.getType().getCodingFirstRep(); - coding.setSystem(RESOURCE_TYPES_SYSTEM); - coding.setCode("Bundle"); - Reference outputBundleReference = + task.setStatus(Task.TaskStatus.COMPLETED); + // TODO KHS this Task will probably be too large for large jobs. Revisit this model once we support Provenance + // resources. + myPatchOutputBundles.forEach(outputBundle -> { + Task.TaskOutputComponent output = task.addOutput(); + Coding coding = output.getType().getCodingFirstRep(); + coding.setSystem(RESOURCE_TYPES_SYSTEM); + coding.setCode("Bundle"); + Reference outputBundleReference = new Reference("#" + outputBundle.getIdElement().getIdPart()); - output.setValue(outputBundleReference); - task.addContained(outputBundle); - }); + output.setValue(outputBundleReference); + task.addContained(outputBundle); + }); - myDaoRegistry.getResourceDao(Task.class).update(task, requestDetails); + myTaskDao.update(task, requestDetails); - ReplaceReferenceResultsJson result = new ReplaceReferenceResultsJson(); - result.setTaskId(params.getTaskId()); - theDataSink.accept(result); + ReplaceReferenceResultsJson result = new ReplaceReferenceResultsJson(); + result.setTaskId(params.getTaskId()); + theDataSink.accept(result); - // Reusing the same reducer for all jobs feels confusing and dangerous to me. We need to fix this. - // See https://github.com/hapifhir/hapi-fhir/pull/6551 - myPatchOutputBundles.clear(); - - return new RunOutcome(myPatchOutputBundles.size()); + return new RunOutcome(myPatchOutputBundles.size()); + } finally { + // Reusing the same reducer for all jobs feels confusing and dangerous to me. We need to fix this. + // See https://github.com/hapifhir/hapi-fhir/pull/6551 + // TODO KHS add new methods to the api called init() and cleanup() that are called by the api so we can move + // this finally block out + myPatchOutputBundles.clear(); + } } } From f58188bea5b22042182dc4b99d92af2335b28ccd Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Fri, 20 Dec 2024 12:07:26 -0500 Subject: [PATCH 118/148] review feedback --- .../ReplaceReferenceUpdateTaskReducerStep.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java index 04208a5483d4..11497d7af013 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferenceUpdateTaskReducerStep.java @@ -75,11 +75,11 @@ public RunOutcome run( try { ReplaceReferencesJobParameters params = theStepExecutionDetails.getParameters(); SystemRequestDetails requestDetails = SystemRequestDetails.forRequestPartitionId(params.getPartitionId()); - Task task = - myTaskDao.read(params.getTaskId().asIdDt(), requestDetails); + Task task = myTaskDao.read(params.getTaskId().asIdDt(), requestDetails); task.setStatus(Task.TaskStatus.COMPLETED); - // TODO KHS this Task will probably be too large for large jobs. Revisit this model once we support Provenance + // TODO KHS this Task will probably be too large for large jobs. Revisit this model once we support + // Provenance // resources. myPatchOutputBundles.forEach(outputBundle -> { Task.TaskOutputComponent output = task.addOutput(); @@ -87,7 +87,7 @@ public RunOutcome run( coding.setSystem(RESOURCE_TYPES_SYSTEM); coding.setCode("Bundle"); Reference outputBundleReference = - new Reference("#" + outputBundle.getIdElement().getIdPart()); + new Reference("#" + outputBundle.getIdElement().getIdPart()); output.setValue(outputBundleReference); task.addContained(outputBundle); }); From 71f87f77122dbe921c3600d6fd2fa53d107ff3fe Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Fri, 20 Dec 2024 11:48:53 -0500 Subject: [PATCH 119/148] fix checkstyle errors, rename MergeOperationInputParameters and add Msg.code --- .../BaseJpaResourceProviderPatient.java | 9 ++- ...=> BaseMergeOperationInputParameters.java} | 4 +- .../merge/MergeValidationService.java | 10 +-- .../PatientMergeOperationInputParameters.java | 2 +- .../provider/merge/ResourceMergeService.java | 12 +-- .../merge/ResourceMergeServiceTest.java | 74 +++++++++---------- .../jpa/provider/r4/PatientMergeR4Test.java | 20 ++--- .../fhir/batch2/util/Batch2TaskHelper.java | 7 +- .../uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java | 2 +- 9 files changed, 73 insertions(+), 67 deletions(-) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/{MergeOperationInputParameters.java => BaseMergeOperationInputParameters.java} (96%) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index 768ebe4bacb5..d140b17d6242 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -23,7 +23,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.dao.PatientEverythingParameters; import ca.uhn.fhir.jpa.model.util.JpaConstants; -import ca.uhn.fhir.jpa.provider.merge.MergeOperationInputParameters; +import ca.uhn.fhir.jpa.provider.merge.BaseMergeOperationInputParameters; import ca.uhn.fhir.jpa.provider.merge.MergeOperationOutcome; import ca.uhn.fhir.jpa.provider.merge.PatientMergeOperationInputParameters; import ca.uhn.fhir.jpa.provider.merge.ResourceMergeService; @@ -295,7 +295,7 @@ public IBaseParameters patientMerge( try { int batchSize = myStorageSettings.getTransactionWriteBatchSizeFromOperationParameter(theBatchSize); - MergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( + BaseMergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( theSourcePatientIdentifier, theTargetPatientIdentifier, theSourcePatient, @@ -346,7 +346,7 @@ private IBaseParameters buildMergeOperationOutputParameters( return retVal; } - private MergeOperationInputParameters buildMergeOperationInputParameters( + private BaseMergeOperationInputParameters buildMergeOperationInputParameters( List theSourcePatientIdentifier, List theTargetPatientIdentifier, IBaseReference theSourcePatient, @@ -355,7 +355,8 @@ private MergeOperationInputParameters buildMergeOperationInputParameters( IPrimitiveType theDeleteSource, IBaseResource theResultPatient, int theBatchSize) { - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(theBatchSize); + BaseMergeOperationInputParameters mergeOperationParameters = + new PatientMergeOperationInputParameters(theBatchSize); if (theSourcePatientIdentifier != null) { List sourceResourceIdentifiers = theSourcePatientIdentifier.stream() .map(CanonicalIdentifier::fromIdentifier) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParameters.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java similarity index 96% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParameters.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java index 85c5e5fd0016..e35ddf850fe8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParameters.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java @@ -28,7 +28,7 @@ /** * See Patient $merge spec */ -public abstract class MergeOperationInputParameters { +public abstract class BaseMergeOperationInputParameters { private List mySourceResourceIdentifiers; private List myTargetResourceIdentifiers; @@ -39,7 +39,7 @@ public abstract class MergeOperationInputParameters { private IBaseResource myResultResource; private final int myBatchSize; - protected MergeOperationInputParameters(int theBatchSize) { + protected BaseMergeOperationInputParameters(int theBatchSize) { myBatchSize = theBatchSize; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java index 9c58152d0aa3..b831b6a3be99 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java @@ -37,7 +37,7 @@ public MergeValidationService(FhirContext theFhirContext, DaoRegistry theDaoRegi } MergeValidationResult validate( - MergeOperationInputParameters theMergeOperationParameters, + BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails, MergeOperationOutcome theMergeOutcome) { @@ -80,7 +80,7 @@ MergeValidationResult validate( } private boolean validateResultResourceIfExists( - MergeOperationInputParameters theMergeOperationParameters, + BaseMergeOperationInputParameters theMergeOperationParameters, Patient theResolvedTargetResource, Patient theResolvedSourceResource, IBaseOperationOutcome theOperationOutcome) { @@ -262,7 +262,7 @@ private boolean validateSourceAndTargetAreSuitableForMerge( * @return true if the parameters are valid, false otherwise */ private boolean validateMergeOperationParameters( - MergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { + BaseMergeOperationInputParameters theMergeOperationParameters, IBaseOperationOutcome theOutcome) { List errorMessages = new ArrayList<>(); if (!theMergeOperationParameters.hasAtLeastOneSourceIdentifier() && theMergeOperationParameters.getSourceResource() == null) { @@ -331,7 +331,7 @@ private boolean validateMergeOperationParameters( } private IBaseResource resolveSourceResource( - MergeOperationInputParameters theOperationParameters, + BaseMergeOperationInputParameters theOperationParameters, RequestDetails theRequestDetails, IBaseOperationOutcome theOutcome) { return resolveResource( @@ -344,7 +344,7 @@ private IBaseResource resolveSourceResource( } private IBaseResource resolveTargetResource( - MergeOperationInputParameters theOperationParameters, + BaseMergeOperationInputParameters theOperationParameters, RequestDetails theRequestDetails, IBaseOperationOutcome theOutcome) { return resolveResource( diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java index 7362f0367b18..dfd802433b8b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java @@ -28,7 +28,7 @@ /** * See Patient $merge spec */ -public class PatientMergeOperationInputParameters extends MergeOperationInputParameters { +public class PatientMergeOperationInputParameters extends BaseMergeOperationInputParameters { public PatientMergeOperationInputParameters(int theBatchSize) { super(theBatchSize); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 0d2a3729665d..812db3f18757 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -95,7 +95,7 @@ public ResourceMergeService( * @return the merge outcome containing OperationOutcome and HTTP status code */ public MergeOperationOutcome merge( - MergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); @@ -118,7 +118,7 @@ public MergeOperationOutcome merge( } private void validateAndMerge( - MergeOperationInputParameters theMergeOperationParameters, + BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails, MergeOperationOutcome theMergeOutcome) { @@ -150,7 +150,7 @@ private void validateAndMerge( private void handlePreview( Patient theSourceResource, Patient theTargetResource, - MergeOperationInputParameters theMergeOperationParameters, + BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails, MergeOperationOutcome theMergeOutcome) { @@ -170,7 +170,7 @@ private void handlePreview( } private void doMerge( - MergeOperationInputParameters theMergeOperationParameters, + BaseMergeOperationInputParameters theMergeOperationParameters, Patient theSourceResource, Patient theTargetResource, RequestDetails theRequestDetails, @@ -217,7 +217,7 @@ private void doMerge( } private void doMergeSync( - MergeOperationInputParameters theMergeOperationParameters, + BaseMergeOperationInputParameters theMergeOperationParameters, Patient theSourceResource, Patient theTargetResource, RequestDetails theRequestDetails, @@ -249,7 +249,7 @@ private void doMergeSync( } private void doMergeAsync( - MergeOperationInputParameters theMergeOperationParameters, + BaseMergeOperationInputParameters theMergeOperationParameters, Patient theSourceResource, Patient theTargetResource, RequestDetails theRequestDetails, diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java index 70c86cbfa9d0..44a10391d166 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java @@ -133,7 +133,7 @@ void setup() { @Test void testMerge_WithoutResultResource_Success() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); @@ -173,7 +173,7 @@ void testMerge_WithoutResultResource_Success() { @Test void testMerge_WithoutResultResource_TargetSetToActiveExplicitly_Success() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1); @@ -200,7 +200,7 @@ void testMerge_WithoutResultResource_TargetSetToActiveExplicitly_Success() { @Test void testMerge_WithResultResource_Success() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); Patient resultPatient = createPatient(TARGET_PATIENT_TEST_ID); @@ -235,7 +235,7 @@ void testMerge_WithResultResource_Success() { @Test void testMerge_WithResultResource_ResultHasAllTargetIdentifiers_Success() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of( new CanonicalIdentifier().setSystem("sys").setValue("val1"), @@ -276,7 +276,7 @@ void testMerge_WithResultResource_ResultHasAllTargetIdentifiers_Success() { @Test void testMerge_WithDeleteSourceTrue_Success() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setDeleteSource(true); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -305,7 +305,7 @@ void testMerge_WithDeleteSourceTrue_Success() { @Test void testMerge_WithDeleteSourceTrue_And_WithResultResource_Success() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setDeleteSource(true); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -335,7 +335,7 @@ void testMerge_WithDeleteSourceTrue_And_WithResultResource_Success() { @Test void testMerge_WithPreviewTrue_Success() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(true); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -366,7 +366,7 @@ void testMerge_WithPreviewTrue_Success() { @Test void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersionAreTheSame_Success() { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID_WITH_VERSION_2)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID_WITH_VERSION_2)); Patient sourcePatient = createPatient(SOURCE_PATIENT_TEST_ID_WITH_VERSION_2); @@ -399,7 +399,7 @@ void testMerge_ResolvesResourcesByReferenceThatHasVersions_CurrentResourceVersio }) void testMerge_AsyncBecauseOfPreferHeader_Success(boolean theWithResultResource, boolean theWithDeleteSource) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); mergeOperationParameters.setDeleteSource(theWithDeleteSource); @@ -438,7 +438,7 @@ void testMerge_AsyncBecauseOfPreferHeader_Success(boolean theWithResultResource, void testMerge_AsyncBecauseOfLargeNumberOfRefs_Success(boolean theWithResultResource, boolean theWithDeleteSource) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); mergeOperationParameters.setDeleteSource(theWithDeleteSource); @@ -475,7 +475,7 @@ void testMerge_AsyncBecauseOfLargeNumberOfRefs_Success(boolean theWithResultReso @ValueSource(booleans = {true, false}) void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheException(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -502,7 +502,7 @@ void testMerge_UnhandledServerResponseExceptionThrown_UsesStatusCodeOfTheExcepti @ValueSource(booleans = {true, false}) void testMerge_UnhandledExceptionThrown_Uses500StatusCode(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -529,7 +529,7 @@ void testMerge_UnhandledExceptionThrown_Uses500StatusCode(boolean thePreview) { @ValueSource(booleans = {true, false}) void testMerge_ValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorWith400Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -554,7 +554,7 @@ void testMerge_ValidatesInputParameters_MissingSourcePatientParams_ReturnsErrorW @ValueSource(booleans = {true, false}) void testMerge_ValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorWith400Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); @@ -579,7 +579,7 @@ void testMerge_ValidatesInputParameters_MissingTargetPatientParams_ReturnsErrorW @ValueSource(booleans = {true, false}) void testMerge_ValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ReturnsErrorsWith400Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); // When @@ -606,7 +606,7 @@ void testMerge_ValidatesInputParameters_MissingBothSourceAndTargetPatientParams_ @ValueSource(booleans = {true, false}) void testMerge_ValidatesInputParameters_BothSourceResourceAndSourceIdentifierParamsProvided_ReturnsErrorWith400Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); @@ -634,7 +634,7 @@ void testMerge_ValidatesInputParameters_BothSourceResourceAndSourceIdentifierPar @ValueSource(booleans = {true, false}) void testMerge_ValidatesInputParameters_BothTargetResourceAndTargetIdentifiersParamsProvided_ReturnsErrorWith400Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue( "val"))); @@ -661,7 +661,7 @@ void testMerge_ValidatesInputParameters_BothTargetResourceAndTargetIdentifiersPa @ValueSource(booleans = {true, false}) void testMerge_ValidatesInputParameters_SourceResourceParamHasNoReferenceElement_ReturnsErrorWith400Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference()); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -688,7 +688,7 @@ void testMerge_ValidatesInputParameters_SourceResourceParamHasNoReferenceElement @ValueSource(booleans = {true, false}) void testMerge_ValidatesInputParameters_TargetResourceParamHasNoReferenceElement_ReturnsErrorWith400Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference()); @@ -715,7 +715,7 @@ void testMerge_ValidatesInputParameters_TargetResourceParamHasNoReferenceElement @ValueSource(booleans = {true, false}) void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWith422Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -741,7 +741,7 @@ void testMerge_ResolvesSourceResourceByReference_ResourceNotFound_ReturnsErrorWi @ValueSource(booleans = {true, false}) void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWith422Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -769,7 +769,7 @@ void testMerge_ResolvesTargetResourceByReference_ResourceNotFound_ReturnsErrorWi @ValueSource(booleans = {true, false}) void testMerge_ResolvesSourceResourceByIdentifiers_NoMatchFound_ReturnsErrorWith422Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResourceIdentifiers(List.of( new CanonicalIdentifier().setSystem("sys").setValue("val1"), @@ -800,7 +800,7 @@ void testMerge_ResolvesSourceResourceByIdentifiers_NoMatchFound_ReturnsErrorWith @ValueSource(booleans = {true, false}) void testMerge_ResolvesSourceResourceByIdentifiers_MultipleMatchesFound_ReturnsErrorWith422Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -834,7 +834,7 @@ void testMerge_ResolvesSourceResourceByIdentifiers_MultipleMatchesFound_ReturnsE @ValueSource(booleans = {true, false}) void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith422Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of( @@ -867,7 +867,7 @@ void testMerge_ResolvesTargetResourceByIdentifiers_NoMatchFound_ReturnsErrorWith @ValueSource(booleans = {true, false}) void testMerge_ResolvesTargetResourceByIdentifiers_MultipleMatchesFound_ReturnsErrorWith422Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); @@ -902,7 +902,7 @@ void testMerge_ResolvesTargetResourceByIdentifiers_MultipleMatchesFound_ReturnsE @ValueSource(booleans = {true, false}) void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID_WITH_VERSION_1)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -930,7 +930,7 @@ void testMerge_ResolvesSourceResourceByReferenceThatHasVersion_CurrentResourceVe @ValueSource(booleans = {true, false}) void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVersionIsDifferent_ReturnsErrorWith422Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID_WITH_VERSION_1)); @@ -961,7 +961,7 @@ void testMerge_ResolvesTargetResourceByReferenceThatHasVersion_CurrentResourceVe @ValueSource(booleans = {true, false}) void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val1"))); mergeOperationParameters.setTargetResourceIdentifiers(List.of(new CanonicalIdentifier().setSystem("sys").setValue("val2"))); @@ -991,7 +991,7 @@ void testMerge_SourceAndTargetResolvesToSameResource_ReturnsErrorWith422Status(b @ValueSource(booleans = {true, false}) void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -1020,7 +1020,7 @@ void testMerge_TargetResourceIsInactive_ReturnsErrorWith422Status(boolean thePre @ValueSource(booleans = {true, false}) void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsErrorWith422Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -1051,7 +1051,7 @@ void testMerge_TargetResourceWasPreviouslyReplacedByAnotherResource_ReturnsError @ValueSource(booleans = {true, false}) void testMerge_SourceResourceWasPreviouslyReplacedByAnotherResource_ReturnsErrorWith422Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -1081,7 +1081,7 @@ void testMerge_SourceResourceWasPreviouslyReplacedByAnotherResource_ReturnsError @ValueSource(booleans = {true, false}) void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetResource_ReturnsErrorWith400Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -1116,7 +1116,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetRes @ValueSource(booleans = {true, false}) void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersProvidedInTargetIdentifiers_ReturnsErrorWith400Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResourceIdentifiers(List.of( @@ -1155,7 +1155,7 @@ void testMerge_ValidatesResultResource_ResultResourceDoesNotHaveAllIdentifiersPr @ValueSource(booleans = {true, false}) void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLinkAtAll_ReturnsErrorWith400Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -1186,7 +1186,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLinkAtAll_Retu @ValueSource(booleans = {true, false}) void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLinkToSource_ReturnsErrorWith400Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -1219,7 +1219,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasNoReplacesLinkToSource_R @ValueSource(booleans = {true, false}) void testMerge_ValidatesResultResource_ResultResourceHasReplacesLinkAndDeleteSourceIsTrue_ReturnsErrorWith400Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); @@ -1252,7 +1252,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasReplacesLinkAndDeleteSou @ValueSource(booleans = {true, false}) void testMerge_ValidatesResultResource_ResultResourceHasRedundantReplacesLinksToSource_ReturnsErrorWith400Status(boolean thePreview) { // Given - MergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); + BaseMergeOperationInputParameters mergeOperationParameters = new PatientMergeOperationInputParameters(PAGE_SIZE); mergeOperationParameters.setPreview(thePreview); mergeOperationParameters.setSourceResource(new Reference(SOURCE_PATIENT_TEST_ID)); mergeOperationParameters.setTargetResource(new Reference(TARGET_PATIENT_TEST_ID)); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index ed988766bbb6..5d3217827a0e 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -255,7 +255,7 @@ void testMerge_SourceResourceCannotBeDeletedBecauseAnotherResourceReferencingSou Encounter enc = new Encounter(); enc.setStatus(Encounter.EncounterStatus.ARRIVED); enc.getSubject().setReferenceElement(myTestHelper.getSourcePatientId()); - myEncounterDao.create(enc, mySrd).getId().toUnqualifiedVersionless(); + myEncounterDao.create(enc, mySrd); myBatch2JobHelper.awaitJobFailure(jobId); @@ -305,6 +305,15 @@ public void testMultipleSourceMatchesFails(boolean withDelete, boolean withInput assertUnprocessibleEntityWithMessage(inParameters, "Multiple resources found matching the identifier(s) specified in 'source-patient-identifier'"); } + @Test + void test_MissingRequiredParameters_Returns400BadRequest() { + assertThatThrownBy(() -> callMergeOperation(new Parameters()) + ).isInstanceOf(InvalidRequestException.class) + .extracting(InvalidRequestException.class::cast) + .extracting(BaseServerResponseException::getStatusCode) + .isEqualTo(400); + } + private void assertUnprocessibleEntityWithMessage(Parameters inParameters, String theExpectedMessage) { assertThatThrownBy(() -> callMergeOperation(inParameters)) @@ -333,14 +342,7 @@ private Parameters callMergeOperation(Parameters inParameters, boolean isAsync) .execute(); } - @Test - void test_MissingRequiredParameters_Returns400BadRequest() { - assertThatThrownBy(() -> callMergeOperation(new Parameters()) - ).isInstanceOf(InvalidRequestException.class) - .extracting(InvalidRequestException.class::cast) - .extracting(BaseServerResponseException::getStatusCode) - .isEqualTo(400); - } + class MyExceptionHandler implements TestExecutionExceptionHandler { @Override diff --git a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java index f1073bf3b60c..4b3b61430f6c 100644 --- a/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java +++ b/hapi-fhir-storage-batch2/src/main/java/ca/uhn/fhir/batch2/util/Batch2TaskHelper.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.batch2.jobs.parameters.BatchJobParametersWithTaskId; import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; import ca.uhn.fhir.batch2.model.StatusEnum; +import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.batch.models.Batch2JobStartResponse; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -78,8 +79,10 @@ public void updateTaskStatusOnJobCompletion( taskStatus = Task.TaskStatus.CANCELLED; break; default: - throw new IllegalStateException(String.format( - "Cannot handle job status '%s'. COMPLETED, FAILED or CANCELLED were expected", jobStatus)); + throw new IllegalStateException(Msg.code(2595) + + String.format( + "Cannot handle job status '%s'. COMPLETED, FAILED or CANCELLED were expected", + jobStatus)); } Task task = theTaskDao.read(jobParams.getTaskId().asIdDt(), theRequestDetails); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java index 64664af3c109..31cb9a10aae3 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/svc/IBatch2DaoSvc.java @@ -86,6 +86,6 @@ default IResourcePidStream fetchResourceIdStream( * @param theTargetId the id of the resource we are searching for references to */ default Stream streamSourceIdsThatReferenceTargetId(IIdType theTargetId) { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(Msg.code(2594) + "Not implemented unless explicitly overridden"); } } From 10e23ec6442599a676f3ee653ddcac001162da1a Mon Sep 17 00:00:00 2001 From: Emre Dincturk Date: Fri, 20 Dec 2024 13:05:18 -0500 Subject: [PATCH 120/148] fix replace reference parameter type, and some copyright headers --- .../fhir/jpa/provider/JpaSystemProvider.java | 10 +++++----- .../provider/merge/MergeValidationResult.java | 19 +++++++++++++++++++ .../merge/MergeValidationService.java | 19 +++++++++++++++++++ .../validation/ValidationTestUtil.java | 19 +++++++++++++++++++ 4 files changed, 62 insertions(+), 5 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index f47a683c4f34..19a0399ff4a7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -167,23 +167,23 @@ public IBaseParameters replaceReferences( name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_SOURCE_REFERENCE_ID, min = 1, typeName = "string") - String theSourceId, + IPrimitiveType theSourceId, @OperationParam( name = ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID, min = 1, typeName = "string") - String theTargetId, + IPrimitiveType theTargetId, @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_BATCH_SIZE, typeName = "unsignedInt") IPrimitiveType theBatchSize, ServletRequestDetails theServletRequest) { startRequest(theServletRequest); try { - validateReplaceReferencesParams(theSourceId, theTargetId); + validateReplaceReferencesParams(theSourceId.getValue(), theTargetId.getValue()); int batchSize = myStorageSettings.getTransactionWriteBatchSizeFromOperationParameter(theBatchSize); - IdDt sourceId = new IdDt(theSourceId); - IdDt targetId = new IdDt(theTargetId); + IdDt sourceId = new IdDt(theSourceId.getValue()); + IdDt targetId = new IdDt(theTargetId.getValue()); RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( theServletRequest, ReadPartitionIdRequestDetails.forRead(targetId)); ReplaceReferencesRequest replaceReferencesRequest = diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java index 6e0278bbf59b..3d0ec68fe2b2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.jpa.provider.merge; import org.hl7.fhir.r4.model.Patient; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java index b831b6a3be99..d6ff6cc3c795 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.jpa.provider.merge; import ca.uhn.fhir.context.FhirContext; diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/ValidationTestUtil.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/ValidationTestUtil.java index 56a3235fea13..0ad2d570a360 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/ValidationTestUtil.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/validation/ValidationTestUtil.java @@ -1,3 +1,22 @@ +/*- + * #%L + * HAPI FHIR Test Utilities + * %% + * Copyright (C) 2014 - 2024 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ package ca.uhn.fhir.test.utilities.validation; import ca.uhn.fhir.rest.api.MethodOutcome; From a3a98ea1dbb212acf3a55026dc161603b4c10a0f Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 13:21:21 -0500 Subject: [PATCH 121/148] review feedback --- .../uhn/fhir/util/StopLimitAccumulator.java | 7 + .../uhn/hapi/fhir/docs/validation/examples.md | 2 +- .../provider/merge/MergeValidationResult.java | 18 +- .../merge/MergeValidationService.java | 21 +-- .../provider/merge/ResourceMergeService.java | 176 +++++++++--------- 5 files changed, 117 insertions(+), 107 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java index f003214d03ff..10bc69c86710 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java @@ -27,6 +27,13 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; +/** + * This class collects items from a stream to a given limit and know whether there are + * still more items beyond that limit. + * + * @param the type of object being streamed + */ + public class StopLimitAccumulator { private final boolean isTruncated; private final List myList; diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/examples.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/examples.md index d39fba7cb660..fe0d1dd89560 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/examples.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/validation/examples.md @@ -39,5 +39,5 @@ FhirValidator validator = myFhirCtx.newValidator(); validator.registerValidatorModule(instanceValidator); // Validate theResource -ValidationResult mergeValidationResult = validator.validateWithResult(theResource); +ValidationResult validationResult = validator.validateWithResult(theResource); ``` diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java index 3d0ec68fe2b2..8de70b857b8f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java @@ -22,21 +22,23 @@ import org.hl7.fhir.r4.model.Patient; class MergeValidationResult { - protected final Patient sourceResource; - protected final Patient targetResource; - protected final boolean isValid; + final Patient sourceResource; + final Patient targetResource; + final boolean isValid; + final Integer httpStatusCode; - private MergeValidationResult(Patient theSourceResource, Patient theTargetResource, boolean theIsValid) { + private MergeValidationResult(boolean theIsValid, Integer theHttpStatusCode, Patient theSourceResource, Patient theTargetResource) { + isValid = theIsValid; + httpStatusCode = theHttpStatusCode; sourceResource = theSourceResource; targetResource = theTargetResource; - isValid = theIsValid; } - public static MergeValidationResult invalidResult() { - return new MergeValidationResult(null, null, false); + public static MergeValidationResult invalidResult(int theHttpStatusCode) { + return new MergeValidationResult(false, theHttpStatusCode, null, null); } public static MergeValidationResult validResult(Patient theSourceResource, Patient theTargetResource) { - return new MergeValidationResult(theSourceResource, theTargetResource, true); + return new MergeValidationResult(true, null, theSourceResource, theTargetResource); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java index d6ff6cc3c795..c68464324af6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java @@ -46,7 +46,11 @@ import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_400_BAD_REQUEST; import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_422_UNPROCESSABLE_ENTITY; -public class MergeValidationService { +/** + * Supporting class that validates input parameters to {@link ResourceMergeService}. + */ + +class MergeValidationService { private final FhirContext myFhirContext; private final IFhirResourceDao myPatientDao; @@ -63,8 +67,7 @@ MergeValidationResult validate( IBaseOperationOutcome operationOutcome = theMergeOutcome.getOperationOutcome(); if (!validateMergeOperationParameters(theMergeOperationParameters, operationOutcome)) { - theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); - return MergeValidationResult.invalidResult(); + return MergeValidationResult.invalidResult(STATUS_HTTP_400_BAD_REQUEST); } // cast to Patient, since we only support merging Patient resources for now @@ -72,8 +75,7 @@ MergeValidationResult validate( (Patient) resolveSourceResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (sourceResource == null) { - theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); - return MergeValidationResult.invalidResult(); + return MergeValidationResult.invalidResult(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); } // cast to Patient, since we only support merging Patient resources for now @@ -81,19 +83,16 @@ MergeValidationResult validate( (Patient) resolveTargetResource(theMergeOperationParameters, theRequestDetails, operationOutcome); if (targetResource == null) { - theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); - return MergeValidationResult.invalidResult(); + return MergeValidationResult.invalidResult(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); } if (!validateSourceAndTargetAreSuitableForMerge(sourceResource, targetResource, operationOutcome)) { - theMergeOutcome.setHttpStatusCode(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); - return MergeValidationResult.invalidResult(); + return MergeValidationResult.invalidResult(STATUS_HTTP_422_UNPROCESSABLE_ENTITY); } if (!validateResultResourceIfExists( theMergeOperationParameters, targetResource, sourceResource, operationOutcome)) { - theMergeOutcome.setHttpStatusCode(STATUS_HTTP_400_BAD_REQUEST); - return MergeValidationResult.invalidResult(); + return MergeValidationResult.invalidResult(STATUS_HTTP_400_BAD_REQUEST); } return MergeValidationResult.validResult(sourceResource, targetResource); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 812db3f18757..6abbb0b6eaae 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -66,12 +66,12 @@ public class ResourceMergeService { private final MergeValidationService myMergeValidationService; public ResourceMergeService( - DaoRegistry theDaoRegistry, - IReplaceReferencesSvc theReplaceReferencesSvc, - IHapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator, - Batch2TaskHelper theBatch2TaskHelper) { + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + IHapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper) { myPatientDao = theDaoRegistry.getResourceDao(Patient.class); myTaskDao = theDaoRegistry.getResourceDao(Task.class); @@ -95,14 +95,13 @@ public ResourceMergeService( * @return the merge outcome containing OperationOutcome and HTTP status code */ public MergeOperationOutcome merge( - BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); mergeOutcome.setOperationOutcome(operationOutcome); // default to 200 OK, would be changed to another code during processing as required mergeOutcome.setHttpStatusCode(STATUS_HTTP_200_OK); - try { validateAndMerge(theMergeOperationParameters, theRequestDetails, mergeOutcome); } catch (Exception e) { @@ -118,12 +117,12 @@ public MergeOperationOutcome merge( } private void validateAndMerge( - BaseMergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + BaseMergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { MergeValidationResult mergeValidationResult = - myMergeValidationService.validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); + myMergeValidationService.validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); if (mergeValidationResult.isValid) { Patient sourceResource = mergeValidationResult.sourceResource; @@ -131,36 +130,39 @@ private void validateAndMerge( if (theMergeOperationParameters.getPreview()) { handlePreview( - sourceResource, - targetResource, - theMergeOperationParameters, - theRequestDetails, - theMergeOutcome); + sourceResource, + targetResource, + theMergeOperationParameters, + theRequestDetails, + theMergeOutcome); } else { doMerge( - theMergeOperationParameters, - sourceResource, - targetResource, - theRequestDetails, - theMergeOutcome); + theMergeOperationParameters, + sourceResource, + targetResource, + theRequestDetails, + theMergeOutcome); } + } else { + theMergeOutcome.setHttpStatusCode(mergeValidationResult.httpStatusCode); } + } private void handlePreview( - Patient theSourceResource, - Patient theTargetResource, - BaseMergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + Patient theSourceResource, + Patient theTargetResource, + BaseMergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); + theSourceResource.getIdElement().toVersionless(), theRequestDetails); // in preview mode, we should also return what the target would look like Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); Patient targetPatientAsIfUpdated = myMergeResourceHelper.prepareTargetPatientForUpdate( - theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); + theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); // adding +2 because the source and the target resources would be updated as well @@ -170,65 +172,65 @@ private void handlePreview( } private void doMerge( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( - theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); + theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); if (theRequestDetails.isPreferAsync()) { // client prefers async processing, do async doMergeAsync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } else { // count the number of refs, if it is larger than batch size then process async, otherwise process sync Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); + theSourceResource.getIdElement().toVersionless(), theRequestDetails); if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { ourLog.info( - "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", - numberOfRefs, - theMergeOperationParameters.getBatchSize()); + "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", + numberOfRefs, + theMergeOperationParameters.getBatchSize()); doMergeAsync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } else { doMergeSync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } } } private void doMergeSync( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId partitionId) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId partitionId) { ReplaceReferencesRequest replaceReferencesRequest = new ReplaceReferencesRequest( - theSourceResource.getIdElement(), - theTargetResource.getIdElement(), - theMergeOperationParameters.getBatchSize(), - partitionId); + theSourceResource.getIdElement(), + theTargetResource.getIdElement(), + theMergeOperationParameters.getBatchSize(), + partitionId); // We don't want replace references to flip to async mode once we've already made the decision to go sync here. replaceReferencesRequest.setForceSync(true); @@ -236,12 +238,12 @@ private void doMergeSync( myReplaceReferencesSvc.replaceReferences(replaceReferencesRequest, theRequestDetails); Patient updatedTarget = myMergeResourceHelper.updateMergedResourcesAfterReferencesReplaced( - myHapiTransactionService, - theSourceResource, - theTargetResource, - (Patient) theMergeOperationParameters.getResultResource(), - theMergeOperationParameters.getDeleteSource(), - theRequestDetails); + myHapiTransactionService, + theSourceResource, + theTargetResource, + (Patient) theMergeOperationParameters.getResultResource(), + theMergeOperationParameters.getDeleteSource(), + theRequestDetails); theMergeOutcome.setUpdatedTargetResource(updatedTarget); String detailsText = "Merge operation completed successfully."; @@ -249,29 +251,29 @@ private void doMergeSync( } private void doMergeAsync( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId thePartitionId) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId thePartitionId) { MergeJobParameters mergeJobParameters = new MergeJobParameters(); if (theMergeOperationParameters.getResultResource() != null) { mergeJobParameters.setResultResource(myFhirContext - .newJsonParser() - .encodeResourceToString(theMergeOperationParameters.getResultResource())); + .newJsonParser() + .encodeResourceToString(theMergeOperationParameters.getResultResource())); } mergeJobParameters.setDeleteSource(theMergeOperationParameters.getDeleteSource()); mergeJobParameters.setBatchSize(theMergeOperationParameters.getBatchSize()); mergeJobParameters.setSourceId( - new FhirIdJson(theSourceResource.getIdElement().toVersionless())); + new FhirIdJson(theSourceResource.getIdElement().toVersionless())); mergeJobParameters.setTargetId( - new FhirIdJson(theTargetResource.getIdElement().toVersionless())); + new FhirIdJson(theTargetResource.getIdElement().toVersionless())); mergeJobParameters.setPartitionId(thePartitionId); Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( - myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); + myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); task.getMeta().setVersionId(null); @@ -279,14 +281,14 @@ private void doMergeAsync( theMergeOutcome.setHttpStatusCode(STATUS_HTTP_202_ACCEPTED); String detailsText = "Merge request is accepted, and will be processed asynchronously. See" - + " task resource returned in this response for details."; + + " task resource returned in this response for details."; addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); } private void addInfoToOperationOutcome( - IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { + IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { IBase issue = - OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); OperationOutcomeUtil.addDetailsToIssue(myFhirContext, issue, null, null, theDetailsText); } } From f3003f43d1ead4bf0499b1eb554fbf068c155b8a Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 13:21:59 -0500 Subject: [PATCH 122/148] review feedback --- .../provider/merge/ResourceMergeService.java | 173 +++++++++--------- 1 file changed, 86 insertions(+), 87 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 6abbb0b6eaae..5dc7a5b661d6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -66,12 +66,12 @@ public class ResourceMergeService { private final MergeValidationService myMergeValidationService; public ResourceMergeService( - DaoRegistry theDaoRegistry, - IReplaceReferencesSvc theReplaceReferencesSvc, - IHapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator, - Batch2TaskHelper theBatch2TaskHelper) { + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + IHapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper) { myPatientDao = theDaoRegistry.getResourceDao(Patient.class); myTaskDao = theDaoRegistry.getResourceDao(Task.class); @@ -95,7 +95,7 @@ public ResourceMergeService( * @return the merge outcome containing OperationOutcome and HTTP status code */ public MergeOperationOutcome merge( - BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); @@ -117,12 +117,12 @@ public MergeOperationOutcome merge( } private void validateAndMerge( - BaseMergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + BaseMergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { MergeValidationResult mergeValidationResult = - myMergeValidationService.validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); + myMergeValidationService.validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); if (mergeValidationResult.isValid) { Patient sourceResource = mergeValidationResult.sourceResource; @@ -130,39 +130,38 @@ private void validateAndMerge( if (theMergeOperationParameters.getPreview()) { handlePreview( - sourceResource, - targetResource, - theMergeOperationParameters, - theRequestDetails, - theMergeOutcome); + sourceResource, + targetResource, + theMergeOperationParameters, + theRequestDetails, + theMergeOutcome); } else { doMerge( - theMergeOperationParameters, - sourceResource, - targetResource, - theRequestDetails, - theMergeOutcome); + theMergeOperationParameters, + sourceResource, + targetResource, + theRequestDetails, + theMergeOutcome); } } else { theMergeOutcome.setHttpStatusCode(mergeValidationResult.httpStatusCode); } - } private void handlePreview( - Patient theSourceResource, - Patient theTargetResource, - BaseMergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + Patient theSourceResource, + Patient theTargetResource, + BaseMergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); + theSourceResource.getIdElement().toVersionless(), theRequestDetails); // in preview mode, we should also return what the target would look like Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); Patient targetPatientAsIfUpdated = myMergeResourceHelper.prepareTargetPatientForUpdate( - theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); + theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); // adding +2 because the source and the target resources would be updated as well @@ -172,65 +171,65 @@ private void handlePreview( } private void doMerge( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( - theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); + theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); if (theRequestDetails.isPreferAsync()) { // client prefers async processing, do async doMergeAsync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); - } else { - // count the number of refs, if it is larger than batch size then process async, otherwise process sync - Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); - if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { - ourLog.info( - "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", - numberOfRefs, - theMergeOperationParameters.getBatchSize()); - doMergeAsync( theMergeOperationParameters, theSourceResource, theTargetResource, theRequestDetails, theMergeOutcome, partitionId); + } else { + // count the number of refs, if it is larger than batch size then process async, otherwise process sync + Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( + theSourceResource.getIdElement().toVersionless(), theRequestDetails); + if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { + ourLog.info( + "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", + numberOfRefs, + theMergeOperationParameters.getBatchSize()); + doMergeAsync( + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } else { doMergeSync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } } } private void doMergeSync( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId partitionId) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId partitionId) { ReplaceReferencesRequest replaceReferencesRequest = new ReplaceReferencesRequest( - theSourceResource.getIdElement(), - theTargetResource.getIdElement(), - theMergeOperationParameters.getBatchSize(), - partitionId); + theSourceResource.getIdElement(), + theTargetResource.getIdElement(), + theMergeOperationParameters.getBatchSize(), + partitionId); // We don't want replace references to flip to async mode once we've already made the decision to go sync here. replaceReferencesRequest.setForceSync(true); @@ -238,12 +237,12 @@ private void doMergeSync( myReplaceReferencesSvc.replaceReferences(replaceReferencesRequest, theRequestDetails); Patient updatedTarget = myMergeResourceHelper.updateMergedResourcesAfterReferencesReplaced( - myHapiTransactionService, - theSourceResource, - theTargetResource, - (Patient) theMergeOperationParameters.getResultResource(), - theMergeOperationParameters.getDeleteSource(), - theRequestDetails); + myHapiTransactionService, + theSourceResource, + theTargetResource, + (Patient) theMergeOperationParameters.getResultResource(), + theMergeOperationParameters.getDeleteSource(), + theRequestDetails); theMergeOutcome.setUpdatedTargetResource(updatedTarget); String detailsText = "Merge operation completed successfully."; @@ -251,29 +250,29 @@ private void doMergeSync( } private void doMergeAsync( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId thePartitionId) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId thePartitionId) { MergeJobParameters mergeJobParameters = new MergeJobParameters(); if (theMergeOperationParameters.getResultResource() != null) { mergeJobParameters.setResultResource(myFhirContext - .newJsonParser() - .encodeResourceToString(theMergeOperationParameters.getResultResource())); + .newJsonParser() + .encodeResourceToString(theMergeOperationParameters.getResultResource())); } mergeJobParameters.setDeleteSource(theMergeOperationParameters.getDeleteSource()); mergeJobParameters.setBatchSize(theMergeOperationParameters.getBatchSize()); mergeJobParameters.setSourceId( - new FhirIdJson(theSourceResource.getIdElement().toVersionless())); + new FhirIdJson(theSourceResource.getIdElement().toVersionless())); mergeJobParameters.setTargetId( - new FhirIdJson(theTargetResource.getIdElement().toVersionless())); + new FhirIdJson(theTargetResource.getIdElement().toVersionless())); mergeJobParameters.setPartitionId(thePartitionId); Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( - myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); + myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); task.getMeta().setVersionId(null); @@ -281,14 +280,14 @@ private void doMergeAsync( theMergeOutcome.setHttpStatusCode(STATUS_HTTP_202_ACCEPTED); String detailsText = "Merge request is accepted, and will be processed asynchronously. See" - + " task resource returned in this response for details."; + + " task resource returned in this response for details."; addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); } private void addInfoToOperationOutcome( - IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { + IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { IBase issue = - OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); OperationOutcomeUtil.addDetailsToIssue(myFhirContext, issue, null, null, theDetailsText); } } From ca20ef3ba47de9fc669d23943ca9e5350e7839d6 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 13:22:13 -0500 Subject: [PATCH 123/148] review feedback --- .../provider/merge/ResourceMergeService.java | 174 +++++++++--------- 1 file changed, 88 insertions(+), 86 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 5dc7a5b661d6..f31b3efe3845 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -66,12 +66,12 @@ public class ResourceMergeService { private final MergeValidationService myMergeValidationService; public ResourceMergeService( - DaoRegistry theDaoRegistry, - IReplaceReferencesSvc theReplaceReferencesSvc, - IHapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator, - Batch2TaskHelper theBatch2TaskHelper) { + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + IHapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper) { myPatientDao = theDaoRegistry.getResourceDao(Patient.class); myTaskDao = theDaoRegistry.getResourceDao(Task.class); @@ -95,7 +95,7 @@ public ResourceMergeService( * @return the merge outcome containing OperationOutcome and HTTP status code */ public MergeOperationOutcome merge( - BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); @@ -117,12 +117,13 @@ public MergeOperationOutcome merge( } private void validateAndMerge( - BaseMergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + BaseMergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { + // TODO KHS remove the outparameter and instead accumulate issues in the validation result MergeValidationResult mergeValidationResult = - myMergeValidationService.validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); + myMergeValidationService.validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); if (mergeValidationResult.isValid) { Patient sourceResource = mergeValidationResult.sourceResource; @@ -130,38 +131,39 @@ private void validateAndMerge( if (theMergeOperationParameters.getPreview()) { handlePreview( - sourceResource, - targetResource, - theMergeOperationParameters, - theRequestDetails, - theMergeOutcome); + sourceResource, + targetResource, + theMergeOperationParameters, + theRequestDetails, + theMergeOutcome); } else { doMerge( - theMergeOperationParameters, - sourceResource, - targetResource, - theRequestDetails, - theMergeOutcome); + theMergeOperationParameters, + sourceResource, + targetResource, + theRequestDetails, + theMergeOutcome); } } else { theMergeOutcome.setHttpStatusCode(mergeValidationResult.httpStatusCode); } + } private void handlePreview( - Patient theSourceResource, - Patient theTargetResource, - BaseMergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + Patient theSourceResource, + Patient theTargetResource, + BaseMergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); + theSourceResource.getIdElement().toVersionless(), theRequestDetails); // in preview mode, we should also return what the target would look like Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); Patient targetPatientAsIfUpdated = myMergeResourceHelper.prepareTargetPatientForUpdate( - theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); + theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); // adding +2 because the source and the target resources would be updated as well @@ -171,65 +173,65 @@ private void handlePreview( } private void doMerge( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( - theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); + theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); if (theRequestDetails.isPreferAsync()) { // client prefers async processing, do async doMergeAsync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } else { // count the number of refs, if it is larger than batch size then process async, otherwise process sync Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); + theSourceResource.getIdElement().toVersionless(), theRequestDetails); if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { ourLog.info( - "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", - numberOfRefs, - theMergeOperationParameters.getBatchSize()); + "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", + numberOfRefs, + theMergeOperationParameters.getBatchSize()); doMergeAsync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } else { doMergeSync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } } } private void doMergeSync( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId partitionId) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId partitionId) { ReplaceReferencesRequest replaceReferencesRequest = new ReplaceReferencesRequest( - theSourceResource.getIdElement(), - theTargetResource.getIdElement(), - theMergeOperationParameters.getBatchSize(), - partitionId); + theSourceResource.getIdElement(), + theTargetResource.getIdElement(), + theMergeOperationParameters.getBatchSize(), + partitionId); // We don't want replace references to flip to async mode once we've already made the decision to go sync here. replaceReferencesRequest.setForceSync(true); @@ -237,12 +239,12 @@ private void doMergeSync( myReplaceReferencesSvc.replaceReferences(replaceReferencesRequest, theRequestDetails); Patient updatedTarget = myMergeResourceHelper.updateMergedResourcesAfterReferencesReplaced( - myHapiTransactionService, - theSourceResource, - theTargetResource, - (Patient) theMergeOperationParameters.getResultResource(), - theMergeOperationParameters.getDeleteSource(), - theRequestDetails); + myHapiTransactionService, + theSourceResource, + theTargetResource, + (Patient) theMergeOperationParameters.getResultResource(), + theMergeOperationParameters.getDeleteSource(), + theRequestDetails); theMergeOutcome.setUpdatedTargetResource(updatedTarget); String detailsText = "Merge operation completed successfully."; @@ -250,29 +252,29 @@ private void doMergeSync( } private void doMergeAsync( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId thePartitionId) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId thePartitionId) { MergeJobParameters mergeJobParameters = new MergeJobParameters(); if (theMergeOperationParameters.getResultResource() != null) { mergeJobParameters.setResultResource(myFhirContext - .newJsonParser() - .encodeResourceToString(theMergeOperationParameters.getResultResource())); + .newJsonParser() + .encodeResourceToString(theMergeOperationParameters.getResultResource())); } mergeJobParameters.setDeleteSource(theMergeOperationParameters.getDeleteSource()); mergeJobParameters.setBatchSize(theMergeOperationParameters.getBatchSize()); mergeJobParameters.setSourceId( - new FhirIdJson(theSourceResource.getIdElement().toVersionless())); + new FhirIdJson(theSourceResource.getIdElement().toVersionless())); mergeJobParameters.setTargetId( - new FhirIdJson(theTargetResource.getIdElement().toVersionless())); + new FhirIdJson(theTargetResource.getIdElement().toVersionless())); mergeJobParameters.setPartitionId(thePartitionId); Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( - myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); + myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); task.getMeta().setVersionId(null); @@ -280,14 +282,14 @@ private void doMergeAsync( theMergeOutcome.setHttpStatusCode(STATUS_HTTP_202_ACCEPTED); String detailsText = "Merge request is accepted, and will be processed asynchronously. See" - + " task resource returned in this response for details."; + + " task resource returned in this response for details."; addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); } private void addInfoToOperationOutcome( - IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { + IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { IBase issue = - OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); OperationOutcomeUtil.addDetailsToIssue(myFhirContext, issue, null, null, theDetailsText); } } From 837b09a65e682dad99a7f96839a3913b33a6979a Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 13:22:49 -0500 Subject: [PATCH 124/148] review feedback --- .../uhn/fhir/util/StopLimitAccumulator.java | 1 - .../provider/merge/MergeValidationResult.java | 3 +- .../merge/MergeValidationService.java | 1 - .../provider/merge/ResourceMergeService.java | 173 +++++++++--------- 4 files changed, 88 insertions(+), 90 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java index 10bc69c86710..06de80765bbb 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/StopLimitAccumulator.java @@ -33,7 +33,6 @@ * * @param the type of object being streamed */ - public class StopLimitAccumulator { private final boolean isTruncated; private final List myList; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java index 8de70b857b8f..da020d659fea 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationResult.java @@ -27,7 +27,8 @@ class MergeValidationResult { final boolean isValid; final Integer httpStatusCode; - private MergeValidationResult(boolean theIsValid, Integer theHttpStatusCode, Patient theSourceResource, Patient theTargetResource) { + private MergeValidationResult( + boolean theIsValid, Integer theHttpStatusCode, Patient theSourceResource, Patient theTargetResource) { isValid = theIsValid; httpStatusCode = theHttpStatusCode; sourceResource = theSourceResource; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java index c68464324af6..9f17332ebea8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java @@ -49,7 +49,6 @@ /** * Supporting class that validates input parameters to {@link ResourceMergeService}. */ - class MergeValidationService { private final FhirContext myFhirContext; private final IFhirResourceDao myPatientDao; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index f31b3efe3845..03c482973140 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -66,12 +66,12 @@ public class ResourceMergeService { private final MergeValidationService myMergeValidationService; public ResourceMergeService( - DaoRegistry theDaoRegistry, - IReplaceReferencesSvc theReplaceReferencesSvc, - IHapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator, - Batch2TaskHelper theBatch2TaskHelper) { + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + IHapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper) { myPatientDao = theDaoRegistry.getResourceDao(Patient.class); myTaskDao = theDaoRegistry.getResourceDao(Task.class); @@ -95,7 +95,7 @@ public ResourceMergeService( * @return the merge outcome containing OperationOutcome and HTTP status code */ public MergeOperationOutcome merge( - BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); @@ -117,13 +117,13 @@ public MergeOperationOutcome merge( } private void validateAndMerge( - BaseMergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + BaseMergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { // TODO KHS remove the outparameter and instead accumulate issues in the validation result MergeValidationResult mergeValidationResult = - myMergeValidationService.validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); + myMergeValidationService.validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); if (mergeValidationResult.isValid) { Patient sourceResource = mergeValidationResult.sourceResource; @@ -131,39 +131,38 @@ private void validateAndMerge( if (theMergeOperationParameters.getPreview()) { handlePreview( - sourceResource, - targetResource, - theMergeOperationParameters, - theRequestDetails, - theMergeOutcome); + sourceResource, + targetResource, + theMergeOperationParameters, + theRequestDetails, + theMergeOutcome); } else { doMerge( - theMergeOperationParameters, - sourceResource, - targetResource, - theRequestDetails, - theMergeOutcome); + theMergeOperationParameters, + sourceResource, + targetResource, + theRequestDetails, + theMergeOutcome); } } else { theMergeOutcome.setHttpStatusCode(mergeValidationResult.httpStatusCode); } - } private void handlePreview( - Patient theSourceResource, - Patient theTargetResource, - BaseMergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + Patient theSourceResource, + Patient theTargetResource, + BaseMergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); + theSourceResource.getIdElement().toVersionless(), theRequestDetails); // in preview mode, we should also return what the target would look like Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); Patient targetPatientAsIfUpdated = myMergeResourceHelper.prepareTargetPatientForUpdate( - theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); + theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); // adding +2 because the source and the target resources would be updated as well @@ -173,65 +172,65 @@ private void handlePreview( } private void doMerge( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( - theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); + theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); if (theRequestDetails.isPreferAsync()) { // client prefers async processing, do async doMergeAsync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); - } else { - // count the number of refs, if it is larger than batch size then process async, otherwise process sync - Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); - if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { - ourLog.info( - "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", - numberOfRefs, - theMergeOperationParameters.getBatchSize()); - doMergeAsync( theMergeOperationParameters, theSourceResource, theTargetResource, theRequestDetails, theMergeOutcome, partitionId); + } else { + // count the number of refs, if it is larger than batch size then process async, otherwise process sync + Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( + theSourceResource.getIdElement().toVersionless(), theRequestDetails); + if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { + ourLog.info( + "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", + numberOfRefs, + theMergeOperationParameters.getBatchSize()); + doMergeAsync( + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } else { doMergeSync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } } } private void doMergeSync( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId partitionId) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId partitionId) { ReplaceReferencesRequest replaceReferencesRequest = new ReplaceReferencesRequest( - theSourceResource.getIdElement(), - theTargetResource.getIdElement(), - theMergeOperationParameters.getBatchSize(), - partitionId); + theSourceResource.getIdElement(), + theTargetResource.getIdElement(), + theMergeOperationParameters.getBatchSize(), + partitionId); // We don't want replace references to flip to async mode once we've already made the decision to go sync here. replaceReferencesRequest.setForceSync(true); @@ -239,12 +238,12 @@ private void doMergeSync( myReplaceReferencesSvc.replaceReferences(replaceReferencesRequest, theRequestDetails); Patient updatedTarget = myMergeResourceHelper.updateMergedResourcesAfterReferencesReplaced( - myHapiTransactionService, - theSourceResource, - theTargetResource, - (Patient) theMergeOperationParameters.getResultResource(), - theMergeOperationParameters.getDeleteSource(), - theRequestDetails); + myHapiTransactionService, + theSourceResource, + theTargetResource, + (Patient) theMergeOperationParameters.getResultResource(), + theMergeOperationParameters.getDeleteSource(), + theRequestDetails); theMergeOutcome.setUpdatedTargetResource(updatedTarget); String detailsText = "Merge operation completed successfully."; @@ -252,29 +251,29 @@ private void doMergeSync( } private void doMergeAsync( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId thePartitionId) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId thePartitionId) { MergeJobParameters mergeJobParameters = new MergeJobParameters(); if (theMergeOperationParameters.getResultResource() != null) { mergeJobParameters.setResultResource(myFhirContext - .newJsonParser() - .encodeResourceToString(theMergeOperationParameters.getResultResource())); + .newJsonParser() + .encodeResourceToString(theMergeOperationParameters.getResultResource())); } mergeJobParameters.setDeleteSource(theMergeOperationParameters.getDeleteSource()); mergeJobParameters.setBatchSize(theMergeOperationParameters.getBatchSize()); mergeJobParameters.setSourceId( - new FhirIdJson(theSourceResource.getIdElement().toVersionless())); + new FhirIdJson(theSourceResource.getIdElement().toVersionless())); mergeJobParameters.setTargetId( - new FhirIdJson(theTargetResource.getIdElement().toVersionless())); + new FhirIdJson(theTargetResource.getIdElement().toVersionless())); mergeJobParameters.setPartitionId(thePartitionId); Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( - myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); + myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); task.getMeta().setVersionId(null); @@ -282,14 +281,14 @@ private void doMergeAsync( theMergeOutcome.setHttpStatusCode(STATUS_HTTP_202_ACCEPTED); String detailsText = "Merge request is accepted, and will be processed asynchronously. See" - + " task resource returned in this response for details."; + + " task resource returned in this response for details."; addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); } private void addInfoToOperationOutcome( - IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { + IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { IBase issue = - OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); OperationOutcomeUtil.addDetailsToIssue(myFhirContext, issue, null, null, theDetailsText); } } From 0d4c07c29ff35435fdbe4771b49ba1e510f65a7d Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 13:38:05 -0500 Subject: [PATCH 125/148] review feedback --- .../ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java | 3 ++- .../uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java index 9f17332ebea8..f64dca05c78d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeValidationService.java @@ -114,9 +114,10 @@ private boolean validateResultResourceIfExists( // validate the result resource's id as same as the target resource if (!theResolvedTargetResource.getIdElement().toVersionless().equals(theResultResource.getIdElement())) { String msg = String.format( - "'%s' must have the same versionless id as the actual resolved target resource. " + "'%s' must have the same versionless id as the actual resolved target resource '%s'. " + "The actual resolved target resource's id is: '%s'", theMergeOperationParameters.getResultResourceParameterName(), + theResultResource.getIdElement(), theResolvedTargetResource.getIdElement().toVersionless().getValue()); addErrorToOperationOutcome(theOperationOutcome, msg, "invalid"); retval = false; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java index 44a10391d166..7024d8a8990f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java @@ -1106,7 +1106,7 @@ void testMerge_ValidatesResultResource_ResultResourceHasDifferentIdThanTargetRes assertThat(issue.getSeverity()).isEqualTo(OperationOutcome.IssueSeverity.ERROR); assertThat(issue.getDiagnostics()).contains("'result-patient' must have the same versionless id " + "as the actual" + - " resolved target resource. The actual resolved target resource's id is: '" + TARGET_PATIENT_TEST_ID +"'"); + " resolved target resource 'Patient/not-the-target-id'. The actual resolved target resource's id is: '" + TARGET_PATIENT_TEST_ID +"'"); verifyNoMoreInteractions(myPatientDaoMock); } From a0ea555503ef842003def2aa756c83bd56ebafec Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 14:05:52 -0500 Subject: [PATCH 126/148] review feedback --- .../BaseMergeOperationInputParameters.java | 22 ++ .../provider/merge/ResourceMergeService.java | 196 ++++++++---------- 2 files changed, 111 insertions(+), 107 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java index e35ddf850fe8..ce2df0fb867d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java @@ -19,9 +19,14 @@ */ package ca.uhn.fhir.jpa.provider.merge; +import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; +import ca.uhn.fhir.batch2.jobs.merge.MergeJobParameters; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.util.CanonicalIdentifier; import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Patient; import java.util.List; @@ -120,4 +125,21 @@ public void setTargetResource(IBaseReference theTargetResource) { public int getBatchSize() { return myBatchSize; } + + public MergeJobParameters asMergeJobParameters(FhirContext theFhirContext, Patient theSourceResource, Patient theTargetResource, RequestPartitionId thePartitionId) { + MergeJobParameters retval = new MergeJobParameters(); + if (getResultResource() != null) { + retval.setResultResource(theFhirContext + .newJsonParser() + .encodeResourceToString(getResultResource())); + } + retval.setDeleteSource(getDeleteSource()); + retval.setBatchSize(getBatchSize()); + retval.setSourceId( + new FhirIdJson(theSourceResource.getIdElement().toVersionless())); + retval.setTargetId( + new FhirIdJson(theTargetResource.getIdElement().toVersionless())); + retval.setPartitionId(thePartitionId); + return retval; + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 03c482973140..dafacc29b3dd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -20,7 +20,6 @@ package ca.uhn.fhir.jpa.provider.merge; import ca.uhn.fhir.batch2.api.IJobCoordinator; -import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; import ca.uhn.fhir.batch2.jobs.merge.MergeJobParameters; import ca.uhn.fhir.batch2.jobs.merge.MergeResourceHelper; import ca.uhn.fhir.batch2.util.Batch2TaskHelper; @@ -66,12 +65,12 @@ public class ResourceMergeService { private final MergeValidationService myMergeValidationService; public ResourceMergeService( - DaoRegistry theDaoRegistry, - IReplaceReferencesSvc theReplaceReferencesSvc, - IHapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator, - Batch2TaskHelper theBatch2TaskHelper) { + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + IHapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper) { myPatientDao = theDaoRegistry.getResourceDao(Patient.class); myTaskDao = theDaoRegistry.getResourceDao(Task.class); @@ -95,7 +94,7 @@ public ResourceMergeService( * @return the merge outcome containing OperationOutcome and HTTP status code */ public MergeOperationOutcome merge( - BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); @@ -117,13 +116,13 @@ public MergeOperationOutcome merge( } private void validateAndMerge( - BaseMergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + BaseMergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { // TODO KHS remove the outparameter and instead accumulate issues in the validation result MergeValidationResult mergeValidationResult = - myMergeValidationService.validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); + myMergeValidationService.validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); if (mergeValidationResult.isValid) { Patient sourceResource = mergeValidationResult.sourceResource; @@ -131,18 +130,18 @@ private void validateAndMerge( if (theMergeOperationParameters.getPreview()) { handlePreview( - sourceResource, - targetResource, - theMergeOperationParameters, - theRequestDetails, - theMergeOutcome); + sourceResource, + targetResource, + theMergeOperationParameters, + theRequestDetails, + theMergeOutcome); } else { doMerge( - theMergeOperationParameters, - sourceResource, - targetResource, - theRequestDetails, - theMergeOutcome); + theMergeOperationParameters, + sourceResource, + targetResource, + theRequestDetails, + theMergeOutcome); } } else { theMergeOutcome.setHttpStatusCode(mergeValidationResult.httpStatusCode); @@ -150,19 +149,19 @@ private void validateAndMerge( } private void handlePreview( - Patient theSourceResource, - Patient theTargetResource, - BaseMergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + Patient theSourceResource, + Patient theTargetResource, + BaseMergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); + theSourceResource.getIdElement().toVersionless(), theRequestDetails); // in preview mode, we should also return what the target would look like Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); Patient targetPatientAsIfUpdated = myMergeResourceHelper.prepareTargetPatientForUpdate( - theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); + theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); // adding +2 because the source and the target resources would be updated as well @@ -172,65 +171,60 @@ private void handlePreview( } private void doMerge( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( - theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); + theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); - if (theRequestDetails.isPreferAsync()) { - // client prefers async processing, do async + if (theRequestDetails.isPreferAsync() || referenceCountExceedsSyncLimit(theSourceResource, theRequestDetails, theMergeOperationParameters.getBatchSize())) { doMergeAsync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } else { - // count the number of refs, if it is larger than batch size then process async, otherwise process sync - Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); - if (numberOfRefs > theMergeOperationParameters.getBatchSize()) { - ourLog.info( - "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", - numberOfRefs, - theMergeOperationParameters.getBatchSize()); - doMergeAsync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); - } else { - doMergeSync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); - } + doMergeSync( + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); + } + } + + private boolean referenceCountExceedsSyncLimit(Patient theSourceResource, RequestDetails theRequestDetails, Integer theBatchSize) { + Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( + theSourceResource.getIdElement().toVersionless(), theRequestDetails); + boolean exceedsSyncLimit = numberOfRefs > theBatchSize; + if (exceedsSyncLimit) { + ourLog.info( + "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", + numberOfRefs, + theBatchSize); } + return exceedsSyncLimit; } private void doMergeSync( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId partitionId) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId partitionId) { ReplaceReferencesRequest replaceReferencesRequest = new ReplaceReferencesRequest( - theSourceResource.getIdElement(), - theTargetResource.getIdElement(), - theMergeOperationParameters.getBatchSize(), - partitionId); + theSourceResource.getIdElement(), + theTargetResource.getIdElement(), + theMergeOperationParameters.getBatchSize(), + partitionId); // We don't want replace references to flip to async mode once we've already made the decision to go sync here. replaceReferencesRequest.setForceSync(true); @@ -238,12 +232,12 @@ private void doMergeSync( myReplaceReferencesSvc.replaceReferences(replaceReferencesRequest, theRequestDetails); Patient updatedTarget = myMergeResourceHelper.updateMergedResourcesAfterReferencesReplaced( - myHapiTransactionService, - theSourceResource, - theTargetResource, - (Patient) theMergeOperationParameters.getResultResource(), - theMergeOperationParameters.getDeleteSource(), - theRequestDetails); + myHapiTransactionService, + theSourceResource, + theTargetResource, + (Patient) theMergeOperationParameters.getResultResource(), + theMergeOperationParameters.getDeleteSource(), + theRequestDetails); theMergeOutcome.setUpdatedTargetResource(updatedTarget); String detailsText = "Merge operation completed successfully."; @@ -251,29 +245,17 @@ private void doMergeSync( } private void doMergeAsync( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId thePartitionId) { - - MergeJobParameters mergeJobParameters = new MergeJobParameters(); - if (theMergeOperationParameters.getResultResource() != null) { - mergeJobParameters.setResultResource(myFhirContext - .newJsonParser() - .encodeResourceToString(theMergeOperationParameters.getResultResource())); - } - mergeJobParameters.setDeleteSource(theMergeOperationParameters.getDeleteSource()); - mergeJobParameters.setBatchSize(theMergeOperationParameters.getBatchSize()); - mergeJobParameters.setSourceId( - new FhirIdJson(theSourceResource.getIdElement().toVersionless())); - mergeJobParameters.setTargetId( - new FhirIdJson(theTargetResource.getIdElement().toVersionless())); - mergeJobParameters.setPartitionId(thePartitionId); + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId thePartitionId) { + + MergeJobParameters mergeJobParameters = theMergeOperationParameters.asMergeJobParameters(myFhirContext, theSourceResource, theTargetResource, thePartitionId); Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( - myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); + myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); task.getMeta().setVersionId(null); @@ -281,14 +263,14 @@ private void doMergeAsync( theMergeOutcome.setHttpStatusCode(STATUS_HTTP_202_ACCEPTED); String detailsText = "Merge request is accepted, and will be processed asynchronously. See" - + " task resource returned in this response for details."; + + " task resource returned in this response for details."; addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); } private void addInfoToOperationOutcome( - IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { + IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { IBase issue = - OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); OperationOutcomeUtil.addDetailsToIssue(myFhirContext, issue, null, null, theDetailsText); } } From 611e19649f377ac9c25b149e04caa9bb06ec9555 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 14:13:40 -0500 Subject: [PATCH 127/148] review feedback --- .../BaseMergeOperationInputParameters.java | 16 +- .../provider/merge/ResourceMergeService.java | 162 +++++++++--------- .../config/BaseSubscriptionSettings.java | 8 +- .../jpa/api/config/JpaStorageSettings.java | 16 +- 4 files changed, 103 insertions(+), 99 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java index ce2df0fb867d..2496395a1f1e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java @@ -126,19 +126,19 @@ public int getBatchSize() { return myBatchSize; } - public MergeJobParameters asMergeJobParameters(FhirContext theFhirContext, Patient theSourceResource, Patient theTargetResource, RequestPartitionId thePartitionId) { + public MergeJobParameters asMergeJobParameters( + FhirContext theFhirContext, + Patient theSourceResource, + Patient theTargetResource, + RequestPartitionId thePartitionId) { MergeJobParameters retval = new MergeJobParameters(); if (getResultResource() != null) { - retval.setResultResource(theFhirContext - .newJsonParser() - .encodeResourceToString(getResultResource())); + retval.setResultResource(theFhirContext.newJsonParser().encodeResourceToString(getResultResource())); } retval.setDeleteSource(getDeleteSource()); retval.setBatchSize(getBatchSize()); - retval.setSourceId( - new FhirIdJson(theSourceResource.getIdElement().toVersionless())); - retval.setTargetId( - new FhirIdJson(theTargetResource.getIdElement().toVersionless())); + retval.setSourceId(new FhirIdJson(theSourceResource.getIdElement().toVersionless())); + retval.setTargetId(new FhirIdJson(theTargetResource.getIdElement().toVersionless())); retval.setPartitionId(thePartitionId); return retval; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index dafacc29b3dd..69c68d762b69 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -65,12 +65,12 @@ public class ResourceMergeService { private final MergeValidationService myMergeValidationService; public ResourceMergeService( - DaoRegistry theDaoRegistry, - IReplaceReferencesSvc theReplaceReferencesSvc, - IHapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator, - Batch2TaskHelper theBatch2TaskHelper) { + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + IHapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper) { myPatientDao = theDaoRegistry.getResourceDao(Patient.class); myTaskDao = theDaoRegistry.getResourceDao(Task.class); @@ -94,7 +94,7 @@ public ResourceMergeService( * @return the merge outcome containing OperationOutcome and HTTP status code */ public MergeOperationOutcome merge( - BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { + BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); @@ -116,13 +116,13 @@ public MergeOperationOutcome merge( } private void validateAndMerge( - BaseMergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + BaseMergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { // TODO KHS remove the outparameter and instead accumulate issues in the validation result MergeValidationResult mergeValidationResult = - myMergeValidationService.validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); + myMergeValidationService.validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); if (mergeValidationResult.isValid) { Patient sourceResource = mergeValidationResult.sourceResource; @@ -130,18 +130,18 @@ private void validateAndMerge( if (theMergeOperationParameters.getPreview()) { handlePreview( - sourceResource, - targetResource, - theMergeOperationParameters, - theRequestDetails, - theMergeOutcome); + sourceResource, + targetResource, + theMergeOperationParameters, + theRequestDetails, + theMergeOutcome); } else { doMerge( - theMergeOperationParameters, - sourceResource, - targetResource, - theRequestDetails, - theMergeOutcome); + theMergeOperationParameters, + sourceResource, + targetResource, + theRequestDetails, + theMergeOutcome); } } else { theMergeOutcome.setHttpStatusCode(mergeValidationResult.httpStatusCode); @@ -149,19 +149,19 @@ private void validateAndMerge( } private void handlePreview( - Patient theSourceResource, - Patient theTargetResource, - BaseMergeOperationInputParameters theMergeOperationParameters, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + Patient theSourceResource, + Patient theTargetResource, + BaseMergeOperationInputParameters theMergeOperationParameters, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); + theSourceResource.getIdElement().toVersionless(), theRequestDetails); // in preview mode, we should also return what the target would look like Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); Patient targetPatientAsIfUpdated = myMergeResourceHelper.prepareTargetPatientForUpdate( - theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); + theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); // adding +2 because the source and the target resources would be updated as well @@ -171,60 +171,63 @@ private void handlePreview( } private void doMerge( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome) { RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( - theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); + theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); - if (theRequestDetails.isPreferAsync() || referenceCountExceedsSyncLimit(theSourceResource, theRequestDetails, theMergeOperationParameters.getBatchSize())) { + if (theRequestDetails.isPreferAsync() + || referenceCountExceedsSyncLimit( + theSourceResource, theRequestDetails, theMergeOperationParameters.getBatchSize())) { doMergeAsync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } else { doMergeSync( - theMergeOperationParameters, - theSourceResource, - theTargetResource, - theRequestDetails, - theMergeOutcome, - partitionId); + theMergeOperationParameters, + theSourceResource, + theTargetResource, + theRequestDetails, + theMergeOutcome, + partitionId); } } - private boolean referenceCountExceedsSyncLimit(Patient theSourceResource, RequestDetails theRequestDetails, Integer theBatchSize) { + private boolean referenceCountExceedsSyncLimit( + Patient theSourceResource, RequestDetails theRequestDetails, Integer theBatchSize) { Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); + theSourceResource.getIdElement().toVersionless(), theRequestDetails); boolean exceedsSyncLimit = numberOfRefs > theBatchSize; if (exceedsSyncLimit) { ourLog.info( - "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", - numberOfRefs, - theBatchSize); + "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", + numberOfRefs, + theBatchSize); } return exceedsSyncLimit; } private void doMergeSync( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId partitionId) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId partitionId) { ReplaceReferencesRequest replaceReferencesRequest = new ReplaceReferencesRequest( - theSourceResource.getIdElement(), - theTargetResource.getIdElement(), - theMergeOperationParameters.getBatchSize(), - partitionId); + theSourceResource.getIdElement(), + theTargetResource.getIdElement(), + theMergeOperationParameters.getBatchSize(), + partitionId); // We don't want replace references to flip to async mode once we've already made the decision to go sync here. replaceReferencesRequest.setForceSync(true); @@ -232,12 +235,12 @@ private void doMergeSync( myReplaceReferencesSvc.replaceReferences(replaceReferencesRequest, theRequestDetails); Patient updatedTarget = myMergeResourceHelper.updateMergedResourcesAfterReferencesReplaced( - myHapiTransactionService, - theSourceResource, - theTargetResource, - (Patient) theMergeOperationParameters.getResultResource(), - theMergeOperationParameters.getDeleteSource(), - theRequestDetails); + myHapiTransactionService, + theSourceResource, + theTargetResource, + (Patient) theMergeOperationParameters.getResultResource(), + theMergeOperationParameters.getDeleteSource(), + theRequestDetails); theMergeOutcome.setUpdatedTargetResource(updatedTarget); String detailsText = "Merge operation completed successfully."; @@ -245,17 +248,18 @@ private void doMergeSync( } private void doMergeAsync( - BaseMergeOperationInputParameters theMergeOperationParameters, - Patient theSourceResource, - Patient theTargetResource, - RequestDetails theRequestDetails, - MergeOperationOutcome theMergeOutcome, - RequestPartitionId thePartitionId) { + BaseMergeOperationInputParameters theMergeOperationParameters, + Patient theSourceResource, + Patient theTargetResource, + RequestDetails theRequestDetails, + MergeOperationOutcome theMergeOutcome, + RequestPartitionId thePartitionId) { - MergeJobParameters mergeJobParameters = theMergeOperationParameters.asMergeJobParameters(myFhirContext, theSourceResource, theTargetResource, thePartitionId); + MergeJobParameters mergeJobParameters = theMergeOperationParameters.asMergeJobParameters( + myFhirContext, theSourceResource, theTargetResource, thePartitionId); Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( - myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); + myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); task.getMeta().setVersionId(null); @@ -263,14 +267,14 @@ private void doMergeAsync( theMergeOutcome.setHttpStatusCode(STATUS_HTTP_202_ACCEPTED); String detailsText = "Merge request is accepted, and will be processed asynchronously. See" - + " task resource returned in this response for details."; + + " task resource returned in this response for details."; addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); } private void addInfoToOperationOutcome( - IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { + IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { IBase issue = - OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); + OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); OperationOutcomeUtil.addDetailsToIssue(myFhirContext, issue, null, null, theDetailsText); } } diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/config/BaseSubscriptionSettings.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/config/BaseSubscriptionSettings.java index 4d22cd7bdcbd..d27142d8a93c 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/config/BaseSubscriptionSettings.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/config/BaseSubscriptionSettings.java @@ -55,12 +55,12 @@ public abstract class BaseSubscriptionSettings { * If this is enabled (default is {@literal false}), changes to Subscription resource would be put on queue immediately. * Reducing delay between creation of the Subscription and Activation. * - * @since 7.8.0 + * @since 8.0.0 */ private boolean mySubscriptionChangeQueuedImmediately = false; /** - * @since 7.8.0 + * @since 8.0.0 * * Regex To perform validation on the endpoint URL for Subscription of type RESTHOOK. */ @@ -289,7 +289,7 @@ public boolean hasRestHookEndpointUrlValidationRegex() { * If this is enabled (default is {@literal false}), changes to Subscription resource would be put on queue immediately. * Reducing delay between creation of the Subscription and Activation. * - * @since 7.8.0 + * @since 8.0.0 */ public boolean isSubscriptionChangeQueuedImmediately() { return mySubscriptionChangeQueuedImmediately; @@ -299,7 +299,7 @@ public boolean isSubscriptionChangeQueuedImmediately() { * If this is enabled (default is {@literal false}), changes to Subscription resource would be put on queue immediately. * Reducing delay between creation of the Subscription and Activation. * - * @since 7.8.0 + * @since 8.0.0 */ public void setSubscriptionChangeQueuedImmediately(boolean theSubscriptionChangeQueuedImmediately) { mySubscriptionChangeQueuedImmediately = theSubscriptionChangeQueuedImmediately; diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java index 9c967fd43f27..fa858efee46f 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java @@ -123,7 +123,7 @@ public class JpaStorageSettings extends StorageSettings { /** * If we are batching write operations in transactions, what should the maximum number of write operations per * transaction be? - * @since 7.8.0 + * @since 8.0.0 */ public static final String DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING = "10000"; @@ -133,7 +133,7 @@ public class JpaStorageSettings extends StorageSettings { /** * If we are batching write operations in transactions, what should the default number of write operations per * transaction be? - * @since 7.8.0 + * @since 8.0.0 */ public static final String DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE_STRING = "512"; @@ -418,14 +418,14 @@ public class JpaStorageSettings extends StorageSettings { /** * If we are batching write operations in transactions, what should the maximum number of write operations per * transaction be? - * @since 7.8.0 + * @since 8.0.0 */ private int myMaxTransactionEntriesForWrite = DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE; /** * If we are batching write operations in transactions, what should the default number of write operations per * transaction be? - * @since 7.8.0 + * @since 8.0.0 */ private int myDefaultTransactionEntriesForWrite = DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE; @@ -2690,7 +2690,7 @@ public void setRestDeleteByUrlResourceIdThreshold(long theRestDeleteByUrlResourc /** * If we are batching write operations in transactions, what should the maximum number of write operations per * transaction be? - * @since 7.8.0 + * @since 8.0.0 */ public int getMaxTransactionEntriesForWrite() { return myMaxTransactionEntriesForWrite; @@ -2699,7 +2699,7 @@ public int getMaxTransactionEntriesForWrite() { /** * If we are batching write operations in transactions, what should the maximum number of write operations per * transaction be? - * @since 7.8.0 + * @since 8.0.0 */ public void setMaxTransactionEntriesForWrite(int theMaxTransactionEntriesForWrite) { myMaxTransactionEntriesForWrite = theMaxTransactionEntriesForWrite; @@ -2708,7 +2708,7 @@ public void setMaxTransactionEntriesForWrite(int theMaxTransactionEntriesForWrit /** * If we are batching write operations in transactions, what should the default number of write operations per * transaction be? - * @since 7.8.0 + * @since 8.0.0 */ public int getDefaultTransactionEntriesForWrite() { return myDefaultTransactionEntriesForWrite; @@ -2717,7 +2717,7 @@ public int getDefaultTransactionEntriesForWrite() { /** * If we are batching write operations in transactions, what should the default number of write operations per * transaction be? - * @since 7.8.0 + * @since 8.0.0 */ public void setDefaultTransactionEntriesForWrite(int theDefaultTransactionEntriesForWrite) { myDefaultTransactionEntriesForWrite = theDefaultTransactionEntriesForWrite; From df5e73321bc9216feeeca70b5e8e01dbf3b26004 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 14:30:29 -0500 Subject: [PATCH 128/148] review feedback --- .../fhir/replacereferences/ReplaceReferencesRequest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java index d21ea598ef52..2bc04dc28eb8 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java @@ -30,9 +30,15 @@ import static org.apache.commons.lang3.StringUtils.isBlank; public class ReplaceReferencesRequest { + /** + * Unqualified source id + */ @Nonnull public final IIdType sourceId; + /** + * Unqualified target id + */ @Nonnull public final IIdType targetId; From 20707ac75c2727d4756b610f8ce4c57340ee2655 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 15:32:12 -0500 Subject: [PATCH 129/148] move $merge into JPA R4 --- .../ca/uhn/fhir/jpa/config/JpaConfig.java | 17 -- .../uhn/fhir/jpa/config/r4/JpaR4Config.java | 30 ++++ .../BaseJpaResourceProviderPatient.java | 122 ------------- .../provider/merge/PatientMergeProvider.java | 162 ++++++++++++++++++ .../provider/BaseResourceProviderR4Test.java | 2 + .../ca/uhn/fhir/jpa/test/BaseJpaR4Test.java | 6 +- .../batch2/jobs/config/Batch2JobsConfig.java | 4 +- 7 files changed, 200 insertions(+), 143 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index 1272aaed7d83..f9178c5d6946 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -960,21 +960,4 @@ public IReplaceReferencesSvc replaceReferencesSvc( public ReplaceReferencesPatchBundleSvc replaceReferencesPatchBundleSvc(DaoRegistry theDaoRegistry) { return new ReplaceReferencesPatchBundleSvc(theDaoRegistry); } - - @Bean - public ResourceMergeService resourceMergeService( - DaoRegistry theDaoRegistry, - IReplaceReferencesSvc theReplaceReferencesSvc, - HapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator, - Batch2TaskHelper theBatch2TaskHelper) { - return new ResourceMergeService( - theDaoRegistry, - theReplaceReferencesSvc, - theHapiTransactionService, - theRequestPartitionHelperSvc, - theJobCoordinator, - theBatch2TaskHelper); - } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java index 58f8197c0758..f2ef8929e9bc 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java @@ -19,17 +19,25 @@ */ package ca.uhn.fhir.jpa.config.r4; +import ca.uhn.fhir.batch2.api.IJobCoordinator; +import ca.uhn.fhir.batch2.util.Batch2TaskHelper; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.jpa.api.IDaoRegistry; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.config.GeneratedDaoAndResourceProviderConfigR4; import ca.uhn.fhir.jpa.config.JpaConfig; import ca.uhn.fhir.jpa.dao.ITransactionProcessorVersionAdapter; import ca.uhn.fhir.jpa.dao.r4.TransactionProcessorVersionAdapterR4; +import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.graphql.GraphQLProvider; import ca.uhn.fhir.jpa.graphql.GraphQLProviderWithIntrospection; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; +import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; import ca.uhn.fhir.jpa.provider.JpaSystemProvider; +import ca.uhn.fhir.jpa.provider.merge.PatientMergeProvider; +import ca.uhn.fhir.jpa.provider.merge.ResourceMergeService; import ca.uhn.fhir.jpa.term.TermLoaderSvcImpl; import ca.uhn.fhir.jpa.term.TermVersionAdapterSvcR4; import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc; @@ -96,4 +104,26 @@ public ITermLoaderSvc termLoaderService( ITermDeferredStorageSvc theDeferredStorageSvc, ITermCodeSystemStorageSvc theCodeSystemStorageSvc) { return new TermLoaderSvcImpl(theDeferredStorageSvc, theCodeSystemStorageSvc); } + + @Bean + public ResourceMergeService resourceMergeService( + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + HapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper) { + return new ResourceMergeService( + theDaoRegistry, + theReplaceReferencesSvc, + theHapiTransactionService, + theRequestPartitionHelperSvc, + theJobCoordinator, + theBatch2TaskHelper); + } + + @Bean + public PatientMergeProvider patientMergeProvider(FhirContext theFhirContext, ResourceMergeService theResourceMergeService) { + return new PatientMergeProvider(theFhirContext, theResourceMergeService); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index d140b17d6242..f4b6fe689b62 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -68,8 +68,6 @@ public abstract class BaseJpaResourceProviderPatient extends BaseJpaResourceProvider { - @Autowired - private ResourceMergeService myResourceMergeService; @Autowired private FhirContext myFhirContext; @@ -263,126 +261,6 @@ public IBundleProvider patientTypeEverything( } } - /** - * /Patient/$merge - */ - @Operation( - name = ProviderConstants.OPERATION_MERGE, - canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge") - public IBaseParameters patientMerge( - HttpServletRequest theServletRequest, - HttpServletResponse theServletResponse, - ServletRequestDetails theRequestDetails, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT_IDENTIFIER) - List theSourcePatientIdentifier, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT_IDENTIFIER) - List theTargetPatientIdentifier, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT, max = 1) - IBaseReference theSourcePatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT, max = 1) - IBaseReference theTargetPatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_PREVIEW, typeName = "boolean", max = 1) - IPrimitiveType thePreview, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_DELETE_SOURCE, typeName = "boolean", max = 1) - IPrimitiveType theDeleteSource, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_RESULT_PATIENT, max = 1) - IBaseResource theResultPatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_BATCH_SIZE, typeName = "unsignedInt") - IPrimitiveType theBatchSize) { - - startRequest(theServletRequest); - - try { - int batchSize = myStorageSettings.getTransactionWriteBatchSizeFromOperationParameter(theBatchSize); - - BaseMergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( - theSourcePatientIdentifier, - theTargetPatientIdentifier, - theSourcePatient, - theTargetPatient, - thePreview, - theDeleteSource, - theResultPatient, - batchSize); - - MergeOperationOutcome mergeOutcome = - myResourceMergeService.merge(mergeOperationParameters, theRequestDetails); - - theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); - return buildMergeOperationOutputParameters(myFhirContext, mergeOutcome, theRequestDetails.getResource()); - } finally { - endRequest(theServletRequest); - } - } - - private IBaseParameters buildMergeOperationOutputParameters( - FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome, IBaseResource theInputParameters) { - - IBaseParameters retVal = ParametersUtil.newInstance(theFhirContext); - ParametersUtil.addParameterToParameters( - theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT, theInputParameters); - - ParametersUtil.addParameterToParameters( - theFhirContext, - retVal, - ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME, - theMergeOutcome.getOperationOutcome()); - - if (theMergeOutcome.getUpdatedTargetResource() != null) { - ParametersUtil.addParameterToParameters( - theFhirContext, - retVal, - OPERATION_MERGE_OUTPUT_PARAM_RESULT, - theMergeOutcome.getUpdatedTargetResource()); - } - - if (theMergeOutcome.getTask() != null) { - ParametersUtil.addParameterToParameters( - theFhirContext, - retVal, - ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK, - theMergeOutcome.getTask()); - } - return retVal; - } - - private BaseMergeOperationInputParameters buildMergeOperationInputParameters( - List theSourcePatientIdentifier, - List theTargetPatientIdentifier, - IBaseReference theSourcePatient, - IBaseReference theTargetPatient, - IPrimitiveType thePreview, - IPrimitiveType theDeleteSource, - IBaseResource theResultPatient, - int theBatchSize) { - BaseMergeOperationInputParameters mergeOperationParameters = - new PatientMergeOperationInputParameters(theBatchSize); - if (theSourcePatientIdentifier != null) { - List sourceResourceIdentifiers = theSourcePatientIdentifier.stream() - .map(CanonicalIdentifier::fromIdentifier) - .collect(Collectors.toList()); - mergeOperationParameters.setSourceResourceIdentifiers(sourceResourceIdentifiers); - } - if (theTargetPatientIdentifier != null) { - List targetResourceIdentifiers = theTargetPatientIdentifier.stream() - .map(CanonicalIdentifier::fromIdentifier) - .collect(Collectors.toList()); - mergeOperationParameters.setTargetResourceIdentifiers(targetResourceIdentifiers); - } - mergeOperationParameters.setSourceResource(theSourcePatient); - mergeOperationParameters.setTargetResource(theTargetPatient); - mergeOperationParameters.setPreview(thePreview != null && thePreview.getValue()); - mergeOperationParameters.setDeleteSource(theDeleteSource != null && theDeleteSource.getValue()); - - if (theResultPatient != null) { - // pass in a copy of the result patient as we don't want it to be modified. It will be - // returned back to the client as part of the response. - mergeOperationParameters.setResultResource(((Patient) theResultPatient).copy()); - } - - return mergeOperationParameters; - } - /** * Given a list of string types, return only the ID portions of any parameters passed in. */ diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java new file mode 100644 index 000000000000..35c15a5a061f --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java @@ -0,0 +1,162 @@ +package ca.uhn.fhir.jpa.provider.merge; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.jpa.provider.BaseJpaResourceProvider; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.server.provider.ProviderConstants; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.util.CanonicalIdentifier; +import ca.uhn.fhir.util.ParametersUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Patient; + +import java.util.List; +import java.util.stream.Collectors; + +import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; + +public class PatientMergeProvider extends BaseJpaResourceProvider { + + private final FhirContext myFhirContext; + private final ResourceMergeService myResourceMergeService; + + public PatientMergeProvider(FhirContext theFhirContext, ResourceMergeService theResourceMergeService) { + myFhirContext = theFhirContext; + assert myFhirContext.getVersion().getVersion() == FhirVersionEnum.R4; + myResourceMergeService = theResourceMergeService; + } + + @Override + public Class getResourceType() { + return Patient.class; + } + + /** + * /Patient/$merge + */ + @Operation( + name = ProviderConstants.OPERATION_MERGE, + canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge") + public IBaseParameters patientMerge( + HttpServletRequest theServletRequest, + HttpServletResponse theServletResponse, + ServletRequestDetails theRequestDetails, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT_IDENTIFIER) + List theSourcePatientIdentifier, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT_IDENTIFIER) + List theTargetPatientIdentifier, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT, max = 1) + IBaseReference theSourcePatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT, max = 1) + IBaseReference theTargetPatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_PREVIEW, typeName = "boolean", max = 1) + IPrimitiveType thePreview, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_DELETE_SOURCE, typeName = "boolean", max = 1) + IPrimitiveType theDeleteSource, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_RESULT_PATIENT, max = 1) + IBaseResource theResultPatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_BATCH_SIZE, typeName = "unsignedInt") + IPrimitiveType theBatchSize) { + + startRequest(theServletRequest); + + try { + int batchSize = myStorageSettings.getTransactionWriteBatchSizeFromOperationParameter(theBatchSize); + + BaseMergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( + theSourcePatientIdentifier, + theTargetPatientIdentifier, + theSourcePatient, + theTargetPatient, + thePreview, + theDeleteSource, + theResultPatient, + batchSize); + + MergeOperationOutcome mergeOutcome = + myResourceMergeService.merge(mergeOperationParameters, theRequestDetails); + + theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); + return buildMergeOperationOutputParameters(myFhirContext, mergeOutcome, theRequestDetails.getResource()); + } finally { + endRequest(theServletRequest); + } + } + + private IBaseParameters buildMergeOperationOutputParameters( + FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome, IBaseResource theInputParameters) { + + IBaseParameters retVal = ParametersUtil.newInstance(theFhirContext); + ParametersUtil.addParameterToParameters( + theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT, theInputParameters); + + ParametersUtil.addParameterToParameters( + theFhirContext, + retVal, + ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME, + theMergeOutcome.getOperationOutcome()); + + if (theMergeOutcome.getUpdatedTargetResource() != null) { + ParametersUtil.addParameterToParameters( + theFhirContext, + retVal, + OPERATION_MERGE_OUTPUT_PARAM_RESULT, + theMergeOutcome.getUpdatedTargetResource()); + } + + if (theMergeOutcome.getTask() != null) { + ParametersUtil.addParameterToParameters( + theFhirContext, + retVal, + ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK, + theMergeOutcome.getTask()); + } + return retVal; + } + + private BaseMergeOperationInputParameters buildMergeOperationInputParameters( + List theSourcePatientIdentifier, + List theTargetPatientIdentifier, + IBaseReference theSourcePatient, + IBaseReference theTargetPatient, + IPrimitiveType thePreview, + IPrimitiveType theDeleteSource, + IBaseResource theResultPatient, + int theBatchSize) { + BaseMergeOperationInputParameters mergeOperationParameters = + new PatientMergeOperationInputParameters(theBatchSize); + if (theSourcePatientIdentifier != null) { + List sourceResourceIdentifiers = theSourcePatientIdentifier.stream() + .map(CanonicalIdentifier::fromIdentifier) + .collect(Collectors.toList()); + mergeOperationParameters.setSourceResourceIdentifiers(sourceResourceIdentifiers); + } + if (theTargetPatientIdentifier != null) { + List targetResourceIdentifiers = theTargetPatientIdentifier.stream() + .map(CanonicalIdentifier::fromIdentifier) + .collect(Collectors.toList()); + mergeOperationParameters.setTargetResourceIdentifiers(targetResourceIdentifiers); + } + mergeOperationParameters.setSourceResource(theSourcePatient); + mergeOperationParameters.setTargetResource(theTargetPatient); + mergeOperationParameters.setPreview(thePreview != null && thePreview.getValue()); + mergeOperationParameters.setDeleteSource(theDeleteSource != null && theDeleteSource.getValue()); + + if (theResultPatient != null) { + // pass in a copy of the result patient as we don't want it to be modified. It will be + // returned back to the client as part of the response. + mergeOperationParameters.setResultResource(((Patient) theResultPatient).copy()); + } + + return mergeOperationParameters; + } + +} diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java index 81a0dabc77ee..523f3ec80547 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderR4Test.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.data.IPartitionDao; import ca.uhn.fhir.jpa.graphql.GraphQLProvider; +import ca.uhn.fhir.jpa.provider.merge.PatientMergeProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionLoader; import ca.uhn.fhir.jpa.test.BaseJpaR4Test; @@ -99,6 +100,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test { s.registerProvider(myAppCtx.getBean(SubscriptionTriggeringProvider.class)); s.registerProvider(myAppCtx.getBean(TerminologyUploaderProvider.class)); s.registerProvider(myAppCtx.getBean(ValueSetOperationProvider.class)); + s.registerProvider(myAppCtx.getBean(PatientMergeProvider.class)); s.setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class)); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java index 2b3dc840048d..894bae0815fc 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaR4Test.java @@ -22,6 +22,8 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.api.IJobMaintenanceService; import ca.uhn.fhir.batch2.jobs.export.BulkDataExportProvider; +import ca.uhn.fhir.batch2.jobs.merge.MergeAppCtx; +import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.interceptor.api.IInterceptorService; @@ -224,7 +226,9 @@ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = { - TestR4Config.class + TestR4Config.class, + ReplaceReferencesAppCtx.class, // Batch job + MergeAppCtx.class // Batch job }) public abstract class BaseJpaR4Test extends BaseJpaTest implements ITestDataBuilder { public static final String MY_VALUE_SET = "my-value-set"; diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java index 3ae549ff96af..0272e94259b2 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java @@ -39,8 +39,6 @@ DeleteExpungeAppCtx.class, BulkExportAppCtx.class, TermCodeSystemJobConfig.class, - BulkImportPullConfig.class, - ReplaceReferencesAppCtx.class, - MergeAppCtx.class, + BulkImportPullConfig.class }) public class Batch2JobsConfig {} From da2530ebc9cbe4c6a747467f404ddc03fd850bd6 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 15:34:32 -0500 Subject: [PATCH 130/148] move $merge into JPA R4 --- .../ca/uhn/fhir/jpa/config/JpaConfig.java | 1 - .../uhn/fhir/jpa/config/r4/JpaR4Config.java | 27 ++-- .../BaseJpaResourceProviderPatient.java | 17 --- .../provider/merge/PatientMergeProvider.java | 115 +++++++++--------- .../batch2/jobs/config/Batch2JobsConfig.java | 2 - 5 files changed, 71 insertions(+), 91 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index f9178c5d6946..66e541c77622 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -108,7 +108,6 @@ import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider; import ca.uhn.fhir.jpa.provider.ValueSetOperationProvider; import ca.uhn.fhir.jpa.provider.ValueSetOperationProviderDstu2; -import ca.uhn.fhir.jpa.provider.merge.ResourceMergeService; import ca.uhn.fhir.jpa.sched.AutowiringSpringBeanJobFactory; import ca.uhn.fhir.jpa.sched.HapiSchedulerServiceImpl; import ca.uhn.fhir.jpa.search.ISynchronousSearchSvc; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java index f2ef8929e9bc..08efc7d611f4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java @@ -107,23 +107,24 @@ public ITermLoaderSvc termLoaderService( @Bean public ResourceMergeService resourceMergeService( - DaoRegistry theDaoRegistry, - IReplaceReferencesSvc theReplaceReferencesSvc, - HapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator, - Batch2TaskHelper theBatch2TaskHelper) { + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + HapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper) { return new ResourceMergeService( - theDaoRegistry, - theReplaceReferencesSvc, - theHapiTransactionService, - theRequestPartitionHelperSvc, - theJobCoordinator, - theBatch2TaskHelper); + theDaoRegistry, + theReplaceReferencesSvc, + theHapiTransactionService, + theRequestPartitionHelperSvc, + theJobCoordinator, + theBatch2TaskHelper); } @Bean - public PatientMergeProvider patientMergeProvider(FhirContext theFhirContext, ResourceMergeService theResourceMergeService) { + public PatientMergeProvider patientMergeProvider( + FhirContext theFhirContext, ResourceMergeService theResourceMergeService) { return new PatientMergeProvider(theFhirContext, theResourceMergeService); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java index f4b6fe689b62..136eee4120fe 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProviderPatient.java @@ -23,10 +23,6 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.dao.PatientEverythingParameters; import ca.uhn.fhir.jpa.model.util.JpaConstants; -import ca.uhn.fhir.jpa.provider.merge.BaseMergeOperationInputParameters; -import ca.uhn.fhir.jpa.provider.merge.MergeOperationOutcome; -import ca.uhn.fhir.jpa.provider.merge.PatientMergeOperationInputParameters; -import ca.uhn.fhir.jpa.provider.merge.ResourceMergeService; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.valueset.BundleTypeEnum; @@ -44,31 +40,18 @@ import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenOrListParam; import ca.uhn.fhir.rest.param.TokenParam; -import ca.uhn.fhir.rest.server.provider.ProviderConstants; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import ca.uhn.fhir.util.CanonicalIdentifier; -import ca.uhn.fhir.util.ParametersUtil; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.hl7.fhir.instance.model.api.IBaseParameters; -import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r4.model.Identifier; -import org.hl7.fhir.r4.model.Patient; import org.springframework.beans.factory.annotation.Autowired; import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; -import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; import static org.apache.commons.lang3.StringUtils.isNotBlank; public abstract class BaseJpaResourceProviderPatient extends BaseJpaResourceProvider { - @Autowired private FhirContext myFhirContext; /** diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java index 35c15a5a061f..43a6fac276bd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java @@ -43,28 +43,28 @@ public Class getResourceType() { * /Patient/$merge */ @Operation( - name = ProviderConstants.OPERATION_MERGE, - canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge") + name = ProviderConstants.OPERATION_MERGE, + canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge") public IBaseParameters patientMerge( - HttpServletRequest theServletRequest, - HttpServletResponse theServletResponse, - ServletRequestDetails theRequestDetails, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT_IDENTIFIER) - List theSourcePatientIdentifier, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT_IDENTIFIER) - List theTargetPatientIdentifier, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT, max = 1) - IBaseReference theSourcePatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT, max = 1) - IBaseReference theTargetPatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_PREVIEW, typeName = "boolean", max = 1) - IPrimitiveType thePreview, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_DELETE_SOURCE, typeName = "boolean", max = 1) - IPrimitiveType theDeleteSource, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_RESULT_PATIENT, max = 1) - IBaseResource theResultPatient, - @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_BATCH_SIZE, typeName = "unsignedInt") - IPrimitiveType theBatchSize) { + HttpServletRequest theServletRequest, + HttpServletResponse theServletResponse, + ServletRequestDetails theRequestDetails, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT_IDENTIFIER) + List theSourcePatientIdentifier, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT_IDENTIFIER) + List theTargetPatientIdentifier, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT, max = 1) + IBaseReference theSourcePatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT, max = 1) + IBaseReference theTargetPatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_PREVIEW, typeName = "boolean", max = 1) + IPrimitiveType thePreview, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_DELETE_SOURCE, typeName = "boolean", max = 1) + IPrimitiveType theDeleteSource, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_RESULT_PATIENT, max = 1) + IBaseResource theResultPatient, + @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_BATCH_SIZE, typeName = "unsignedInt") + IPrimitiveType theBatchSize) { startRequest(theServletRequest); @@ -72,17 +72,17 @@ public IBaseParameters patientMerge( int batchSize = myStorageSettings.getTransactionWriteBatchSizeFromOperationParameter(theBatchSize); BaseMergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( - theSourcePatientIdentifier, - theTargetPatientIdentifier, - theSourcePatient, - theTargetPatient, - thePreview, - theDeleteSource, - theResultPatient, - batchSize); + theSourcePatientIdentifier, + theTargetPatientIdentifier, + theSourcePatient, + theTargetPatient, + thePreview, + theDeleteSource, + theResultPatient, + batchSize); MergeOperationOutcome mergeOutcome = - myResourceMergeService.merge(mergeOperationParameters, theRequestDetails); + myResourceMergeService.merge(mergeOperationParameters, theRequestDetails); theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); return buildMergeOperationOutputParameters(myFhirContext, mergeOutcome, theRequestDetails.getResource()); @@ -92,57 +92,57 @@ public IBaseParameters patientMerge( } private IBaseParameters buildMergeOperationOutputParameters( - FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome, IBaseResource theInputParameters) { + FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome, IBaseResource theInputParameters) { IBaseParameters retVal = ParametersUtil.newInstance(theFhirContext); ParametersUtil.addParameterToParameters( - theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT, theInputParameters); + theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT, theInputParameters); ParametersUtil.addParameterToParameters( - theFhirContext, - retVal, - ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME, - theMergeOutcome.getOperationOutcome()); + theFhirContext, + retVal, + ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME, + theMergeOutcome.getOperationOutcome()); if (theMergeOutcome.getUpdatedTargetResource() != null) { ParametersUtil.addParameterToParameters( - theFhirContext, - retVal, - OPERATION_MERGE_OUTPUT_PARAM_RESULT, - theMergeOutcome.getUpdatedTargetResource()); + theFhirContext, + retVal, + OPERATION_MERGE_OUTPUT_PARAM_RESULT, + theMergeOutcome.getUpdatedTargetResource()); } if (theMergeOutcome.getTask() != null) { ParametersUtil.addParameterToParameters( - theFhirContext, - retVal, - ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK, - theMergeOutcome.getTask()); + theFhirContext, + retVal, + ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK, + theMergeOutcome.getTask()); } return retVal; } private BaseMergeOperationInputParameters buildMergeOperationInputParameters( - List theSourcePatientIdentifier, - List theTargetPatientIdentifier, - IBaseReference theSourcePatient, - IBaseReference theTargetPatient, - IPrimitiveType thePreview, - IPrimitiveType theDeleteSource, - IBaseResource theResultPatient, - int theBatchSize) { + List theSourcePatientIdentifier, + List theTargetPatientIdentifier, + IBaseReference theSourcePatient, + IBaseReference theTargetPatient, + IPrimitiveType thePreview, + IPrimitiveType theDeleteSource, + IBaseResource theResultPatient, + int theBatchSize) { BaseMergeOperationInputParameters mergeOperationParameters = - new PatientMergeOperationInputParameters(theBatchSize); + new PatientMergeOperationInputParameters(theBatchSize); if (theSourcePatientIdentifier != null) { List sourceResourceIdentifiers = theSourcePatientIdentifier.stream() - .map(CanonicalIdentifier::fromIdentifier) - .collect(Collectors.toList()); + .map(CanonicalIdentifier::fromIdentifier) + .collect(Collectors.toList()); mergeOperationParameters.setSourceResourceIdentifiers(sourceResourceIdentifiers); } if (theTargetPatientIdentifier != null) { List targetResourceIdentifiers = theTargetPatientIdentifier.stream() - .map(CanonicalIdentifier::fromIdentifier) - .collect(Collectors.toList()); + .map(CanonicalIdentifier::fromIdentifier) + .collect(Collectors.toList()); mergeOperationParameters.setTargetResourceIdentifiers(targetResourceIdentifiers); } mergeOperationParameters.setSourceResource(theSourcePatient); @@ -158,5 +158,4 @@ private BaseMergeOperationInputParameters buildMergeOperationInputParameters( return mergeOperationParameters; } - } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java index 0272e94259b2..5e94aca6bb71 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/config/Batch2JobsConfig.java @@ -23,9 +23,7 @@ import ca.uhn.fhir.batch2.jobs.expunge.DeleteExpungeAppCtx; import ca.uhn.fhir.batch2.jobs.importpull.BulkImportPullConfig; import ca.uhn.fhir.batch2.jobs.imprt.BulkImportAppCtx; -import ca.uhn.fhir.batch2.jobs.merge.MergeAppCtx; import ca.uhn.fhir.batch2.jobs.reindex.ReindexAppCtx; -import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx; import ca.uhn.fhir.batch2.jobs.termcodesystem.TermCodeSystemJobConfig; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; From d1367cd4a391fefd0bc9351294952b77338f0e3c Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 19:07:37 -0500 Subject: [PATCH 131/148] still need to return 412 --- .../fhir/jpa/provider/JpaSystemProvider.java | 12 ++- .../provider/ReplaceReferencesSvcImpl.java | 31 +------- .../BaseMergeOperationInputParameters.java | 12 +-- .../PatientMergeOperationInputParameters.java | 4 +- .../provider/merge/PatientMergeProvider.java | 12 +-- .../provider/merge/ResourceMergeService.java | 15 ++-- .../merge/ResourceMergeServiceTest.java | 2 +- .../jpa/provider/r4/PatientMergeR4Test.java | 4 +- .../provider/r4/ReplaceReferencesR4Test.java | 4 +- .../ReplaceReferencesTestHelper.java | 16 ++-- .../server/provider/ProviderConstants.java | 11 ++- .../jobs/merge/MergeResourceHelper.java | 73 +++++++++++-------- .../ReplaceReferencesJobParameters.java | 26 +++---- .../ReplaceReferencesQueryIdsStep.java | 2 +- .../jpa/api/config/JpaStorageSettings.java | 13 +--- .../ReplaceReferencesRequest.java | 15 +--- 16 files changed, 111 insertions(+), 141 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index 19a0399ff4a7..564a5d11fdc8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -19,6 +19,7 @@ */ package ca.uhn.fhir.jpa.provider; +import ca.uhn.fhir.batch2.jobs.merge.MergeResourceHelper; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; import ca.uhn.fhir.interceptor.model.RequestPartitionId; @@ -173,21 +174,24 @@ public IBaseParameters replaceReferences( min = 1, typeName = "string") IPrimitiveType theTargetId, - @OperationParam(name = ProviderConstants.OPERATION_REPLACE_REFERENCES_BATCH_SIZE, typeName = "unsignedInt") - IPrimitiveType theBatchSize, + @OperationParam( + name = ProviderConstants.OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT, + typeName = "unsignedInt") + IPrimitiveType theResourceLimit, ServletRequestDetails theServletRequest) { startRequest(theServletRequest); try { validateReplaceReferencesParams(theSourceId.getValue(), theTargetId.getValue()); - int batchSize = myStorageSettings.getTransactionWriteBatchSizeFromOperationParameter(theBatchSize); + + int resourceLimit = MergeResourceHelper.setResourceLimitFromParameter(myStorageSettings, theResourceLimit); IdDt sourceId = new IdDt(theSourceId.getValue()); IdDt targetId = new IdDt(theTargetId.getValue()); RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( theServletRequest, ReadPartitionIdRequestDetails.forRead(targetId)); ReplaceReferencesRequest replaceReferencesRequest = - new ReplaceReferencesRequest(sourceId, targetId, batchSize, partitionId); + new ReplaceReferencesRequest(sourceId, targetId, resourceLimit, partitionId); IBaseParameters retval = getReplaceReferencesSvc().replaceReferences(replaceReferencesRequest, theServletRequest); if (ParametersUtil.getNamedParameter(getContext(), retval, OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 3d63504853f8..a52cbd4b7bd4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -79,8 +79,6 @@ public IBaseParameters replaceReferences( if (theRequestDetails.isPreferAsync()) { return replaceReferencesPreferAsync(theReplaceReferencesRequest, theRequestDetails); - } else if (theReplaceReferencesRequest.isForceSync()) { - return replaceReferencesForceSync(theReplaceReferencesRequest, theRequestDetails); } else { return replaceReferencesPreferSync(theReplaceReferencesRequest, theRequestDetails); } @@ -126,6 +124,7 @@ private IBaseParameters replaceReferencesPreferSync( .execute(() -> getAllPidsWithLimit(theReplaceReferencesRequest)); if (accumulator.isTruncated()) { + // FIXME KHS undo this? ourLog.warn("Too many results. Switching to asynchronous reference replacement."); return replaceReferencesPreferAsync(theReplaceReferencesRequest, theRequestDetails); } @@ -140,32 +139,6 @@ private IBaseParameters replaceReferencesPreferSync( return retval; } - /** - * Perform the operation synchronously. This should be only called if the number of resources to be - * updated is predetermined before calling, and it is small enough to handle synchronously. - */ - @Nonnull - private IBaseParameters replaceReferencesForceSync( - ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) { - - List allIds = myHapiTransactionService - .withRequest(theRequestDetails) - .execute(() -> myResourceLinkDao - .streamSourceIdsForTargetFhirId( - theReplaceReferencesRequest.sourceId.getResourceType(), - theReplaceReferencesRequest.sourceId.getIdPart()) - .collect(Collectors.toList())); - - Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources( - theReplaceReferencesRequest, allIds, theRequestDetails); - - Parameters retval = new Parameters(); - retval.addParameter() - .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME) - .setResource(result); - return retval; - } - private @Nonnull StopLimitAccumulator getAllPidsWithLimit( ReplaceReferencesRequest theReplaceReferencesRequest) { @@ -173,7 +146,7 @@ private IBaseParameters replaceReferencesForceSync( theReplaceReferencesRequest.sourceId.getResourceType(), theReplaceReferencesRequest.sourceId.getIdPart()); StopLimitAccumulator accumulator = - StopLimitAccumulator.fromStreamAndLimit(idStream, theReplaceReferencesRequest.batchSize); + StopLimitAccumulator.fromStreamAndLimit(idStream, theReplaceReferencesRequest.resourceLimit); return accumulator; } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java index 2496395a1f1e..799ef6314cb2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java @@ -42,10 +42,10 @@ public abstract class BaseMergeOperationInputParameters { private boolean myPreview; private boolean myDeleteSource; private IBaseResource myResultResource; - private final int myBatchSize; + private final int myResourceLimit; - protected BaseMergeOperationInputParameters(int theBatchSize) { - myBatchSize = theBatchSize; + protected BaseMergeOperationInputParameters(int theResourceLimit) { + myResourceLimit = theResourceLimit; } public abstract String getSourceResourceParameterName(); @@ -122,8 +122,8 @@ public void setTargetResource(IBaseReference theTargetResource) { this.myTargetResource = theTargetResource; } - public int getBatchSize() { - return myBatchSize; + public int getResourceLimit() { + return myResourceLimit; } public MergeJobParameters asMergeJobParameters( @@ -136,7 +136,7 @@ public MergeJobParameters asMergeJobParameters( retval.setResultResource(theFhirContext.newJsonParser().encodeResourceToString(getResultResource())); } retval.setDeleteSource(getDeleteSource()); - retval.setBatchSize(getBatchSize()); + retval.setResourceLimit(getResourceLimit()); retval.setSourceId(new FhirIdJson(theSourceResource.getIdElement().toVersionless())); retval.setTargetId(new FhirIdJson(theTargetResource.getIdElement().toVersionless())); retval.setPartitionId(thePartitionId); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java index dfd802433b8b..d1f98bc1d6cd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeOperationInputParameters.java @@ -29,8 +29,8 @@ * See Patient $merge spec */ public class PatientMergeOperationInputParameters extends BaseMergeOperationInputParameters { - public PatientMergeOperationInputParameters(int theBatchSize) { - super(theBatchSize); + public PatientMergeOperationInputParameters(int theResourceLimit) { + super(theResourceLimit); } @Override diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java index 43a6fac276bd..39942d4612ff 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.provider.merge; +import ca.uhn.fhir.batch2.jobs.merge.MergeResourceHelper; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.jpa.provider.BaseJpaResourceProvider; @@ -22,6 +23,7 @@ import java.util.stream.Collectors; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; public class PatientMergeProvider extends BaseJpaResourceProvider { @@ -64,12 +66,12 @@ public IBaseParameters patientMerge( @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_RESULT_PATIENT, max = 1) IBaseResource theResultPatient, @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_BATCH_SIZE, typeName = "unsignedInt") - IPrimitiveType theBatchSize) { + IPrimitiveType theResourceLimit) { startRequest(theServletRequest); try { - int batchSize = myStorageSettings.getTransactionWriteBatchSizeFromOperationParameter(theBatchSize); + int resourceLimit = MergeResourceHelper.setResourceLimitFromParameter(myStorageSettings, theResourceLimit); BaseMergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( theSourcePatientIdentifier, @@ -79,7 +81,7 @@ public IBaseParameters patientMerge( thePreview, theDeleteSource, theResultPatient, - batchSize); + resourceLimit); MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, theRequestDetails); @@ -130,9 +132,9 @@ private BaseMergeOperationInputParameters buildMergeOperationInputParameters( IPrimitiveType thePreview, IPrimitiveType theDeleteSource, IBaseResource theResultPatient, - int theBatchSize) { + int theResourceLimit) { BaseMergeOperationInputParameters mergeOperationParameters = - new PatientMergeOperationInputParameters(theBatchSize); + new PatientMergeOperationInputParameters(theResourceLimit); if (theSourcePatientIdentifier != null) { List sourceResourceIdentifiers = theSourcePatientIdentifier.stream() .map(CanonicalIdentifier::fromIdentifier) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 69c68d762b69..503f1c7ade41 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -180,9 +180,7 @@ private void doMerge( RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); - if (theRequestDetails.isPreferAsync() - || referenceCountExceedsSyncLimit( - theSourceResource, theRequestDetails, theMergeOperationParameters.getBatchSize())) { + if (theRequestDetails.isPreferAsync()) { doMergeAsync( theMergeOperationParameters, theSourceResource, @@ -202,15 +200,15 @@ private void doMerge( } private boolean referenceCountExceedsSyncLimit( - Patient theSourceResource, RequestDetails theRequestDetails, Integer theBatchSize) { + Patient theSourceResource, RequestDetails theRequestDetails, Integer theResourceLimit) { Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( theSourceResource.getIdElement().toVersionless(), theRequestDetails); - boolean exceedsSyncLimit = numberOfRefs > theBatchSize; + boolean exceedsSyncLimit = numberOfRefs > theResourceLimit; if (exceedsSyncLimit) { ourLog.info( "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", numberOfRefs, - theBatchSize); + theResourceLimit); } return exceedsSyncLimit; } @@ -226,12 +224,9 @@ private void doMergeSync( ReplaceReferencesRequest replaceReferencesRequest = new ReplaceReferencesRequest( theSourceResource.getIdElement(), theTargetResource.getIdElement(), - theMergeOperationParameters.getBatchSize(), + theMergeOperationParameters.getResourceLimit(), partitionId); - // We don't want replace references to flip to async mode once we've already made the decision to go sync here. - replaceReferencesRequest.setForceSync(true); - myReplaceReferencesSvc.replaceReferences(replaceReferencesRequest, theRequestDetails); Patient updatedTarget = myMergeResourceHelper.updateMergedResourcesAfterReferencesReplaced( diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java index 7024d8a8990f..aba3b097905b 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java @@ -1439,7 +1439,7 @@ private void verifyBatch2JobTaskHelperMockInvocation(@Nullable Patient theResult assertThat(jobParametersCaptor.getValue()).isInstanceOf(MergeJobParameters.class); MergeJobParameters capturedJobParams = (MergeJobParameters) jobParametersCaptor.getValue(); - assertThat(capturedJobParams.getBatchSize()).isEqualTo(PAGE_SIZE); + assertThat(capturedJobParams.getResourceLimit()).isEqualTo(PAGE_SIZE); assertThat(capturedJobParams.getSourceId().toString()).isEqualTo(SOURCE_PATIENT_TEST_ID); assertThat(capturedJobParams.getTargetId().toString()).isEqualTo(TARGET_PATIENT_TEST_ID); assertThat(capturedJobParams.getDeleteSource()).isEqualTo(theDeleteSource); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 5d3217827a0e..28fcdaf788ea 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -234,7 +234,7 @@ void testMerge_SourceResourceCannotBeDeletedBecauseAnotherResourceReferencingSou //using a small batch size that would result in multiple chunks to ensure that //the job runs a bit slowly so that we have sometime to add a resource that references the source //after the first step - inParams.batchSize = 5; + inParams.resourceLimit = 5; Parameters inParameters = inParams.asParametersResource(); // exec @@ -342,8 +342,6 @@ private Parameters callMergeOperation(Parameters inParameters, boolean isAsync) .execute(); } - - class MyExceptionHandler implements TestExecutionExceptionHandler { @Override public void handleTestExecutionException(ExtensionContext theExtensionContext, Throwable theThrowable) throws Throwable { diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index 710cd61177d9..73413cd9a960 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -77,9 +77,9 @@ private JobInstance awaitJobCompletion(Task task) { @ParameterizedTest @ValueSource(booleans = {false, true}) - void testReplaceReferencesSmallBatchSize(boolean isAsync) { + void testReplaceReferencesSmallResourceLimit(boolean isAsync) { // exec - Parameters outParams = myTestHelper.callReplaceReferencesWithBatchSize(myClient, isAsync, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); + Parameters outParams = myTestHelper.callReplaceReferencesWithResourceLimit(myClient, isAsync, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); assertThat(getLastHttpStatusCode()).isEqualTo(HttpServletResponse.SC_ACCEPTED); diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java index d78859c5e970..794bdc9b6f79 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/replacereferences/ReplaceReferencesTestHelper.java @@ -229,11 +229,11 @@ public String getJobIdFromTask(Task task) { } public Parameters callReplaceReferences(IGenericClient theFhirClient, boolean theIsAsync) { - return callReplaceReferencesWithBatchSize(theFhirClient, theIsAsync, null); + return callReplaceReferencesWithResourceLimit(theFhirClient, theIsAsync, null); } - public Parameters callReplaceReferencesWithBatchSize( - IGenericClient theFhirClient, boolean theIsAsync, Integer theBatchSize) { + public Parameters callReplaceReferencesWithResourceLimit( + IGenericClient theFhirClient, boolean theIsAsync, Integer theResourceLimit) { IOperationUntypedWithInputAndPartialOutput request = theFhirClient .operation() .onServer() @@ -245,9 +245,9 @@ public Parameters callReplaceReferencesWithBatchSize( .andParameter( ProviderConstants.OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID, new StringType(myTargetPatientId.getValue())); - if (theBatchSize != null) { + if (theResourceLimit != null) { request.andParameter( - ProviderConstants.OPERATION_REPLACE_REFERENCES_BATCH_SIZE, new IntegerType(theBatchSize)); + ProviderConstants.OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT, new IntegerType(theResourceLimit)); } if (theIsAsync) { @@ -339,7 +339,7 @@ public static class PatientMergeInputParameters { public Patient resultPatient; public Boolean preview; public Boolean deleteSource; - public Integer batchSize; + public Integer resourceLimit; public Parameters asParametersResource() { Parameters inParams = new Parameters(); @@ -364,8 +364,8 @@ public Parameters asParametersResource() { if (deleteSource != null) { inParams.addParameter().setName("delete-source").setValue(new BooleanType(deleteSource)); } - if (batchSize != null) { - inParams.addParameter().setName("batch-size").setValue(new IntegerType(batchSize)); + if (resourceLimit != null) { + inParams.addParameter().setName("batch-size").setValue(new IntegerType(resourceLimit)); } return inParams; } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index c51059ebc50e..b0edc3d517b2 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -261,10 +261,12 @@ public class ProviderConstants { public static final String OPERATION_REPLACE_REFERENCES_PARAM_TARGET_REFERENCE_ID = "target-reference-id"; /** - * The number of resources that will be modified at a time. If the number of resources that need to change - * exceeds this amount, the operation will switch to async mode. + * If the request is being performed synchronously and the number of resources that need to change + * exceeds this amount, the operation will fail with 412 Precondition Failed. */ - public static final String OPERATION_REPLACE_REFERENCES_BATCH_SIZE = "batch-size"; + + // FIXME KHS change tests and var names + public static final String OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT = "resource-limit"; /** * $replace-references output Parameters names @@ -296,4 +298,7 @@ public class ProviderConstants { public static final String OPERATION_MERGE_OUTPUT_PARAM_TASK = OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; public static final String HAPI_BATCH_JOB_ID_SYSTEM = "http://hapifhir.io/batch/jobId"; + public static final String OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT_DEFAULT_STRING = "512"; + public static final Integer OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT_DEFAULT = + Integer.parseInt(OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT_DEFAULT_STRING); } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeResourceHelper.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeResourceHelper.java index 672d9d543c66..f841f117a11c 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeResourceHelper.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeResourceHelper.java @@ -19,12 +19,15 @@ */ package ca.uhn.fhir.batch2.jobs.merge; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.provider.ProviderConstants; 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.Identifier; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Reference; @@ -32,6 +35,8 @@ import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; + /** * This class contains code that is used to update source and target resources after the references are replaced. * This is the common functionality that is used in sync case and in the async case as the reduction step. @@ -44,37 +49,47 @@ public MergeResourceHelper(IFhirResourceDao theDao) { myPatientDao = theDao; } + public static int setResourceLimitFromParameter(JpaStorageSettings theStorageSettings, IPrimitiveType theResourceLimit) { + int retval = defaultIfNull( + IPrimitiveType.toValueOrNull(theResourceLimit), + ProviderConstants.OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT_DEFAULT); + if (retval > theStorageSettings.getMaxTransactionEntriesForWrite()) { + retval = theStorageSettings.getMaxTransactionEntriesForWrite(); + } + return retval; + } + public void updateMergedResourcesAfterReferencesReplaced( - IHapiTransactionService myHapiTransactionService, - IIdType theSourceResourceId, - IIdType theTargetResourceId, - @Nullable Patient theResultResource, - boolean theDeleteSource, - RequestDetails theRequestDetails) { + IHapiTransactionService myHapiTransactionService, + IIdType theSourceResourceId, + IIdType theTargetResourceId, + @Nullable Patient theResultResource, + boolean theDeleteSource, + RequestDetails theRequestDetails) { Patient sourceResource = myPatientDao.read(theSourceResourceId, theRequestDetails); Patient targetResource = myPatientDao.read(theTargetResourceId, theRequestDetails); updateMergedResourcesAfterReferencesReplaced( - myHapiTransactionService, - sourceResource, - targetResource, - theResultResource, - theDeleteSource, - theRequestDetails); + myHapiTransactionService, + sourceResource, + targetResource, + theResultResource, + theDeleteSource, + theRequestDetails); } public Patient updateMergedResourcesAfterReferencesReplaced( - IHapiTransactionService myHapiTransactionService, - Patient theSourceResource, - Patient theTargetResource, - @Nullable Patient theResultResource, - boolean theDeleteSource, - RequestDetails theRequestDetails) { + IHapiTransactionService myHapiTransactionService, + Patient theSourceResource, + Patient theTargetResource, + @Nullable Patient theResultResource, + boolean theDeleteSource, + RequestDetails theRequestDetails) { AtomicReference targetPatientAfterUpdate = new AtomicReference<>(); myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { Patient patientToUpdate = prepareTargetPatientForUpdate( - theTargetResource, theSourceResource, theResultResource, theDeleteSource); + theTargetResource, theSourceResource, theResultResource, theDeleteSource); targetPatientAfterUpdate.set(updateResource(patientToUpdate, theRequestDetails)); @@ -90,10 +105,10 @@ public Patient updateMergedResourcesAfterReferencesReplaced( } public Patient prepareTargetPatientForUpdate( - Patient theTargetResource, - Patient theSourceResource, - @Nullable Patient theResultResource, - boolean theDeleteSource) { + Patient theTargetResource, + Patient theSourceResource, + @Nullable Patient theResultResource, + boolean theDeleteSource) { // if the client provided a result resource as input then use it to update the target resource if (theResultResource != null) { @@ -104,9 +119,9 @@ public Patient prepareTargetPatientForUpdate( // add the replaces link to the target resource, if the source resource is not to be deleted if (!theDeleteSource) { theTargetResource - .addLink() - .setType(Patient.LinkType.REPLACES) - .setOther(new Reference(theSourceResource.getIdElement().toVersionless())); + .addLink() + .setType(Patient.LinkType.REPLACES) + .setOther(new Reference(theSourceResource.getIdElement().toVersionless())); } // copy all identifiers from the source to the target @@ -118,9 +133,9 @@ public Patient prepareTargetPatientForUpdate( private void prepareSourcePatientForUpdate(Patient theSourceResource, Patient theTargetResource) { theSourceResource.setActive(false); theSourceResource - .addLink() - .setType(Patient.LinkType.REPLACEDBY) - .setOther(new Reference(theTargetResource.getIdElement().toVersionless())); + .addLink() + .setType(Patient.LinkType.REPLACEDBY) + .setOther(new Reference(theTargetResource.getIdElement().toVersionless())); } /** diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java index f4b63e1039f5..890e8320302d 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java @@ -23,11 +23,9 @@ import ca.uhn.fhir.batch2.jobs.parameters.BatchJobParametersWithTaskId; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; +import ca.uhn.fhir.rest.server.provider.ProviderConstants; import com.fasterxml.jackson.annotation.JsonProperty; -import static ca.uhn.fhir.jpa.api.config.JpaStorageSettings.DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE; -import static ca.uhn.fhir.jpa.api.config.JpaStorageSettings.DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING; - public class ReplaceReferencesJobParameters extends BatchJobParametersWithTaskId { @JsonProperty("sourceId") @@ -37,10 +35,10 @@ public class ReplaceReferencesJobParameters extends BatchJobParametersWithTaskId private FhirIdJson myTargetId; @JsonProperty( - value = "batchSize", - defaultValue = DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE_STRING, + value = "resourceLimit", + defaultValue = ProviderConstants.OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT_DEFAULT_STRING, required = false) - private int myBatchSize; + private int myResourceLimit; @JsonProperty("partitionId") private RequestPartitionId myPartitionId; @@ -50,7 +48,7 @@ public ReplaceReferencesJobParameters() {} public ReplaceReferencesJobParameters(ReplaceReferencesRequest theRequest) { mySourceId = new FhirIdJson(theRequest.sourceId); myTargetId = new FhirIdJson(theRequest.targetId); - myBatchSize = theRequest.batchSize; + myResourceLimit = theRequest.resourceLimit; myPartitionId = theRequest.partitionId; } @@ -70,15 +68,15 @@ public void setTargetId(FhirIdJson theTargetId) { myTargetId = theTargetId; } - public int getBatchSize() { - if (myBatchSize <= 0) { - myBatchSize = DEFAULT_MAX_TRANSACTION_ENTRIES_FOR_WRITE; + public int getResourceLimit() { + if (myResourceLimit <= 0) { + myResourceLimit = ProviderConstants.OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT_DEFAULT; } - return myBatchSize; + return myResourceLimit; } - public void setBatchSize(int theBatchSize) { - myBatchSize = theBatchSize; + public void setResourceLimit(int theResourceLimit) { + myResourceLimit = theResourceLimit; } public RequestPartitionId getPartitionId() { @@ -90,6 +88,6 @@ public void setPartitionId(RequestPartitionId thePartitionId) { } public ReplaceReferencesRequest asReplaceReferencesRequest() { - return new ReplaceReferencesRequest(mySourceId.asIdDt(), myTargetId.asIdDt(), myBatchSize, myPartitionId); + return new ReplaceReferencesRequest(mySourceId.asIdDt(), myTargetId.asIdDt(), myResourceLimit, myPartitionId); } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java index 2e551cb518b4..9106a177c3ad 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java @@ -70,7 +70,7 @@ public RunOutcome run( params.getSourceId().asIdDt()) .map(FhirIdJson::new); - StreamUtil.partition(stream, params.getBatchSize()) + StreamUtil.partition(stream, params.getResourceLimit()) .forEach(chunk -> totalCount.addAndGet(processChunk(theDataSink, chunk, params.getPartitionId()))); }); diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java index fa858efee46f..bf9aebd5234b 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/api/config/JpaStorageSettings.java @@ -36,7 +36,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.time.DateUtils; -import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Bundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,8 +47,6 @@ import java.util.Set; import java.util.TreeSet; -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; - @SuppressWarnings("JavadocLinkAsPlainText") public class JpaStorageSettings extends StorageSettings { private static final Logger ourLog = LoggerFactory.getLogger(JpaStorageSettings.class); @@ -135,7 +132,7 @@ public class JpaStorageSettings extends StorageSettings { * transaction be? * @since 8.0.0 */ - public static final String DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE_STRING = "512"; + public static final String DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE_STRING = "1024"; public static final int DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE = Integer.parseInt(DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE_STRING); @@ -2723,14 +2720,6 @@ public void setDefaultTransactionEntriesForWrite(int theDefaultTransactionEntrie myDefaultTransactionEntriesForWrite = theDefaultTransactionEntriesForWrite; } - public int getTransactionWriteBatchSizeFromOperationParameter(IPrimitiveType theBatchSize) { - int retval = defaultIfNull(IPrimitiveType.toValueOrNull(theBatchSize), getDefaultTransactionEntriesForWrite()); - if (retval > getMaxTransactionEntriesForWrite()) { - retval = getMaxTransactionEntriesForWrite(); - } - return retval; - } - public enum StoreMetaSourceInformationEnum { NONE(false, false), SOURCE_URI(true, false), diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java index 2bc04dc28eb8..5037041802ea 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java @@ -42,20 +42,18 @@ public class ReplaceReferencesRequest { @Nonnull public final IIdType targetId; - public final int batchSize; + public final int resourceLimit; public final RequestPartitionId partitionId; - private boolean myForceSync = false; - public ReplaceReferencesRequest( @Nonnull IIdType theSourceId, @Nonnull IIdType theTargetId, - int theBatchSize, + int theResourceLimit, RequestPartitionId thePartitionId) { sourceId = theSourceId.toUnqualifiedVersionless(); targetId = theTargetId.toUnqualifiedVersionless(); - batchSize = theBatchSize; + resourceLimit = theResourceLimit; partitionId = thePartitionId; } @@ -78,11 +76,4 @@ public void validateOrThrowInvalidParameterException() { } } - public boolean isForceSync() { - return myForceSync; - } - - public void setForceSync(boolean theForceSync) { - this.myForceSync = theForceSync; - } } From d308dc796149981043786ea98fbcc6b0b68a24a9 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 19:08:36 -0500 Subject: [PATCH 132/148] still need to return 412 --- .../provider/ReplaceReferencesSvcImpl.java | 2 - .../provider/merge/PatientMergeProvider.java | 1 - .../jobs/merge/MergeResourceHelper.java | 65 ++++++++++--------- .../ReplaceReferencesRequest.java | 1 - 4 files changed, 33 insertions(+), 36 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index a52cbd4b7bd4..9799594fce51 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -39,8 +39,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; import static ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx.JOB_REPLACE_REFERENCES; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java index 39942d4612ff..a3d093129a18 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java @@ -23,7 +23,6 @@ import java.util.stream.Collectors; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; -import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; public class PatientMergeProvider extends BaseJpaResourceProvider { diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeResourceHelper.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeResourceHelper.java index f841f117a11c..4ad5960a83cb 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeResourceHelper.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/merge/MergeResourceHelper.java @@ -49,10 +49,11 @@ public MergeResourceHelper(IFhirResourceDao theDao) { myPatientDao = theDao; } - public static int setResourceLimitFromParameter(JpaStorageSettings theStorageSettings, IPrimitiveType theResourceLimit) { + public static int setResourceLimitFromParameter( + JpaStorageSettings theStorageSettings, IPrimitiveType theResourceLimit) { int retval = defaultIfNull( - IPrimitiveType.toValueOrNull(theResourceLimit), - ProviderConstants.OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT_DEFAULT); + IPrimitiveType.toValueOrNull(theResourceLimit), + ProviderConstants.OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT_DEFAULT); if (retval > theStorageSettings.getMaxTransactionEntriesForWrite()) { retval = theStorageSettings.getMaxTransactionEntriesForWrite(); } @@ -60,36 +61,36 @@ public static int setResourceLimitFromParameter(JpaStorageSettings theStorageSet } public void updateMergedResourcesAfterReferencesReplaced( - IHapiTransactionService myHapiTransactionService, - IIdType theSourceResourceId, - IIdType theTargetResourceId, - @Nullable Patient theResultResource, - boolean theDeleteSource, - RequestDetails theRequestDetails) { + IHapiTransactionService myHapiTransactionService, + IIdType theSourceResourceId, + IIdType theTargetResourceId, + @Nullable Patient theResultResource, + boolean theDeleteSource, + RequestDetails theRequestDetails) { Patient sourceResource = myPatientDao.read(theSourceResourceId, theRequestDetails); Patient targetResource = myPatientDao.read(theTargetResourceId, theRequestDetails); updateMergedResourcesAfterReferencesReplaced( - myHapiTransactionService, - sourceResource, - targetResource, - theResultResource, - theDeleteSource, - theRequestDetails); + myHapiTransactionService, + sourceResource, + targetResource, + theResultResource, + theDeleteSource, + theRequestDetails); } public Patient updateMergedResourcesAfterReferencesReplaced( - IHapiTransactionService myHapiTransactionService, - Patient theSourceResource, - Patient theTargetResource, - @Nullable Patient theResultResource, - boolean theDeleteSource, - RequestDetails theRequestDetails) { + IHapiTransactionService myHapiTransactionService, + Patient theSourceResource, + Patient theTargetResource, + @Nullable Patient theResultResource, + boolean theDeleteSource, + RequestDetails theRequestDetails) { AtomicReference targetPatientAfterUpdate = new AtomicReference<>(); myHapiTransactionService.withRequest(theRequestDetails).execute(() -> { Patient patientToUpdate = prepareTargetPatientForUpdate( - theTargetResource, theSourceResource, theResultResource, theDeleteSource); + theTargetResource, theSourceResource, theResultResource, theDeleteSource); targetPatientAfterUpdate.set(updateResource(patientToUpdate, theRequestDetails)); @@ -105,10 +106,10 @@ public Patient updateMergedResourcesAfterReferencesReplaced( } public Patient prepareTargetPatientForUpdate( - Patient theTargetResource, - Patient theSourceResource, - @Nullable Patient theResultResource, - boolean theDeleteSource) { + Patient theTargetResource, + Patient theSourceResource, + @Nullable Patient theResultResource, + boolean theDeleteSource) { // if the client provided a result resource as input then use it to update the target resource if (theResultResource != null) { @@ -119,9 +120,9 @@ public Patient prepareTargetPatientForUpdate( // add the replaces link to the target resource, if the source resource is not to be deleted if (!theDeleteSource) { theTargetResource - .addLink() - .setType(Patient.LinkType.REPLACES) - .setOther(new Reference(theSourceResource.getIdElement().toVersionless())); + .addLink() + .setType(Patient.LinkType.REPLACES) + .setOther(new Reference(theSourceResource.getIdElement().toVersionless())); } // copy all identifiers from the source to the target @@ -133,9 +134,9 @@ public Patient prepareTargetPatientForUpdate( private void prepareSourcePatientForUpdate(Patient theSourceResource, Patient theTargetResource) { theSourceResource.setActive(false); theSourceResource - .addLink() - .setType(Patient.LinkType.REPLACEDBY) - .setOther(new Reference(theTargetResource.getIdElement().toVersionless())); + .addLink() + .setType(Patient.LinkType.REPLACEDBY) + .setOther(new Reference(theTargetResource.getIdElement().toVersionless())); } /** diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java index 5037041802ea..d3855a24d805 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/replacereferences/ReplaceReferencesRequest.java @@ -75,5 +75,4 @@ public void validateOrThrowInvalidParameterException() { Msg.code(2587) + "Source and target id parameters must be for the same resource type"); } } - } From e5ef9a70c9c34800736aa83079263d8a8cca1bf2 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 19:49:16 -0500 Subject: [PATCH 133/148] return 412 --- .../provider/ReplaceReferencesSvcImpl.java | 9 +++++--- .../provider/merge/ResourceMergeService.java | 14 ----------- .../merge/ResourceMergeServiceTest.java | 23 +++++++++---------- .../provider/r4/ReplaceReferencesR4Test.java | 17 ++++++++++---- 4 files changed, 30 insertions(+), 33 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 9799594fce51..cffc98a7c982 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -29,6 +29,7 @@ import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.util.StopLimitAccumulator; import jakarta.annotation.Nonnull; import org.hl7.fhir.instance.model.api.IBaseParameters; @@ -122,9 +123,11 @@ private IBaseParameters replaceReferencesPreferSync( .execute(() -> getAllPidsWithLimit(theReplaceReferencesRequest)); if (accumulator.isTruncated()) { - // FIXME KHS undo this? - ourLog.warn("Too many results. Switching to asynchronous reference replacement."); - return replaceReferencesPreferAsync(theReplaceReferencesRequest, theRequestDetails); + throw new PreconditionFailedException("Number of resources with references to " + + theReplaceReferencesRequest.sourceId + + " exceeds the resource-limit " + + theReplaceReferencesRequest.resourceLimit + + ". Submit the request asynchronsly by adding the HTTP Header 'Prefer: respond-async'."); } Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources( diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 503f1c7ade41..c8c507f4ddb1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -199,20 +199,6 @@ private void doMerge( } } - private boolean referenceCountExceedsSyncLimit( - Patient theSourceResource, RequestDetails theRequestDetails, Integer theResourceLimit) { - Integer numberOfRefs = myReplaceReferencesSvc.countResourcesReferencingResource( - theSourceResource.getIdElement().toVersionless(), theRequestDetails); - boolean exceedsSyncLimit = numberOfRefs > theResourceLimit; - if (exceedsSyncLimit) { - ourLog.info( - "{} resources need to be updated. This exceeds the batch size of {}. Switching to asynchronous processing; will return a Task in the response that can be used to track progress.", - numberOfRefs, - theResourceLimit); - } - return exceedsSyncLimit; - } - private void doMergeSync( BaseMergeOperationInputParameters theMergeOperationParameters, Patient theSourceResource, diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java index aba3b097905b..33fceea713f2 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java @@ -18,6 +18,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.CanonicalIdentifier; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -76,6 +77,7 @@ public class ResourceMergeServiceTest { private static final String TARGET_PATIENT_TEST_ID = "Patient/456"; private static final String TARGET_PATIENT_TEST_ID_WITH_VERSION_1 = TARGET_PATIENT_TEST_ID + "/_history/1"; private static final String TARGET_PATIENT_TEST_ID_WITH_VERSION_2 = TARGET_PATIENT_TEST_ID + "/_history/2"; + public static final String PRECONDITION_FAILED_MESSAGE = "bad wolf"; @Mock DaoRegistry myDaoRegistryMock; @@ -449,25 +451,24 @@ void testMerge_AsyncBecauseOfLargeNumberOfRefs_Success(boolean theWithResultReso setupDaoMockForSuccessfulRead(sourcePatient); setupDaoMockForSuccessfulRead(targetPatient); - when(myReplaceReferencesSvcMock.countResourcesReferencingResource(new IdType(SOURCE_PATIENT_TEST_ID), - myRequestDetailsMock)).thenReturn(PAGE_SIZE + 1); - - Patient resultResource = null; if (theWithResultResource) { - resultResource = createValidResultPatient(theWithDeleteSource); + Patient resultResource = createValidResultPatient(theWithDeleteSource); mergeOperationParameters.setResultResource(resultResource); } - Task task = new Task(); - setupBatch2JobTaskHelperMock(task); + when(myReplaceReferencesSvcMock.replaceReferences(any(), any())).thenThrow(new PreconditionFailedException(PRECONDITION_FAILED_MESSAGE)); MergeOperationOutcome mergeOutcome = myResourceMergeService.merge(mergeOperationParameters, myRequestDetailsMock); - verifySuccessfulOutcomeForAsync(mergeOutcome, task); - verifyBatch2JobTaskHelperMockInvocation(resultResource, theWithDeleteSource); - + verifyFailedOutcome(mergeOutcome); verifyNoMoreInteractions(myPatientDaoMock); + } + private void verifyFailedOutcome(MergeOperationOutcome theMergeOutcome) { + assertThat(theMergeOutcome.getHttpStatusCode()).isEqualTo(PreconditionFailedException.STATUS_CODE); + OperationOutcome operationOutcome = (OperationOutcome) theMergeOutcome.getOperationOutcome(); + assertThat(operationOutcome.getIssue()).hasSize(1); + assertThat(operationOutcome.getIssueFirstRep().getDiagnostics()).isEqualTo(PRECONDITION_FAILED_MESSAGE); } // ERROR CASES @@ -1411,8 +1412,6 @@ private void verifyUpdatedTargetPatient(boolean theExpectLinkToSourcePatient, Li private void setupReplaceReferencesForSuccessForSync() { // set the count to less that the page size for sync processing - when(myReplaceReferencesSvcMock.countResourcesReferencingResource(new IdType(SOURCE_PATIENT_TEST_ID), - myRequestDetailsMock)).thenReturn(PAGE_SIZE - 1); when(myReplaceReferencesSvcMock.replaceReferences(isA(ReplaceReferencesRequest.class), eq(myRequestDetailsMock))).thenReturn(new Parameters()); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index 73413cd9a960..952c28d1e02d 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -3,6 +3,7 @@ import ca.uhn.fhir.batch2.model.JobInstance; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import jakarta.servlet.http.HttpServletResponse; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; @@ -11,6 +12,7 @@ import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.Task; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -21,6 +23,7 @@ import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME; import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -75,11 +78,17 @@ private JobInstance awaitJobCompletion(Task task) { return myBatch2JobHelper.awaitJobCompletion(jobId); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) - void testReplaceReferencesSmallResourceLimit(boolean isAsync) { + @Test + void testReplaceReferencesSmallResourceLimitSync() { + assertThatThrownBy(() -> myTestHelper.callReplaceReferencesWithResourceLimit(myClient, false, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE)) + .isInstanceOf(PreconditionFailedException.class) + .hasMessage("HTTP 412 Precondition Failed: Number of resources with references to " + myTestHelper.getSourcePatientId() + " exceeds the resource-limit 5. Submit the request asynchronsly by adding the HTTP Header 'Prefer: respond-async'."); + } + + @Test + void testReplaceReferencesSmallResourceLimitAsync() { // exec - Parameters outParams = myTestHelper.callReplaceReferencesWithResourceLimit(myClient, isAsync, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); + Parameters outParams = myTestHelper.callReplaceReferencesWithResourceLimit(myClient, true, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); assertThat(getLastHttpStatusCode()).isEqualTo(HttpServletResponse.SC_ACCEPTED); From 606764166348f28e10cd45ec41a9a1e98618775a Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 19:59:33 -0500 Subject: [PATCH 134/148] moar tests --- .../jpa/provider/ReplaceReferencesSvcImpl.java | 10 +++++----- .../fhir/jpa/provider/r4/PatientMergeR4Test.java | 15 +++++++++++++++ .../rest/server/provider/ProviderConstants.java | 1 - 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index cffc98a7c982..018de53fac81 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -123,11 +123,11 @@ private IBaseParameters replaceReferencesPreferSync( .execute(() -> getAllPidsWithLimit(theReplaceReferencesRequest)); if (accumulator.isTruncated()) { - throw new PreconditionFailedException("Number of resources with references to " + - theReplaceReferencesRequest.sourceId + - " exceeds the resource-limit " + - theReplaceReferencesRequest.resourceLimit + - ". Submit the request asynchronsly by adding the HTTP Header 'Prefer: respond-async'."); + throw new PreconditionFailedException( + "Number of resources with references to " + theReplaceReferencesRequest.sourceId + + " exceeds the resource-limit " + + theReplaceReferencesRequest.resourceLimit + + ". Submit the request asynchronsly by adding the HTTP Header 'Prefer: respond-async'."); } Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources( diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 28fcdaf788ea..0f7404c60873 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -8,6 +8,7 @@ import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInput; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import jakarta.annotation.Nonnull; import jakarta.servlet.http.HttpServletResponse; @@ -226,6 +227,20 @@ public void testMerge(boolean withDelete, boolean withInputResultPatient, boolea } } + @Test + void testMerge_smallResourceLimit() { + ReplaceReferencesTestHelper.PatientMergeInputParameters inParams = new ReplaceReferencesTestHelper.PatientMergeInputParameters(); + myTestHelper.setSourceAndTarget(inParams); + + inParams.resourceLimit = 5; + Parameters inParameters = inParams.asParametersResource(); + + // exec + assertThatThrownBy(() -> callMergeOperation(inParameters, false)) + .isInstanceOf(PreconditionFailedException.class) + .satisfies(ex -> assertThat(extractFailureMessage((BaseServerResponseException) ex)).isEqualTo("Number of resources with references to "+ myTestHelper.getSourcePatientId() + " exceeds the resource-limit 5. Submit the request asynchronsly by adding the HTTP Header 'Prefer: respond-async'.")); + } + @Test void testMerge_SourceResourceCannotBeDeletedBecauseAnotherResourceReferencingSourceWasAddedWhileJobIsRunning_JobFails() { ReplaceReferencesTestHelper.PatientMergeInputParameters inParams = new ReplaceReferencesTestHelper.PatientMergeInputParameters(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index b0edc3d517b2..1a516794b553 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -265,7 +265,6 @@ public class ProviderConstants { * exceeds this amount, the operation will fail with 412 Precondition Failed. */ - // FIXME KHS change tests and var names public static final String OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT = "resource-limit"; /** From e52a8f4b86ad65a49707872b5878dba37878a15d Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 20:00:28 -0500 Subject: [PATCH 135/148] moar tests --- .../java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java | 1 - 1 file changed, 1 deletion(-) diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java index 1a516794b553..2568b0514c39 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ProviderConstants.java @@ -264,7 +264,6 @@ public class ProviderConstants { * If the request is being performed synchronously and the number of resources that need to change * exceeds this amount, the operation will fail with 412 Precondition Failed. */ - public static final String OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT = "resource-limit"; /** From fead7c32a881a6599f51a025c3a880b8adef38b9 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 20:08:34 -0500 Subject: [PATCH 136/148] fix async batch size --- .../ca/uhn/fhir/jpa/config/JpaConfig.java | 17 ++++++----- .../provider/ReplaceReferencesSvcImpl.java | 19 +++++++----- .../BaseMergeOperationInputParameters.java | 2 +- .../merge/ResourceMergeServiceTest.java | 2 +- .../ReplaceReferencesJobParameters.java | 29 ++++++++++--------- .../ReplaceReferencesQueryIdsStep.java | 2 +- 6 files changed, 40 insertions(+), 31 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index 66e541c77622..8a8ccb02964c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -87,6 +87,7 @@ import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.dao.JpaPid; +import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.jpa.model.sched.ISchedulerService; import ca.uhn.fhir.jpa.packages.IHapiPackageCacheManager; import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc; @@ -940,19 +941,21 @@ public Batch2TaskHelper batch2TaskHelper() { @Bean public IReplaceReferencesSvc replaceReferencesSvc( - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IResourceLinkDao theResourceLinkDao, - IJobCoordinator theJobCoordinator, - ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundle, - Batch2TaskHelper theBatch2TaskHelper) { + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IResourceLinkDao theResourceLinkDao, + IJobCoordinator theJobCoordinator, + ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundle, + Batch2TaskHelper theBatch2TaskHelper, + JpaStorageSettings theStorageSettings) { return new ReplaceReferencesSvcImpl( theDaoRegistry, theHapiTransactionService, theResourceLinkDao, theJobCoordinator, theReplaceReferencesPatchBundle, - theBatch2TaskHelper); + theBatch2TaskHelper, + theStorageSettings); } @Bean diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 018de53fac81..16c437dca5db 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -22,9 +22,11 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; import ca.uhn.fhir.batch2.util.Batch2TaskHelper; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; +import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; @@ -55,20 +57,23 @@ public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { private final IJobCoordinator myJobCoordinator; private final ReplaceReferencesPatchBundleSvc myReplaceReferencesPatchBundleSvc; private final Batch2TaskHelper myBatch2TaskHelper; + private final JpaStorageSettings myStorageSettings; public ReplaceReferencesSvcImpl( - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IResourceLinkDao theResourceLinkDao, - IJobCoordinator theJobCoordinator, - ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc, - Batch2TaskHelper theBatch2TaskHelper) { + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IResourceLinkDao theResourceLinkDao, + IJobCoordinator theJobCoordinator, + ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc, + Batch2TaskHelper theBatch2TaskHelper, + JpaStorageSettings theStorageSettings) { myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; myResourceLinkDao = theResourceLinkDao; myJobCoordinator = theJobCoordinator; myReplaceReferencesPatchBundleSvc = theReplaceReferencesPatchBundleSvc; myBatch2TaskHelper = theBatch2TaskHelper; + myStorageSettings = theStorageSettings; } @Override @@ -99,7 +104,7 @@ private IBaseParameters replaceReferencesPreferAsync( theRequestDetails, myJobCoordinator, JOB_REPLACE_REFERENCES, - new ReplaceReferencesJobParameters(theReplaceReferencesRequest)); + new ReplaceReferencesJobParameters(theReplaceReferencesRequest, myStorageSettings.getDefaultTransactionEntriesForWrite())); Parameters retval = new Parameters(); task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java index 799ef6314cb2..7606c867b552 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java @@ -136,7 +136,7 @@ public MergeJobParameters asMergeJobParameters( retval.setResultResource(theFhirContext.newJsonParser().encodeResourceToString(getResultResource())); } retval.setDeleteSource(getDeleteSource()); - retval.setResourceLimit(getResourceLimit()); + retval.setBatchSize(getResourceLimit()); retval.setSourceId(new FhirIdJson(theSourceResource.getIdElement().toVersionless())); retval.setTargetId(new FhirIdJson(theTargetResource.getIdElement().toVersionless())); retval.setPartitionId(thePartitionId); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java index 33fceea713f2..40ace3e5fb86 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java @@ -1438,7 +1438,7 @@ private void verifyBatch2JobTaskHelperMockInvocation(@Nullable Patient theResult assertThat(jobParametersCaptor.getValue()).isInstanceOf(MergeJobParameters.class); MergeJobParameters capturedJobParams = (MergeJobParameters) jobParametersCaptor.getValue(); - assertThat(capturedJobParams.getResourceLimit()).isEqualTo(PAGE_SIZE); + assertThat(capturedJobParams.getBatchSize()).isEqualTo(PAGE_SIZE); assertThat(capturedJobParams.getSourceId().toString()).isEqualTo(SOURCE_PATIENT_TEST_ID); assertThat(capturedJobParams.getTargetId().toString()).isEqualTo(TARGET_PATIENT_TEST_ID); assertThat(capturedJobParams.getDeleteSource()).isEqualTo(theDeleteSource); diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java index 890e8320302d..474e542a2088 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java @@ -35,21 +35,22 @@ public class ReplaceReferencesJobParameters extends BatchJobParametersWithTaskId private FhirIdJson myTargetId; @JsonProperty( - value = "resourceLimit", + value = "batchSize", defaultValue = ProviderConstants.OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT_DEFAULT_STRING, required = false) - private int myResourceLimit; + private int myBatchSize; @JsonProperty("partitionId") private RequestPartitionId myPartitionId; public ReplaceReferencesJobParameters() {} - public ReplaceReferencesJobParameters(ReplaceReferencesRequest theRequest) { - mySourceId = new FhirIdJson(theRequest.sourceId); - myTargetId = new FhirIdJson(theRequest.targetId); - myResourceLimit = theRequest.resourceLimit; - myPartitionId = theRequest.partitionId; + public ReplaceReferencesJobParameters(ReplaceReferencesRequest theReplaceReferencesRequest, int theBatchSize) { + mySourceId = new FhirIdJson(theReplaceReferencesRequest.sourceId); + myTargetId = new FhirIdJson(theReplaceReferencesRequest.targetId); + // Note theReplaceReferencesRequest.resourceLimit is only used for the synchronous case. It is ignored in this async case. + myBatchSize = theBatchSize; + myPartitionId = theReplaceReferencesRequest.partitionId; } public FhirIdJson getSourceId() { @@ -68,15 +69,15 @@ public void setTargetId(FhirIdJson theTargetId) { myTargetId = theTargetId; } - public int getResourceLimit() { - if (myResourceLimit <= 0) { - myResourceLimit = ProviderConstants.OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT_DEFAULT; + public int getBatchSize() { + if (myBatchSize <= 0) { + myBatchSize = ProviderConstants.OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT_DEFAULT; } - return myResourceLimit; + return myBatchSize; } - public void setResourceLimit(int theResourceLimit) { - myResourceLimit = theResourceLimit; + public void setBatchSize(int theBatchSize) { + myBatchSize = theBatchSize; } public RequestPartitionId getPartitionId() { @@ -88,6 +89,6 @@ public void setPartitionId(RequestPartitionId thePartitionId) { } public ReplaceReferencesRequest asReplaceReferencesRequest() { - return new ReplaceReferencesRequest(mySourceId.asIdDt(), myTargetId.asIdDt(), myResourceLimit, myPartitionId); + return new ReplaceReferencesRequest(mySourceId.asIdDt(), myTargetId.asIdDt(), myBatchSize, myPartitionId); } } diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java index 9106a177c3ad..2e551cb518b4 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesQueryIdsStep.java @@ -70,7 +70,7 @@ public RunOutcome run( params.getSourceId().asIdDt()) .map(FhirIdJson::new); - StreamUtil.partition(stream, params.getResourceLimit()) + StreamUtil.partition(stream, params.getBatchSize()) .forEach(chunk -> totalCount.addAndGet(processChunk(theDataSink, chunk, params.getPartitionId()))); }); From 4a35fa1ca371b7c5873342ac50102b3fc3e8d979 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 20:16:20 -0500 Subject: [PATCH 137/148] fix async batch size --- .../ca/uhn/fhir/jpa/config/JpaConfig.java | 17 ++++++++-------- .../uhn/fhir/jpa/config/r4/JpaR4Config.java | 16 +++++++++------ .../provider/ReplaceReferencesSvcImpl.java | 18 ++++++++--------- .../BaseMergeOperationInputParameters.java | 4 +++- .../provider/merge/ResourceMergeService.java | 20 +++++++++++-------- .../merge/ResourceMergeServiceTest.java | 7 ++++++- .../ReplaceReferencesJobParameters.java | 6 ++++-- 7 files changed, 52 insertions(+), 36 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index 8a8ccb02964c..ce4bfd98c26d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -87,7 +87,6 @@ import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.dao.JpaPid; -import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.jpa.model.sched.ISchedulerService; import ca.uhn.fhir.jpa.packages.IHapiPackageCacheManager; import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc; @@ -941,13 +940,13 @@ public Batch2TaskHelper batch2TaskHelper() { @Bean public IReplaceReferencesSvc replaceReferencesSvc( - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IResourceLinkDao theResourceLinkDao, - IJobCoordinator theJobCoordinator, - ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundle, - Batch2TaskHelper theBatch2TaskHelper, - JpaStorageSettings theStorageSettings) { + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IResourceLinkDao theResourceLinkDao, + IJobCoordinator theJobCoordinator, + ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundle, + Batch2TaskHelper theBatch2TaskHelper, + JpaStorageSettings theStorageSettings) { return new ReplaceReferencesSvcImpl( theDaoRegistry, theHapiTransactionService, @@ -955,7 +954,7 @@ public IReplaceReferencesSvc replaceReferencesSvc( theJobCoordinator, theReplaceReferencesPatchBundle, theBatch2TaskHelper, - theStorageSettings); + theStorageSettings); } @Bean diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java index 08efc7d611f4..63b2aa4885ff 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.jpa.api.IDaoRegistry; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.config.GeneratedDaoAndResourceProviderConfigR4; @@ -107,13 +108,16 @@ public ITermLoaderSvc termLoaderService( @Bean public ResourceMergeService resourceMergeService( - DaoRegistry theDaoRegistry, - IReplaceReferencesSvc theReplaceReferencesSvc, - HapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator, - Batch2TaskHelper theBatch2TaskHelper) { + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + HapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper, + JpaStorageSettings theStorageSettings) { + return new ResourceMergeService( + theStorageSettings, theDaoRegistry, theReplaceReferencesSvc, theHapiTransactionService, diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 16c437dca5db..6ef0dbc003a5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -26,7 +26,6 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; -import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc; import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; @@ -60,13 +59,13 @@ public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc { private final JpaStorageSettings myStorageSettings; public ReplaceReferencesSvcImpl( - DaoRegistry theDaoRegistry, - HapiTransactionService theHapiTransactionService, - IResourceLinkDao theResourceLinkDao, - IJobCoordinator theJobCoordinator, - ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc, - Batch2TaskHelper theBatch2TaskHelper, - JpaStorageSettings theStorageSettings) { + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService, + IResourceLinkDao theResourceLinkDao, + IJobCoordinator theJobCoordinator, + ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc, + Batch2TaskHelper theBatch2TaskHelper, + JpaStorageSettings theStorageSettings) { myDaoRegistry = theDaoRegistry; myHapiTransactionService = theHapiTransactionService; myResourceLinkDao = theResourceLinkDao; @@ -104,7 +103,8 @@ private IBaseParameters replaceReferencesPreferAsync( theRequestDetails, myJobCoordinator, JOB_REPLACE_REFERENCES, - new ReplaceReferencesJobParameters(theReplaceReferencesRequest, myStorageSettings.getDefaultTransactionEntriesForWrite())); + new ReplaceReferencesJobParameters( + theReplaceReferencesRequest, myStorageSettings.getDefaultTransactionEntriesForWrite())); Parameters retval = new Parameters(); task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java index 7606c867b552..4fdd8c77d431 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/BaseMergeOperationInputParameters.java @@ -23,6 +23,7 @@ import ca.uhn.fhir.batch2.jobs.merge.MergeJobParameters; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.util.CanonicalIdentifier; import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -128,6 +129,7 @@ public int getResourceLimit() { public MergeJobParameters asMergeJobParameters( FhirContext theFhirContext, + JpaStorageSettings theStorageSettings, Patient theSourceResource, Patient theTargetResource, RequestPartitionId thePartitionId) { @@ -136,7 +138,7 @@ public MergeJobParameters asMergeJobParameters( retval.setResultResource(theFhirContext.newJsonParser().encodeResourceToString(getResultResource())); } retval.setDeleteSource(getDeleteSource()); - retval.setBatchSize(getResourceLimit()); + retval.setBatchSize(theStorageSettings.getDefaultTransactionEntriesForWrite()); retval.setSourceId(new FhirIdJson(theSourceResource.getIdElement().toVersionless())); retval.setTargetId(new FhirIdJson(theTargetResource.getIdElement().toVersionless())); retval.setPartitionId(thePartitionId); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index c8c507f4ddb1..04ef07feef78 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; @@ -53,10 +54,11 @@ public class ResourceMergeService { private static final Logger ourLog = LoggerFactory.getLogger(ResourceMergeService.class); + private final FhirContext myFhirContext; + private final JpaStorageSettings myStorageSettings; private final IFhirResourceDao myPatientDao; private final IReplaceReferencesSvc myReplaceReferencesSvc; private final IHapiTransactionService myHapiTransactionService; - private final FhirContext myFhirContext; private final IRequestPartitionHelperSvc myRequestPartitionHelperSvc; private final IFhirResourceDao myTaskDao; private final IJobCoordinator myJobCoordinator; @@ -65,12 +67,14 @@ public class ResourceMergeService { private final MergeValidationService myMergeValidationService; public ResourceMergeService( - DaoRegistry theDaoRegistry, - IReplaceReferencesSvc theReplaceReferencesSvc, - IHapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator, - Batch2TaskHelper theBatch2TaskHelper) { + JpaStorageSettings theStorageSettings, + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + IHapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper) { + myStorageSettings = theStorageSettings; myPatientDao = theDaoRegistry.getResourceDao(Patient.class); myTaskDao = theDaoRegistry.getResourceDao(Task.class); @@ -237,7 +241,7 @@ private void doMergeAsync( RequestPartitionId thePartitionId) { MergeJobParameters mergeJobParameters = theMergeOperationParameters.asMergeJobParameters( - myFhirContext, theSourceResource, theTargetResource, thePartitionId); + myFhirContext, myStorageSettings, theSourceResource, theTargetResource, thePartitionId); Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java index 40ace3e5fb86..093f7c466d30 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeServiceTest.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.batch2.util.Batch2TaskHelper; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoPatient; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; @@ -109,6 +110,8 @@ public class ResourceMergeServiceTest { @Mock RequestPartitionId myRequestPartitionIdMock; + @Mock + private JpaStorageSettings myStorageSettingsMock; private ResourceMergeService myResourceMergeService; @@ -123,7 +126,9 @@ void setup() { when(myDaoRegistryMock.getResourceDao(eq(Patient.class))).thenReturn(myPatientDaoMock); when(myDaoRegistryMock.getResourceDao(eq(Task.class))).thenReturn(myTaskDaoMock); when(myPatientDaoMock.getContext()).thenReturn(myFhirContext); - myResourceMergeService = new ResourceMergeService(myDaoRegistryMock, + myResourceMergeService = new ResourceMergeService( + myStorageSettingsMock, + myDaoRegistryMock, myReplaceReferencesSvcMock, myTransactionServiceMock, myRequestPartitionHelperSvcMock, diff --git a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java index 474e542a2088..193c8b21bc0a 100644 --- a/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java +++ b/hapi-fhir-storage-batch2-jobs/src/main/java/ca/uhn/fhir/batch2/jobs/replacereferences/ReplaceReferencesJobParameters.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.batch2.jobs.chunk.FhirIdJson; import ca.uhn.fhir.batch2.jobs.parameters.BatchJobParametersWithTaskId; import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; import ca.uhn.fhir.rest.server.provider.ProviderConstants; import com.fasterxml.jackson.annotation.JsonProperty; @@ -48,7 +49,8 @@ public ReplaceReferencesJobParameters() {} public ReplaceReferencesJobParameters(ReplaceReferencesRequest theReplaceReferencesRequest, int theBatchSize) { mySourceId = new FhirIdJson(theReplaceReferencesRequest.sourceId); myTargetId = new FhirIdJson(theReplaceReferencesRequest.targetId); - // Note theReplaceReferencesRequest.resourceLimit is only used for the synchronous case. It is ignored in this async case. + // Note theReplaceReferencesRequest.resourceLimit is only used for the synchronous case. It is ignored in this + // async case. myBatchSize = theBatchSize; myPartitionId = theReplaceReferencesRequest.partitionId; } @@ -71,7 +73,7 @@ public void setTargetId(FhirIdJson theTargetId) { public int getBatchSize() { if (myBatchSize <= 0) { - myBatchSize = ProviderConstants.OPERATION_REPLACE_REFERENCES_RESOURCE_LIMIT_DEFAULT; + myBatchSize = JpaStorageSettings.DEFAULT_TRANSACTION_ENTRIES_FOR_WRITE; } return myBatchSize; } From 44b49eb9bf845e8cb9c2dec1dcabe2e35e8ed85b Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 20:16:51 -0500 Subject: [PATCH 138/148] fix async batch size --- .../ca/uhn/fhir/jpa/config/r4/JpaR4Config.java | 16 ++++++++-------- .../jpa/provider/merge/ResourceMergeService.java | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java index 63b2aa4885ff..bfe6aa9e95db 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java @@ -108,16 +108,16 @@ public ITermLoaderSvc termLoaderService( @Bean public ResourceMergeService resourceMergeService( - DaoRegistry theDaoRegistry, - IReplaceReferencesSvc theReplaceReferencesSvc, - HapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator, - Batch2TaskHelper theBatch2TaskHelper, - JpaStorageSettings theStorageSettings) { + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + HapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper, + JpaStorageSettings theStorageSettings) { return new ResourceMergeService( - theStorageSettings, + theStorageSettings, theDaoRegistry, theReplaceReferencesSvc, theHapiTransactionService, diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java index 04ef07feef78..8ea68fe5514b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/ResourceMergeService.java @@ -67,13 +67,13 @@ public class ResourceMergeService { private final MergeValidationService myMergeValidationService; public ResourceMergeService( - JpaStorageSettings theStorageSettings, - DaoRegistry theDaoRegistry, - IReplaceReferencesSvc theReplaceReferencesSvc, - IHapiTransactionService theHapiTransactionService, - IRequestPartitionHelperSvc theRequestPartitionHelperSvc, - IJobCoordinator theJobCoordinator, - Batch2TaskHelper theBatch2TaskHelper) { + JpaStorageSettings theStorageSettings, + DaoRegistry theDaoRegistry, + IReplaceReferencesSvc theReplaceReferencesSvc, + IHapiTransactionService theHapiTransactionService, + IRequestPartitionHelperSvc theRequestPartitionHelperSvc, + IJobCoordinator theJobCoordinator, + Batch2TaskHelper theBatch2TaskHelper) { myStorageSettings = theStorageSettings; myPatientDao = theDaoRegistry.getResourceDao(Patient.class); From be8f8204f053c692556380bef8560c92cce4d501 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 20:50:48 -0500 Subject: [PATCH 139/148] exception code --- .../ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 6ef0dbc003a5..6b8a759c78e1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -22,6 +22,7 @@ import ca.uhn.fhir.batch2.api.IJobCoordinator; import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters; import ca.uhn.fhir.batch2.util.Batch2TaskHelper; +import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; @@ -128,7 +129,7 @@ private IBaseParameters replaceReferencesPreferSync( .execute(() -> getAllPidsWithLimit(theReplaceReferencesRequest)); if (accumulator.isTruncated()) { - throw new PreconditionFailedException( + throw new PreconditionFailedException(Msg.code(2597) + "Number of resources with references to " + theReplaceReferencesRequest.sourceId + " exceeds the resource-limit " + theReplaceReferencesRequest.resourceLimit From a6544b74222e6cc194c070f71b1d837bbed56ab2 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Mon, 23 Dec 2024 20:51:21 -0500 Subject: [PATCH 140/148] exception code --- .../fhir/jpa/provider/ReplaceReferencesSvcImpl.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java index 6b8a759c78e1..af1262c86554 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/ReplaceReferencesSvcImpl.java @@ -129,11 +129,11 @@ private IBaseParameters replaceReferencesPreferSync( .execute(() -> getAllPidsWithLimit(theReplaceReferencesRequest)); if (accumulator.isTruncated()) { - throw new PreconditionFailedException(Msg.code(2597) + - "Number of resources with references to " + theReplaceReferencesRequest.sourceId - + " exceeds the resource-limit " - + theReplaceReferencesRequest.resourceLimit - + ". Submit the request asynchronsly by adding the HTTP Header 'Prefer: respond-async'."); + throw new PreconditionFailedException(Msg.code(2597) + "Number of resources with references to " + + theReplaceReferencesRequest.sourceId + + " exceeds the resource-limit " + + theReplaceReferencesRequest.resourceLimit + + ". Submit the request asynchronsly by adding the HTTP Header 'Prefer: respond-async'."); } Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources( From fd02d5f9fa1cb8dafcafce242dd5ee392c4c286c Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 24 Dec 2024 10:16:32 -0500 Subject: [PATCH 141/148] fix bean wiring --- .../src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java | 4 ++-- .../ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java index bfe6aa9e95db..0308b4ba97b5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java @@ -128,7 +128,7 @@ public ResourceMergeService resourceMergeService( @Bean public PatientMergeProvider patientMergeProvider( - FhirContext theFhirContext, ResourceMergeService theResourceMergeService) { - return new PatientMergeProvider(theFhirContext, theResourceMergeService); + FhirContext theFhirContext, DaoRegistry theDaoRegistry, ResourceMergeService theResourceMergeService) { + return new PatientMergeProvider(theFhirContext, theDaoRegistry, theResourceMergeService); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java index a3d093129a18..b1c35ad9d8cc 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java @@ -3,6 +3,7 @@ import ca.uhn.fhir.batch2.jobs.merge.MergeResourceHelper; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.provider.BaseJpaResourceProvider; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; @@ -29,7 +30,8 @@ public class PatientMergeProvider extends BaseJpaResourceProvider { private final FhirContext myFhirContext; private final ResourceMergeService myResourceMergeService; - public PatientMergeProvider(FhirContext theFhirContext, ResourceMergeService theResourceMergeService) { + public PatientMergeProvider(FhirContext theFhirContext, DaoRegistry theDaoRegistry, ResourceMergeService theResourceMergeService) { + super(theDaoRegistry.getResourceDao("Patient")); myFhirContext = theFhirContext; assert myFhirContext.getVersion().getVersion() == FhirVersionEnum.R4; myResourceMergeService = theResourceMergeService; From 620538e9b8c7f61602ef2ee17d7cbc7fcec42937 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 24 Dec 2024 10:16:47 -0500 Subject: [PATCH 142/148] fix bean wiring --- .../src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java | 2 +- .../ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java index 0308b4ba97b5..a92ce4942746 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/JpaR4Config.java @@ -128,7 +128,7 @@ public ResourceMergeService resourceMergeService( @Bean public PatientMergeProvider patientMergeProvider( - FhirContext theFhirContext, DaoRegistry theDaoRegistry, ResourceMergeService theResourceMergeService) { + FhirContext theFhirContext, DaoRegistry theDaoRegistry, ResourceMergeService theResourceMergeService) { return new PatientMergeProvider(theFhirContext, theDaoRegistry, theResourceMergeService); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java index b1c35ad9d8cc..eb379cab5f53 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/PatientMergeProvider.java @@ -30,7 +30,8 @@ public class PatientMergeProvider extends BaseJpaResourceProvider { private final FhirContext myFhirContext; private final ResourceMergeService myResourceMergeService; - public PatientMergeProvider(FhirContext theFhirContext, DaoRegistry theDaoRegistry, ResourceMergeService theResourceMergeService) { + public PatientMergeProvider( + FhirContext theFhirContext, DaoRegistry theDaoRegistry, ResourceMergeService theResourceMergeService) { super(theDaoRegistry.getResourceDao("Patient")); myFhirContext = theFhirContext; assert myFhirContext.getVersion().getVersion() == FhirVersionEnum.R4; From f62952239d90977a07ee6f5c3632d5f83ca90e1b Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 24 Dec 2024 11:48:09 -0500 Subject: [PATCH 143/148] fix test --- .../provider/r4/ReplaceReferencesR4Test.java | 17 ++++++++++++++--- .../interceptor/ConsentInterceptorTest.java | 2 ++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java index 952c28d1e02d..3e01dc25e662 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ReplaceReferencesR4Test.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jpa.provider.r4; import ca.uhn.fhir.batch2.model.JobInstance; +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.jpa.replacereferences.ReplaceReferencesTestHelper; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; @@ -11,6 +12,7 @@ import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.Task; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -31,6 +33,13 @@ public class ReplaceReferencesR4Test extends BaseResourceProviderR4Test { ReplaceReferencesTestHelper myTestHelper; + @Override + @AfterEach + public void after() throws Exception { + super.after(); + myStorageSettings.setDefaultTransactionEntriesForWrite(new JpaStorageSettings().getDefaultTransactionEntriesForWrite()); + } + @Override @BeforeEach public void before() throws Exception { @@ -66,7 +75,7 @@ void testReplaceReferences(boolean isAsync) { // validate ReplaceReferencesTestHelper.validatePatchResultBundle(patchResultBundle, ReplaceReferencesTestHelper.TOTAL_EXPECTED_PATCHES, List.of( - "Observation", "Encounter", "CarePlan")); + "Observation", "Encounter", "CarePlan")); // Check that the linked resources were updated @@ -82,11 +91,13 @@ private JobInstance awaitJobCompletion(Task task) { void testReplaceReferencesSmallResourceLimitSync() { assertThatThrownBy(() -> myTestHelper.callReplaceReferencesWithResourceLimit(myClient, false, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE)) .isInstanceOf(PreconditionFailedException.class) - .hasMessage("HTTP 412 Precondition Failed: Number of resources with references to " + myTestHelper.getSourcePatientId() + " exceeds the resource-limit 5. Submit the request asynchronsly by adding the HTTP Header 'Prefer: respond-async'."); + .hasMessage("HTTP 412 Precondition Failed: HAPI-2597: Number of resources with references to " + myTestHelper.getSourcePatientId() + " exceeds the resource-limit 5. Submit the request asynchronsly by adding the HTTP Header 'Prefer: respond-async'."); } @Test - void testReplaceReferencesSmallResourceLimitAsync() { + void testReplaceReferencesSmallTransactionEntriesSize() { + myStorageSettings.setDefaultTransactionEntriesForWrite(5); + // exec Parameters outParams = myTestHelper.callReplaceReferencesWithResourceLimit(myClient, true, ReplaceReferencesTestHelper.SMALL_BATCH_SIZE); diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java index 186d14542323..73a1f9ba1609 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/ConsentInterceptorTest.java @@ -65,6 +65,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.timeout; @@ -138,6 +139,7 @@ public void testOutcomeSuccess() throws IOException { when(myConsentSvc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED); when(myConsentSvc.canSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED); when(myConsentSvc.willSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED); + doNothing().when(myConsentSvc).completeOperationSuccess(any(), any()); HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient"); From 3dd3ebb5ccdebb5e170b3ab512dc8a71208f9b27 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 24 Dec 2024 12:31:02 -0500 Subject: [PATCH 144/148] fix test --- .../java/ca/uhn/fhir/jpa/test/config/TestR5Config.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR5Config.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR5Config.java index 472b0dfdad87..9f3032854f56 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR5Config.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR5Config.java @@ -80,11 +80,12 @@ public class TestR5Config { * and catch any potential deadlocks caused by database connection * starvation * - * A minimum of 2 is necessary for most transactions, - * so 2 will be our limit + * A minimum of 3 is necessary for most transactions, + * so 3 will be our minimum */ if (ourMaxThreads == null) { - ourMaxThreads = (int) (Math.random() * 6.0) + 2; +// ourMaxThreads = (int) (Math.random() * 6.0) + 3; + ourMaxThreads = 3; if (HapiTestSystemProperties.isSingleDbConnectionEnabled()) { ourMaxThreads = 1; From b83339b30145e5df89d475479bbd9181f4b2f1f5 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 24 Dec 2024 12:32:50 -0500 Subject: [PATCH 145/148] fix test --- .../main/java/ca/uhn/fhir/jpa/test/config/TestR5Config.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR5Config.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR5Config.java index 9f3032854f56..ee51e31f65f1 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR5Config.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR5Config.java @@ -84,8 +84,7 @@ public class TestR5Config { * so 3 will be our minimum */ if (ourMaxThreads == null) { -// ourMaxThreads = (int) (Math.random() * 6.0) + 3; - ourMaxThreads = 3; + ourMaxThreads = (int) (Math.random() * 6.0) + 3; if (HapiTestSystemProperties.isSingleDbConnectionEnabled()) { ourMaxThreads = 1; From 5611079661cb3729d11ee526b127cddc79fc6e67 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 24 Dec 2024 13:16:10 -0500 Subject: [PATCH 146/148] fix test --- .../jpa/provider/r4/PatientMergeR4Test.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java index 0f7404c60873..f781f54befc8 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/PatientMergeR4Test.java @@ -67,6 +67,7 @@ public class PatientMergeR4Test extends BaseResourceProviderR4Test { public void after() throws Exception { super.after(); + myStorageSettings.setDefaultTransactionEntriesForWrite(new JpaStorageSettings().getDefaultTransactionEntriesForWrite()); myStorageSettings.setReuseCachedSearchResultsForMillis(new JpaStorageSettings().getReuseCachedSearchResultsForMillis()); } @@ -238,7 +239,7 @@ void testMerge_smallResourceLimit() { // exec assertThatThrownBy(() -> callMergeOperation(inParameters, false)) .isInstanceOf(PreconditionFailedException.class) - .satisfies(ex -> assertThat(extractFailureMessage((BaseServerResponseException) ex)).isEqualTo("Number of resources with references to "+ myTestHelper.getSourcePatientId() + " exceeds the resource-limit 5. Submit the request asynchronsly by adding the HTTP Header 'Prefer: respond-async'.")); + .satisfies(ex -> assertThat(extractFailureMessage((BaseServerResponseException) ex)).isEqualTo("HAPI-2597: Number of resources with references to "+ myTestHelper.getSourcePatientId() + " exceeds the resource-limit 5. Submit the request asynchronsly by adding the HTTP Header 'Prefer: respond-async'.")); } @Test @@ -249,7 +250,7 @@ void testMerge_SourceResourceCannotBeDeletedBecauseAnotherResourceReferencingSou //using a small batch size that would result in multiple chunks to ensure that //the job runs a bit slowly so that we have sometime to add a resource that references the source //after the first step - inParams.resourceLimit = 5; + myStorageSettings.setDefaultTransactionEntriesForWrite(5); Parameters inParameters = inParams.asParametersResource(); // exec @@ -371,11 +372,15 @@ public void handleTestExecutionException(ExtensionContext theExtensionContext, T private @Nonnull String extractFailureMessage(BaseServerResponseException ex) { String body = ex.getResponseBody(); - Parameters outParams = myFhirContext.newJsonParser().parseResource(Parameters.class, body); - OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); - return outcome.getIssue().stream() - .map(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics) - .collect(Collectors.joining(", ")); + if (body != null) { + Parameters outParams = myFhirContext.newJsonParser().parseResource(Parameters.class, body); + OperationOutcome outcome = (OperationOutcome) outParams.getParameter(OPERATION_MERGE_OUTPUT_PARAM_OUTCOME).getResource(); + return outcome.getIssue().stream() + .map(OperationOutcome.OperationOutcomeIssueComponent::getDiagnostics) + .collect(Collectors.joining(", ")); + } else { + return "null"; + } } @Override From 1dbac620134c41d34abc9872a974784306a1ec46 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 24 Dec 2024 14:43:39 -0500 Subject: [PATCH 147/148] fix test --- .../main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index 564a5d11fdc8..dfeed7bbb57f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.primitive.IdDt; @@ -58,7 +59,7 @@ public final class JpaSystemProvider extends BaseJpaSystemProvider { @Autowired - private RequestPartitionHelperSvc myRequestPartitionHelperSvc; + private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; @Description( "Marks all currently existing resources of a given type, or all resources of all types, for reindexing.") From 51cbe71eba2923fa83be4abb928989878f7b1121 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Tue, 24 Dec 2024 14:46:10 -0500 Subject: [PATCH 148/148] bump pom versions --- hapi-deployable-pom/pom.xml | 2 +- hapi-fhir-android/pom.xml | 2 +- hapi-fhir-base/pom.xml | 2 +- hapi-fhir-bom/pom.xml | 4 ++-- hapi-fhir-checkstyle/pom.xml | 2 +- hapi-fhir-cli/hapi-fhir-cli-api/pom.xml | 2 +- hapi-fhir-cli/hapi-fhir-cli-app/pom.xml | 2 +- hapi-fhir-cli/pom.xml | 2 +- hapi-fhir-client-apache-http5/pom.xml | 2 +- hapi-fhir-client-okhttp/pom.xml | 2 +- hapi-fhir-client/pom.xml | 2 +- hapi-fhir-converter/pom.xml | 2 +- hapi-fhir-dist/pom.xml | 2 +- hapi-fhir-docs/pom.xml | 2 +- hapi-fhir-jacoco/pom.xml | 2 +- hapi-fhir-jaxrsserver-base/pom.xml | 2 +- hapi-fhir-jpa/pom.xml | 2 +- hapi-fhir-jpaserver-base/pom.xml | 2 +- .../main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java | 1 - hapi-fhir-jpaserver-elastic-test-utilities/pom.xml | 2 +- hapi-fhir-jpaserver-hfql/pom.xml | 2 +- hapi-fhir-jpaserver-ips/pom.xml | 2 +- hapi-fhir-jpaserver-mdm/pom.xml | 2 +- hapi-fhir-jpaserver-model/pom.xml | 2 +- hapi-fhir-jpaserver-searchparam/pom.xml | 2 +- hapi-fhir-jpaserver-subscription/pom.xml | 2 +- hapi-fhir-jpaserver-test-dstu2/pom.xml | 2 +- hapi-fhir-jpaserver-test-dstu3/pom.xml | 2 +- hapi-fhir-jpaserver-test-r4/pom.xml | 2 +- hapi-fhir-jpaserver-test-r4b/pom.xml | 2 +- hapi-fhir-jpaserver-test-r5/pom.xml | 2 +- hapi-fhir-jpaserver-test-utilities/pom.xml | 2 +- hapi-fhir-jpaserver-uhnfhirtest/pom.xml | 2 +- hapi-fhir-server-cds-hooks/pom.xml | 2 +- hapi-fhir-server-mdm/pom.xml | 2 +- hapi-fhir-server-openapi/pom.xml | 2 +- hapi-fhir-server/pom.xml | 2 +- hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml | 2 +- hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml | 4 ++-- hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml | 2 +- hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml | 2 +- hapi-fhir-serviceloaders/pom.xml | 2 +- .../hapi-fhir-spring-boot-autoconfigure/pom.xml | 2 +- .../hapi-fhir-spring-boot-sample-client-apache/pom.xml | 2 +- .../hapi-fhir-spring-boot-sample-client-okhttp/pom.xml | 2 +- .../hapi-fhir-spring-boot-sample-server-jersey/pom.xml | 2 +- hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml | 2 +- hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml | 2 +- hapi-fhir-spring-boot/pom.xml | 2 +- hapi-fhir-sql-migrate/pom.xml | 2 +- hapi-fhir-storage-batch2-jobs/pom.xml | 2 +- hapi-fhir-storage-batch2-test-utilities/pom.xml | 2 +- hapi-fhir-storage-batch2/pom.xml | 2 +- hapi-fhir-storage-cr/pom.xml | 2 +- hapi-fhir-storage-mdm/pom.xml | 2 +- hapi-fhir-storage-test-utilities/pom.xml | 2 +- hapi-fhir-storage/pom.xml | 2 +- hapi-fhir-structures-dstu2.1/pom.xml | 2 +- hapi-fhir-structures-dstu2/pom.xml | 2 +- hapi-fhir-structures-dstu3/pom.xml | 2 +- hapi-fhir-structures-hl7org-dstu2/pom.xml | 2 +- hapi-fhir-structures-r4/pom.xml | 2 +- hapi-fhir-structures-r4b/pom.xml | 2 +- hapi-fhir-structures-r5/pom.xml | 2 +- hapi-fhir-test-utilities/pom.xml | 2 +- hapi-fhir-testpage-overlay/pom.xml | 2 +- hapi-fhir-validation-resources-dstu2.1/pom.xml | 2 +- hapi-fhir-validation-resources-dstu2/pom.xml | 2 +- hapi-fhir-validation-resources-dstu3/pom.xml | 2 +- hapi-fhir-validation-resources-r4/pom.xml | 2 +- hapi-fhir-validation-resources-r4b/pom.xml | 2 +- hapi-fhir-validation-resources-r5/pom.xml | 2 +- hapi-fhir-validation/pom.xml | 2 +- hapi-tinder-plugin/pom.xml | 2 +- hapi-tinder-test/pom.xml | 2 +- pom.xml | 4 ++-- tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml | 2 +- tests/hapi-fhir-base-test-mindeps-client/pom.xml | 2 +- tests/hapi-fhir-base-test-mindeps-server/pom.xml | 2 +- 79 files changed, 81 insertions(+), 82 deletions(-) diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index 9f7ed00f27aa..86d82d9379e8 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index 1214bb7c3ee0..83c2140b245b 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 303de59fc8f6..d5f8eefd21d9 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index 8eb733fffe3e..8f793eb84569 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -4,7 +4,7 @@ 4.0.0 ca.uhn.hapi.fhir hapi-fhir-bom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT pom HAPI FHIR BOM @@ -12,7 +12,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-checkstyle/pom.xml b/hapi-fhir-checkstyle/pom.xml index 872153e1c12b..e5ecadca0f08 100644 --- a/hapi-fhir-checkstyle/pom.xml +++ b/hapi-fhir-checkstyle/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml index 8634287b5442..91893f54d9fd 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml index 641e9b220d91..a6be9c682ce3 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir-cli - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index d20c929503f0..69bc9c4daee4 100644 --- a/hapi-fhir-cli/pom.xml +++ b/hapi-fhir-cli/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-client-apache-http5/pom.xml b/hapi-fhir-client-apache-http5/pom.xml index 0b07128d239e..2b6900fa63b8 100644 --- a/hapi-fhir-client-apache-http5/pom.xml +++ b/hapi-fhir-client-apache-http5/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index 03ef0f6d47b5..2e56ce4f53e6 100644 --- a/hapi-fhir-client-okhttp/pom.xml +++ b/hapi-fhir-client-okhttp/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml index c08fae28c8d1..e35e959013db 100644 --- a/hapi-fhir-client/pom.xml +++ b/hapi-fhir-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index 3d10a57bf71e..11b14ba36945 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index c1c00c2ee1a1..8a1dae57e7f6 100644 --- a/hapi-fhir-dist/pom.xml +++ b/hapi-fhir-dist/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml index 96fcb0df0c20..1421595fc568 100644 --- a/hapi-fhir-docs/pom.xml +++ b/hapi-fhir-docs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index 497b40cb9c25..9a86748436ba 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -11,7 +11,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index 07cd3ce9cc0f..dadc903e1465 100644 --- a/hapi-fhir-jaxrsserver-base/pom.xml +++ b/hapi-fhir-jaxrsserver-base/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpa/pom.xml b/hapi-fhir-jpa/pom.xml index d6636beece11..528e488ee930 100644 --- a/hapi-fhir-jpa/pom.xml +++ b/hapi-fhir-jpa/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 756c6520f3ef..0f9f573c8204 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java index dfeed7bbb57f..b0e882237629 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProvider.java @@ -26,7 +26,6 @@ import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; -import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml index 294dac1aab27..8a9a38fdabb8 100644 --- a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-hfql/pom.xml b/hapi-fhir-jpaserver-hfql/pom.xml index 1d310f05d698..0e23db4edbc1 100644 --- a/hapi-fhir-jpaserver-hfql/pom.xml +++ b/hapi-fhir-jpaserver-hfql/pom.xml @@ -3,7 +3,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-ips/pom.xml b/hapi-fhir-jpaserver-ips/pom.xml index e9a1ae702fe9..e975d0820298 100644 --- a/hapi-fhir-jpaserver-ips/pom.xml +++ b/hapi-fhir-jpaserver-ips/pom.xml @@ -3,7 +3,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-mdm/pom.xml b/hapi-fhir-jpaserver-mdm/pom.xml index 76b147a3fc21..771ff69dc498 100644 --- a/hapi-fhir-jpaserver-mdm/pom.xml +++ b/hapi-fhir-jpaserver-mdm/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index 537c53f65ff0..56dc6e50e32a 100644 --- a/hapi-fhir-jpaserver-model/pom.xml +++ b/hapi-fhir-jpaserver-model/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index 2957e4b357fd..c4f8781e796e 100755 --- a/hapi-fhir-jpaserver-searchparam/pom.xml +++ b/hapi-fhir-jpaserver-searchparam/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml index 5805778dc2c3..e9f3e8f80693 100644 --- a/hapi-fhir-jpaserver-subscription/pom.xml +++ b/hapi-fhir-jpaserver-subscription/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-dstu2/pom.xml b/hapi-fhir-jpaserver-test-dstu2/pom.xml index 38d2a47c79ff..35f2cd910c8d 100644 --- a/hapi-fhir-jpaserver-test-dstu2/pom.xml +++ b/hapi-fhir-jpaserver-test-dstu2/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-dstu3/pom.xml b/hapi-fhir-jpaserver-test-dstu3/pom.xml index 77bbc6d21254..e5a444ebdd32 100644 --- a/hapi-fhir-jpaserver-test-dstu3/pom.xml +++ b/hapi-fhir-jpaserver-test-dstu3/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r4/pom.xml b/hapi-fhir-jpaserver-test-r4/pom.xml index 783fdbaa7dc4..5a96f279d0d0 100644 --- a/hapi-fhir-jpaserver-test-r4/pom.xml +++ b/hapi-fhir-jpaserver-test-r4/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r4b/pom.xml b/hapi-fhir-jpaserver-test-r4b/pom.xml index 030df7804176..87bdf253448a 100644 --- a/hapi-fhir-jpaserver-test-r4b/pom.xml +++ b/hapi-fhir-jpaserver-test-r4b/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r5/pom.xml b/hapi-fhir-jpaserver-test-r5/pom.xml index 6f025d7b32a1..9ee03e7f63e7 100644 --- a/hapi-fhir-jpaserver-test-r5/pom.xml +++ b/hapi-fhir-jpaserver-test-r5/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-utilities/pom.xml b/hapi-fhir-jpaserver-test-utilities/pom.xml index 6337d9647557..4c7a5a99a7a1 100644 --- a/hapi-fhir-jpaserver-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-test-utilities/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 4c1300e5fe0b..145a5aed18d8 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-server-cds-hooks/pom.xml b/hapi-fhir-server-cds-hooks/pom.xml index f5386ced33ac..ceee8a42231d 100644 --- a/hapi-fhir-server-cds-hooks/pom.xml +++ b/hapi-fhir-server-cds-hooks/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server-mdm/pom.xml b/hapi-fhir-server-mdm/pom.xml index 48232abdc495..9e3545a2e00f 100644 --- a/hapi-fhir-server-mdm/pom.xml +++ b/hapi-fhir-server-mdm/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server-openapi/pom.xml b/hapi-fhir-server-openapi/pom.xml index 94ff32d0d56e..8dcbfe673d8f 100644 --- a/hapi-fhir-server-openapi/pom.xml +++ b/hapi-fhir-server-openapi/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index 03e901047879..2c7cd90992dc 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml index 925c5b26064c..f09365ac6fee 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml index 85ffd69b093d..6184ea3fbb65 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../pom.xml @@ -21,7 +21,7 @@ ca.uhn.hapi.fhir hapi-fhir-caching-api - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml index b3d1a76d6744..dde87cdb57ce 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml index e0cafcc2d812..2462783195d7 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml @@ -7,7 +7,7 @@ hapi-fhir ca.uhn.hapi.fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../../pom.xml diff --git a/hapi-fhir-serviceloaders/pom.xml b/hapi-fhir-serviceloaders/pom.xml index 8fb9053f275c..e34064f72bff 100644 --- a/hapi-fhir-serviceloaders/pom.xml +++ b/hapi-fhir-serviceloaders/pom.xml @@ -5,7 +5,7 @@ hapi-deployable-pom ca.uhn.hapi.fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml index 8e10c83d05c8..01ed191200d7 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml index 673f4ba06c8c..aef431beb382 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT hapi-fhir-spring-boot-sample-client-apache diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml index 3f72bcf0f545..7d6073019dab 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml index 5e76752983ee..1205f85c25b7 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml index b3230500c339..450044fa3f5c 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml index 6224ffffc240..a5505c221013 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/pom.xml b/hapi-fhir-spring-boot/pom.xml index 941d8d153969..ff840b5a7010 100644 --- a/hapi-fhir-spring-boot/pom.xml +++ b/hapi-fhir-spring-boot/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-sql-migrate/pom.xml b/hapi-fhir-sql-migrate/pom.xml index e856ba0eccba..028910783b62 100644 --- a/hapi-fhir-sql-migrate/pom.xml +++ b/hapi-fhir-sql-migrate/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2-jobs/pom.xml b/hapi-fhir-storage-batch2-jobs/pom.xml index af7d8a4ba3fd..8f172b4ffc0d 100644 --- a/hapi-fhir-storage-batch2-jobs/pom.xml +++ b/hapi-fhir-storage-batch2-jobs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2-test-utilities/pom.xml b/hapi-fhir-storage-batch2-test-utilities/pom.xml index 132506ac72eb..6abe157add62 100644 --- a/hapi-fhir-storage-batch2-test-utilities/pom.xml +++ b/hapi-fhir-storage-batch2-test-utilities/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2/pom.xml b/hapi-fhir-storage-batch2/pom.xml index 2c6054a32a5a..f5f8ee533b7a 100644 --- a/hapi-fhir-storage-batch2/pom.xml +++ b/hapi-fhir-storage-batch2/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-cr/pom.xml b/hapi-fhir-storage-cr/pom.xml index ec45cf1cccb9..ad335ec75b5b 100644 --- a/hapi-fhir-storage-cr/pom.xml +++ b/hapi-fhir-storage-cr/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-mdm/pom.xml b/hapi-fhir-storage-mdm/pom.xml index 6c2980b347ef..34019955f1b3 100644 --- a/hapi-fhir-storage-mdm/pom.xml +++ b/hapi-fhir-storage-mdm/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-test-utilities/pom.xml b/hapi-fhir-storage-test-utilities/pom.xml index 0e8dcfe6bb56..7c0e16c142e3 100644 --- a/hapi-fhir-storage-test-utilities/pom.xml +++ b/hapi-fhir-storage-test-utilities/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage/pom.xml b/hapi-fhir-storage/pom.xml index e3d63bd3ac20..d565fdcc2f1d 100644 --- a/hapi-fhir-storage/pom.xml +++ b/hapi-fhir-storage/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index 9aa50c1080cb..901e94989500 100644 --- a/hapi-fhir-structures-dstu2.1/pom.xml +++ b/hapi-fhir-structures-dstu2.1/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 88a52660638b..821818595997 100644 --- a/hapi-fhir-structures-dstu2/pom.xml +++ b/hapi-fhir-structures-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index 048cc7351c16..dee2018d312c 100644 --- a/hapi-fhir-structures-dstu3/pom.xml +++ b/hapi-fhir-structures-dstu3/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index 90f1c63fd951..9897236804fe 100644 --- a/hapi-fhir-structures-hl7org-dstu2/pom.xml +++ b/hapi-fhir-structures-hl7org-dstu2/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index 45715423d8dc..56f110cd4732 100644 --- a/hapi-fhir-structures-r4/pom.xml +++ b/hapi-fhir-structures-r4/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4b/pom.xml b/hapi-fhir-structures-r4b/pom.xml index 4d234e03c116..98cfe1bd0b93 100644 --- a/hapi-fhir-structures-r4b/pom.xml +++ b/hapi-fhir-structures-r4b/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r5/pom.xml b/hapi-fhir-structures-r5/pom.xml index 961aa4c015ba..9ea5a9380e4c 100644 --- a/hapi-fhir-structures-r5/pom.xml +++ b/hapi-fhir-structures-r5/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index 3f20fb916d44..f48a38d1efcd 100644 --- a/hapi-fhir-test-utilities/pom.xml +++ b/hapi-fhir-test-utilities/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index e36bb384c964..8dc3380dddac 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-validation-resources-dstu2.1/pom.xml b/hapi-fhir-validation-resources-dstu2.1/pom.xml index 24ddd7da033f..d8804878cd59 100644 --- a/hapi-fhir-validation-resources-dstu2.1/pom.xml +++ b/hapi-fhir-validation-resources-dstu2.1/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-dstu2/pom.xml b/hapi-fhir-validation-resources-dstu2/pom.xml index 617648dc364c..df7f21cbbef9 100644 --- a/hapi-fhir-validation-resources-dstu2/pom.xml +++ b/hapi-fhir-validation-resources-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-dstu3/pom.xml b/hapi-fhir-validation-resources-dstu3/pom.xml index 5ea89fc67fc2..7b487a8622a1 100644 --- a/hapi-fhir-validation-resources-dstu3/pom.xml +++ b/hapi-fhir-validation-resources-dstu3/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r4/pom.xml b/hapi-fhir-validation-resources-r4/pom.xml index 1dad8ee8663d..2103ecad9c3c 100644 --- a/hapi-fhir-validation-resources-r4/pom.xml +++ b/hapi-fhir-validation-resources-r4/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r4b/pom.xml b/hapi-fhir-validation-resources-r4b/pom.xml index 136214b8ded4..b6d795ebe7bf 100644 --- a/hapi-fhir-validation-resources-r4b/pom.xml +++ b/hapi-fhir-validation-resources-r4b/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r5/pom.xml b/hapi-fhir-validation-resources-r5/pom.xml index 0d974f262eea..77f2a2869177 100644 --- a/hapi-fhir-validation-resources-r5/pom.xml +++ b/hapi-fhir-validation-resources-r5/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml index 58a8c15a5d2b..6ed7a2f03ce7 100644 --- a/hapi-fhir-validation/pom.xml +++ b/hapi-fhir-validation/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index a93556ea13e9..677a3b69e770 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../pom.xml diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml index 568fbaef8792..9ef1aeb85123 100644 --- a/hapi-tinder-test/pom.xml +++ b/hapi-tinder-test/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 7b54f4fb5303..4cd901610e85 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ ca.uhn.hapi.fhir hapi-fhir pom - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT HAPI-FHIR An open-source implementation of the FHIR specification in Java. @@ -2669,7 +2669,7 @@ ca.uhn.hapi.fhir hapi-tinder-plugin - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT diff --git a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml index 90e79a595876..b62ed06354cc 100644 --- a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml +++ b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-client/pom.xml b/tests/hapi-fhir-base-test-mindeps-client/pom.xml index 9635a9280ae8..0616cff36858 100644 --- a/tests/hapi-fhir-base-test-mindeps-client/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-server/pom.xml b/tests/hapi-fhir-base-test-mindeps-server/pom.xml index 0dfe0e34ed4a..6263da7433f7 100644 --- a/tests/hapi-fhir-base-test-mindeps-server/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 7.7.15-SNAPSHOT + 7.7.16-SNAPSHOT ../../pom.xml