diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParametersBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParametersBuilder.java new file mode 100644 index 000000000000..81006514805e --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParametersBuilder.java @@ -0,0 +1,225 @@ +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2025 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; + +// Created by claude-sonnet-4-5 + +import ca.uhn.fhir.context.BaseRuntimeChildDefinition; +import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.IProvenanceAgent; +import ca.uhn.fhir.util.CanonicalIdentifier; +import ca.uhn.fhir.util.ParametersUtil; +import org.hl7.fhir.instance.model.api.IBase; +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 java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Builder for creating MergeOperationInputParameters from FHIR Parameters resource. + * This enables programmatic invocation of the merge operation outside of REST contexts. + */ +public class MergeOperationInputParametersBuilder { + + private final FhirContext myFhirContext; + private final int myResourceLimit; + + /** + * Create a new builder for MergeOperationInputParameters. + * + * @param theFhirContext the FHIR context + * @param theResourceLimit the maximum number of resources to process + */ + public MergeOperationInputParametersBuilder(FhirContext theFhirContext, int theResourceLimit) { + myFhirContext = theFhirContext; + myResourceLimit = theResourceLimit; + } + + /** + * Build MergeOperationInputParameters from REST operation parameters. + * This method is used by REST providers that receive individual operation parameters. + * + * @param theSourcePatientIdentifier list of source patient identifiers + * @param theTargetPatientIdentifier list of target patient identifiers + * @param theSourcePatient source patient reference + * @param theTargetPatient target patient reference + * @param thePreview preview flag + * @param theDeleteSource delete source flag + * @param theResultPatient result patient resource + * @param theProvenanceAgents provenance agents for audit + * @param theOriginalInputParameters original input parameters for provenance + * @return MergeOperationInputParameters ready for use with ResourceMergeService + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public MergeOperationInputParameters fromOperationParams( + List theSourcePatientIdentifier, + List theTargetPatientIdentifier, + IBaseReference theSourcePatient, + IBaseReference theTargetPatient, + IPrimitiveType thePreview, + IPrimitiveType theDeleteSource, + IBaseResource theResultPatient, + List theProvenanceAgents, + IBaseResource theOriginalInputParameters) { + + MergeOperationInputParameters result = new MergeOperationInputParameters(myResourceLimit); + + // Set identifiers + if (theSourcePatientIdentifier != null && !theSourcePatientIdentifier.isEmpty()) { + List sourceIds = theSourcePatientIdentifier.stream() + .map(id -> CanonicalIdentifier.fromIdentifier((IBase) id)) + .collect(Collectors.toList()); + result.setSourceResourceIdentifiers(sourceIds); + } + + if (theTargetPatientIdentifier != null && !theTargetPatientIdentifier.isEmpty()) { + List targetIds = theTargetPatientIdentifier.stream() + .map(id -> CanonicalIdentifier.fromIdentifier((IBase) id)) + .collect(Collectors.toList()); + result.setTargetResourceIdentifiers(targetIds); + } + + // Set references + result.setSourceResource(theSourcePatient); + result.setTargetResource(theTargetPatient); + + // Set flags + result.setPreview(thePreview != null && thePreview.getValue()); + result.setDeleteSource(theDeleteSource != null && theDeleteSource.getValue()); + + // Set result patient (make a copy to avoid modification) + if (theResultPatient != null) { + result.setResultResource(((org.hl7.fhir.r4.model.Patient) theResultPatient).copy()); + } + + // Set provenance and original parameters + result.setProvenanceAgents(theProvenanceAgents); + if (theOriginalInputParameters != null) { + result.setOriginalInputParameters(((org.hl7.fhir.r4.model.Resource) theOriginalInputParameters).copy()); + } + + return result; + } + + /** + * Build MergeOperationInputParameters from a FHIR Parameters resource. + * Extracts all merge operation parameters according to the FHIR spec. + * + * @param theParameters FHIR Parameters resource containing merge operation inputs + * @return MergeOperationInputParameters ready for use with ResourceMergeService + */ + public MergeOperationInputParameters fromParameters(IBaseParameters theParameters) { + MergeOperationInputParameters result = new MergeOperationInputParameters(myResourceLimit); + + // Extract source-patient-identifier (list of identifiers) + List sourceIdentifierParams = + ParametersUtil.getNamedParameters(myFhirContext, theParameters, "source-patient-identifier"); + if (!sourceIdentifierParams.isEmpty()) { + List sourceIds = sourceIdentifierParams.stream() + .map(param -> extractValueFromParameter(param, "value")) + .filter(Objects::nonNull) + .map(CanonicalIdentifier::fromIdentifier) + .collect(Collectors.toList()); + if (!sourceIds.isEmpty()) { + result.setSourceResourceIdentifiers(sourceIds); + } + } + + // Extract target-patient-identifier (list of identifiers) + List targetIdentifierParams = + ParametersUtil.getNamedParameters(myFhirContext, theParameters, "target-patient-identifier"); + if (!targetIdentifierParams.isEmpty()) { + List targetIds = targetIdentifierParams.stream() + .map(param -> extractValueFromParameter(param, "value")) + .filter(Objects::nonNull) + .map(CanonicalIdentifier::fromIdentifier) + .collect(Collectors.toList()); + if (!targetIds.isEmpty()) { + result.setTargetResourceIdentifiers(targetIds); + } + } + + // Extract source-patient reference + List sourcePatientRefs = + ParametersUtil.getNamedParameterReferences(myFhirContext, theParameters, "source-patient"); + if (!sourcePatientRefs.isEmpty()) { + result.setSourceResource(sourcePatientRefs.get(0)); + } + + // Extract target-patient reference + List targetPatientRefs = + ParametersUtil.getNamedParameterReferences(myFhirContext, theParameters, "target-patient"); + if (!targetPatientRefs.isEmpty()) { + result.setTargetResource(targetPatientRefs.get(0)); + } + + // Extract preview flag + Optional previewValue = + ParametersUtil.getNamedParameterValueAsString(myFhirContext, theParameters, "preview"); + previewValue.ifPresent(val -> result.setPreview(Boolean.parseBoolean(val))); + + // Extract delete-source flag + Optional deleteSourceValue = + ParametersUtil.getNamedParameterValueAsString(myFhirContext, theParameters, "delete-source"); + deleteSourceValue.ifPresent(val -> result.setDeleteSource(Boolean.parseBoolean(val))); + + // Extract result-patient + ParametersUtil.getNamedParameterResource(myFhirContext, theParameters, "result-patient") + .ifPresent(result::setResultResource); + + // Store original parameters for provenance + result.setOriginalInputParameters(theParameters); + + return result; + } + + /** + * Extract a value[x] from a Parameters.parameter element. + * In FHIR Parameters, values are stored as value[x] which expands to valueString, valueIdentifier, etc. + * + * @param theParameter the parameter element + * @param theChildNamePrefix the child name prefix (e.g., "value" will match valueIdentifier, valueString, etc.) + * @return the child value or null if not found + */ + private IBase extractValueFromParameter( + IBase theParameter, @SuppressWarnings("SameParameterValue") String theChildNamePrefix) { + + BaseRuntimeElementCompositeDefinition parameterDef = + (BaseRuntimeElementCompositeDefinition) myFhirContext.getElementDefinition(theParameter.getClass()); + + // Try to find a child that starts with the prefix (e.g., "value" matches "valueIdentifier") + for (BaseRuntimeChildDefinition childDef : parameterDef.getChildren()) { + String childName = childDef.getElementName(); + if (childName.startsWith(theChildNamePrefix)) { + List values = childDef.getAccessor().getValues(theParameter); + if (!values.isEmpty()) { + return values.get(0); + } + } + } + return null; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationParametersUtil.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationParametersUtil.java new file mode 100644 index 000000000000..da7f569cc4a3 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationParametersUtil.java @@ -0,0 +1,116 @@ +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2025 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; + +// Created by claude-sonnet-4-5 + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.server.provider.ProviderConstants; +import ca.uhn.fhir.util.ParametersUtil; +import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IBaseResource; + +/** + * Utility class for building FHIR Parameters resources for merge operations. + */ +public class MergeOperationParametersUtil { + + private MergeOperationParametersUtil() { + // Utility class + } + + /** + * Builds the output Parameters resource for a Patient $merge operation following the FHIR specification. + *

+ * The output Parameters includes: + *

    + *
  • input - The original input parameters
  • + *
  • outcome - OperationOutcome describing the merge result
  • + *
  • result - Updated target patient (optional, not included in preview mode)
  • + *
  • task - Task resource tracking the merge (optional)
  • + *
+ *

+ * + * @param theFhirContext FHIR context for building Parameters resource + * @param theMergeOutcome Merge operation outcome containing the result + * @param theInputParameters Original input parameters to include in output + * @return Parameters resource with merge operation output in FHIR $merge format + */ + public static IBaseParameters buildMergeOperationOutputParameters( + FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome, IBaseResource theInputParameters) { + + // Extract components from MergeOperationOutcome and delegate to overloaded method + return buildMergeOperationOutputParameters( + theFhirContext, + theMergeOutcome.getOperationOutcome(), + theMergeOutcome.getUpdatedTargetResource(), + theMergeOutcome.getTask(), + theInputParameters); + } + + /** + * Builds the output Parameters resource for a Patient $merge operation from individual components. + *

+ * This overload is useful when you have the merge result components separately rather than + * wrapped in a {@link MergeOperationOutcome} object. + *

+ * + * @param theFhirContext FHIR context for building Parameters resource + * @param theOperationOutcome Operation outcome describing the merge result + * @param theUpdatedTargetResource Updated target patient resource (may be null in preview mode) + * @param theTask Task resource tracking the merge operation (may be null) + * @param theInputParameters Original input parameters to include in output + * @return Parameters resource with merge operation output in FHIR $merge format + */ + public static IBaseParameters buildMergeOperationOutputParameters( + FhirContext theFhirContext, + IBaseResource theOperationOutcome, + IBaseResource theUpdatedTargetResource, + IBaseResource theTask, + IBaseResource theInputParameters) { + + IBaseParameters retVal = ParametersUtil.newInstance(theFhirContext); + + // Add input parameters + ParametersUtil.addParameterToParameters( + theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT, theInputParameters); + + // Add operation outcome + ParametersUtil.addParameterToParameters( + theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME, theOperationOutcome); + + // Add updated target resource if present + if (theUpdatedTargetResource != null) { + ParametersUtil.addParameterToParameters( + theFhirContext, + retVal, + ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT, + theUpdatedTargetResource); + } + + // Add task if present + if (theTask != null) { + ParametersUtil.addParameterToParameters( + theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK, theTask); + } + + return retVal; + } +} 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 a2584e8516bd..3adcd1c6892c 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 @@ -41,13 +41,10 @@ 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.Resource; 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; @@ -109,7 +106,10 @@ public IBaseParameters patientMerge( List provenanceAgents = ProvenanceAgentsPointcutUtil.ifHasCallHooks(theRequestDetails, myInterceptorBroadcaster); - MergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( + // Use the builder to construct MergeOperationInputParameters + MergeOperationInputParametersBuilder builder = + new MergeOperationInputParametersBuilder(myFhirContext, resourceLimit); + MergeOperationInputParameters mergeOperationParameters = builder.fromOperationParams( theSourcePatientIdentifier, theTargetPatientIdentifier, theSourcePatient, @@ -117,7 +117,6 @@ public IBaseParameters patientMerge( thePreview, theDeleteSource, theResultPatient, - resourceLimit, provenanceAgents, theRequestDetails.getResource()); @@ -125,7 +124,8 @@ public IBaseParameters patientMerge( myResourceMergeService.merge(mergeOperationParameters, theRequestDetails); theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); - return buildMergeOperationOutputParameters(myFhirContext, mergeOutcome, theRequestDetails.getResource()); + return MergeOperationParametersUtil.buildMergeOperationOutputParameters( + myFhirContext, mergeOutcome, theRequestDetails.getResource()); } finally { endRequest(theServletRequest); } @@ -172,37 +172,6 @@ public IBaseParameters patientUndoMerge( } } - 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 UndoMergeOperationInputParameters buildUndoMergeOperationInputParameters( List theSourcePatientIdentifier, List theTargetPatientIdentifier, @@ -245,39 +214,4 @@ private void setCommonMergeOperationInputParameters( theMergeOperationParameters.setSourceResource(theSourcePatient); theMergeOperationParameters.setTargetResource(theTargetPatient); } - - private MergeOperationInputParameters buildMergeOperationInputParameters( - List theSourcePatientIdentifier, - List theTargetPatientIdentifier, - IBaseReference theSourcePatient, - IBaseReference theTargetPatient, - IPrimitiveType thePreview, - IPrimitiveType theDeleteSource, - IBaseResource theResultPatient, - int theResourceLimit, - List theProvenanceAgents, - IBaseResource theOriginalInputParameters) { - - MergeOperationInputParameters mergeOperationParameters = new MergeOperationInputParameters(theResourceLimit); - - setCommonMergeOperationInputParameters( - mergeOperationParameters, - theSourcePatientIdentifier, - theTargetPatientIdentifier, - theSourcePatient, - 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()); - } - - mergeOperationParameters.setProvenanceAgents(theProvenanceAgents); - mergeOperationParameters.setOriginalInputParameters(((Resource) theOriginalInputParameters).copy()); - return mergeOperationParameters; - } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParametersBuilderTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParametersBuilderTest.java new file mode 100644 index 000000000000..429e5ded85e9 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationInputParametersBuilderTest.java @@ -0,0 +1,225 @@ +package ca.uhn.fhir.jpa.provider.merge; + +// Created by claude-sonnet-4-5 + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.util.CanonicalIdentifier; +import org.hl7.fhir.r4.model.BooleanType; +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.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class MergeOperationInputParametersBuilderTest { + + private static final int RESOURCE_LIMIT = 1000; + private MergeOperationInputParametersBuilder myBuilder; + + @BeforeEach + void setUp() { + FhirContext fhirContext = FhirContext.forR4Cached(); + myBuilder = new MergeOperationInputParametersBuilder(fhirContext, RESOURCE_LIMIT); + } + + @Test + void testFromParameters_withAllParameters_success() { + // Arrange + Parameters parameters = new Parameters(); + parameters.addParameter().setName("source-patient").setValue(new Reference("Patient/123")); + parameters.addParameter().setName("target-patient").setValue(new Reference("Patient/456")); + parameters.addParameter().setName("preview").setValue(new BooleanType(true)); + parameters.addParameter().setName("delete-source").setValue(new BooleanType(true)); + + Patient resultPatient = new Patient(); + resultPatient.setId("Patient/789"); + parameters.addParameter().setName("result-patient").setResource(resultPatient); + + // Act + MergeOperationInputParameters result = myBuilder.fromParameters(parameters); + + // Assert + assertThat(result).isNotNull(); + assertThat(result.getSourceResource()).isNotNull(); + assertThat(result.getSourceResource().getReferenceElement().getValue()).isEqualTo("Patient/123"); + assertThat(result.getTargetResource()).isNotNull(); + assertThat(result.getTargetResource().getReferenceElement().getValue()).isEqualTo("Patient/456"); + assertThat(result.getPreview()).isTrue(); + assertThat(result.getDeleteSource()).isTrue(); + assertThat(result.getResultResource()).isNotNull(); + assertThat(result.getOriginalInputParameters()).isNotNull(); + assertThat(result.getResourceLimit()).isEqualTo(RESOURCE_LIMIT); + } + + @Test + void testFromParameters_withOnlyRequiredParameters_success() { + // Arrange + Parameters parameters = new Parameters(); + parameters.addParameter().setName("source-patient").setValue(new Reference("Patient/123")); + parameters.addParameter().setName("target-patient").setValue(new Reference("Patient/456")); + + // Act + MergeOperationInputParameters result = myBuilder.fromParameters(parameters); + + // Assert + assertThat(result).isNotNull(); + assertThat(result.getSourceResource()).isNotNull(); + assertThat(result.getTargetResource()).isNotNull(); + assertThat(result.getPreview()).isFalse(); // default is false + assertThat(result.getDeleteSource()).isFalse(); // default is false + assertThat(result.getResultResource()).isNull(); + assertThat(result.getOriginalInputParameters()).isNotNull(); + } + + @Test + void testFromParameters_withIdentifiers_success() { + // Arrange + Parameters parameters = new Parameters(); + + Identifier sourceIdentifier = new Identifier(); + sourceIdentifier.setSystem("http://example.com/mrn"); + sourceIdentifier.setValue("12345"); + parameters.addParameter().setName("source-patient-identifier").setValue(sourceIdentifier); + + Identifier targetIdentifier = new Identifier(); + targetIdentifier.setSystem("http://example.com/mrn"); + targetIdentifier.setValue("67890"); + parameters.addParameter().setName("target-patient-identifier").setValue(targetIdentifier); + + // Act + MergeOperationInputParameters result = myBuilder.fromParameters(parameters); + + // Assert + assertThat(result).isNotNull(); + assertThat(result.getSourceIdentifiers()).isNotNull(); + assertThat(result.getSourceIdentifiers()).hasSize(1); + CanonicalIdentifier sourceCanonical = result.getSourceIdentifiers().get(0); + assertThat(sourceCanonical.getSystemElement().getValue()).isEqualTo("http://example.com/mrn"); + assertThat(sourceCanonical.getValueElement().getValue()).isEqualTo("12345"); + + assertThat(result.getTargetIdentifiers()).isNotNull(); + assertThat(result.getTargetIdentifiers()).hasSize(1); + CanonicalIdentifier targetCanonical = result.getTargetIdentifiers().get(0); + assertThat(targetCanonical.getSystemElement().getValue()).isEqualTo("http://example.com/mrn"); + assertThat(targetCanonical.getValueElement().getValue()).isEqualTo("67890"); + } + + @Test + void testFromParameters_withMultipleIdentifiers_success() { + // Arrange + Parameters parameters = new Parameters(); + + Identifier sourceIdentifier1 = new Identifier(); + sourceIdentifier1.setSystem("http://example.com/mrn"); + sourceIdentifier1.setValue("12345"); + parameters.addParameter().setName("source-patient-identifier").setValue(sourceIdentifier1); + + Identifier sourceIdentifier2 = new Identifier(); + sourceIdentifier2.setSystem("http://example.com/ssn"); + sourceIdentifier2.setValue("999-99-9999"); + parameters.addParameter().setName("source-patient-identifier").setValue(sourceIdentifier2); + + Identifier targetIdentifier = new Identifier(); + targetIdentifier.setSystem("http://example.com/mrn"); + targetIdentifier.setValue("67890"); + parameters.addParameter().setName("target-patient-identifier").setValue(targetIdentifier); + + // Act + MergeOperationInputParameters result = myBuilder.fromParameters(parameters); + + // Assert + assertThat(result).isNotNull(); + assertThat(result.getSourceIdentifiers()).hasSize(2); + assertThat(result.getTargetIdentifiers()).hasSize(1); + } + + @Test + void testFromParameters_withReferences_success() { + // Arrange + Parameters parameters = new Parameters(); + parameters.addParameter().setName("source-patient").setValue(new Reference("Patient/source-123")); + parameters.addParameter().setName("target-patient").setValue(new Reference("Patient/target-456")); + + // Act + MergeOperationInputParameters result = myBuilder.fromParameters(parameters); + + // Assert + assertThat(result).isNotNull(); + assertThat(result.getSourceResource()).isNotNull(); + assertThat(result.getSourceResource().getReferenceElement().getValue()).isEqualTo("Patient/source-123"); + assertThat(result.getTargetResource()).isNotNull(); + assertThat(result.getTargetResource().getReferenceElement().getValue()).isEqualTo("Patient/target-456"); + } + + @Test + void testFromParameters_withPreviewTrue_setsFlag() { + // Arrange + Parameters parameters = new Parameters(); + parameters.addParameter().setName("source-patient").setValue(new Reference("Patient/123")); + parameters.addParameter().setName("target-patient").setValue(new Reference("Patient/456")); + parameters.addParameter().setName("preview").setValue(new BooleanType(true)); + + // Act + MergeOperationInputParameters result = myBuilder.fromParameters(parameters); + + // Assert + assertThat(result.getPreview()).isTrue(); + } + + @Test + void testFromParameters_withPreviewFalse_setsFlag() { + // Arrange + Parameters parameters = new Parameters(); + parameters.addParameter().setName("source-patient").setValue(new Reference("Patient/123")); + parameters.addParameter().setName("target-patient").setValue(new Reference("Patient/456")); + parameters.addParameter().setName("preview").setValue(new BooleanType(false)); + + // Act + MergeOperationInputParameters result = myBuilder.fromParameters(parameters); + + // Assert + assertThat(result.getPreview()).isFalse(); + } + + @Test + void testFromParameters_withDeleteSourceTrue_setsFlag() { + // Arrange + Parameters parameters = new Parameters(); + parameters.addParameter().setName("source-patient").setValue(new Reference("Patient/123")); + parameters.addParameter().setName("target-patient").setValue(new Reference("Patient/456")); + parameters.addParameter().setName("delete-source").setValue(new BooleanType(true)); + + // Act + MergeOperationInputParameters result = myBuilder.fromParameters(parameters); + + // Assert + assertThat(result.getDeleteSource()).isTrue(); + } + + @Test + void testFromParameters_withResultPatient_storesResource() { + // Arrange + Parameters parameters = new Parameters(); + parameters.addParameter().setName("source-patient").setValue(new Reference("Patient/123")); + parameters.addParameter().setName("target-patient").setValue(new Reference("Patient/456")); + + Patient resultPatient = new Patient(); + resultPatient.setId("Patient/result"); + resultPatient.addName().setFamily("ResultFamily"); + parameters.addParameter().setName("result-patient").setResource(resultPatient); + + // Act + MergeOperationInputParameters result = myBuilder.fromParameters(parameters); + + // Assert + assertThat(result.getResultResource()).isNotNull(); + assertThat(result.getResultResource()).isInstanceOf(Patient.class); + Patient storedPatient = (Patient) result.getResultResource(); + assertThat(storedPatient.getIdElement().getValue()).isEqualTo("Patient/result"); + assertThat(storedPatient.getName()).hasSize(1); + assertThat(storedPatient.getName().get(0).getFamily()).isEqualTo("ResultFamily"); + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationParametersUtilTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationParametersUtilTest.java new file mode 100644 index 000000000000..825ad3217938 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/merge/MergeOperationParametersUtilTest.java @@ -0,0 +1,219 @@ +package ca.uhn.fhir.jpa.provider.merge; + +// Created by claude-sonnet-4-5 + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.server.provider.ProviderConstants; +import ca.uhn.fhir.util.ParametersUtil; +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.hl7.fhir.r4.model.Task; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class MergeOperationParametersUtilTest { + + private FhirContext myFhirContext; + private Parameters myInputParameters; + private OperationOutcome myOperationOutcome; + private Patient myUpdatedPatient; + private Task myTask; + + @BeforeEach + void setUp() { + myFhirContext = FhirContext.forR4Cached(); + + // Create test input parameters + myInputParameters = new Parameters(); + myInputParameters.addParameter().setName("source-patient").setValue(new Reference("Patient/source")); + myInputParameters.addParameter().setName("target-patient").setValue(new Reference("Patient/target")); + + // Create test operation outcome + myOperationOutcome = new OperationOutcome(); + myOperationOutcome.addIssue() + .setSeverity(OperationOutcome.IssueSeverity.INFORMATION) + .setCode(OperationOutcome.IssueType.INFORMATIONAL) + .setDiagnostics("Merge completed successfully"); + + // Create test updated patient + myUpdatedPatient = new Patient(); + myUpdatedPatient.setId("Patient/target"); + myUpdatedPatient.setActive(true); + + // Create test task + myTask = new Task(); + myTask.setId("Task/merge-123"); + myTask.setStatus(Task.TaskStatus.COMPLETED); + } + + @Test + void testBuildOutputParameters_withMergeOperationOutcome_allComponentsPresent() { + // Arrange + MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); + mergeOutcome.setOperationOutcome(myOperationOutcome); + mergeOutcome.setUpdatedTargetResource(myUpdatedPatient); + mergeOutcome.setTask(myTask); + mergeOutcome.setHttpStatusCode(200); + + // Act + Parameters result = (Parameters) MergeOperationParametersUtil.buildMergeOperationOutputParameters( + myFhirContext, mergeOutcome, myInputParameters); + + // Assert + assertThat(result).isNotNull(); + + // Verify input parameter + assertThat(ParametersUtil.getNamedParameters(myFhirContext, result, + ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT)) + .hasSize(1); + + // Verify operation outcome + OperationOutcome outcome = (OperationOutcome) ParametersUtil.getNamedParameterResource( + myFhirContext, result, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME) + .orElseThrow(); + assertThat(outcome.getIssueFirstRep().getDiagnostics()).isEqualTo("Merge completed successfully"); + + // Verify result patient + Patient resultPatient = (Patient) ParametersUtil.getNamedParameterResource( + myFhirContext, result, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT) + .orElseThrow(); + assertThat(resultPatient.getId()).isEqualTo("Patient/target"); + + // Verify task + Task resultTask = (Task) ParametersUtil.getNamedParameterResource( + myFhirContext, result, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK) + .orElseThrow(); + assertThat(resultTask.getId()).isEqualTo("Task/merge-123"); + } + + @Test + void testBuildOutputParameters_withMergeOperationOutcome_optionalComponentsNull() { + // Arrange - outcome with null updatedTargetResource and task (preview mode) + MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); + mergeOutcome.setOperationOutcome(myOperationOutcome); + mergeOutcome.setUpdatedTargetResource(null); + mergeOutcome.setTask(null); + mergeOutcome.setHttpStatusCode(200); + + // Act + Parameters result = (Parameters) MergeOperationParametersUtil.buildMergeOperationOutputParameters( + myFhirContext, mergeOutcome, myInputParameters); + + // Assert + assertThat(result).isNotNull(); + + // Verify input parameter present + assertThat(ParametersUtil.getNamedParameters(myFhirContext, result, + ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT)) + .hasSize(1); + + // Verify operation outcome present + assertThat(ParametersUtil.getNamedParameterResource( + myFhirContext, result, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME)) + .isPresent(); + + // Verify result patient NOT present + assertThat(ParametersUtil.getNamedParameterResource( + myFhirContext, result, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT)) + .isEmpty(); + + // Verify task NOT present + assertThat(ParametersUtil.getNamedParameterResource( + myFhirContext, result, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK)) + .isEmpty(); + } + + @Test + void testBuildOutputParameters_withIndividualComponents_allComponentsPresent() { + // Act + Parameters result = (Parameters) MergeOperationParametersUtil.buildMergeOperationOutputParameters( + myFhirContext, + myOperationOutcome, + myUpdatedPatient, + myTask, + myInputParameters); + + // Assert + assertThat(result).isNotNull(); + + // Verify all components present + assertThat(ParametersUtil.getNamedParameters(myFhirContext, result, + ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT)) + .hasSize(1); + assertThat(ParametersUtil.getNamedParameterResource( + myFhirContext, result, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME)) + .isPresent(); + assertThat(ParametersUtil.getNamedParameterResource( + myFhirContext, result, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT)) + .isPresent(); + assertThat(ParametersUtil.getNamedParameterResource( + myFhirContext, result, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK)) + .isPresent(); + } + + @Test + void testBuildOutputParameters_withIndividualComponents_optionalComponentsNull() { + // Act + Parameters result = (Parameters) MergeOperationParametersUtil.buildMergeOperationOutputParameters( + myFhirContext, + myOperationOutcome, + null, // no updated patient + null, // no task + myInputParameters); + + // Assert + assertThat(result).isNotNull(); + + // Verify required components present + assertThat(ParametersUtil.getNamedParameters(myFhirContext, result, + ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT)) + .hasSize(1); + assertThat(ParametersUtil.getNamedParameterResource( + myFhirContext, result, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME)) + .isPresent(); + + // Verify optional components NOT present + assertThat(ParametersUtil.getNamedParameterResource( + myFhirContext, result, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT)) + .isEmpty(); + assertThat(ParametersUtil.getNamedParameterResource( + myFhirContext, result, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK)) + .isEmpty(); + } + + @Test + void testBuildOutputParameters_bothMethodsProduceSameResult() { + // Arrange + MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); + mergeOutcome.setOperationOutcome(myOperationOutcome); + mergeOutcome.setUpdatedTargetResource(myUpdatedPatient); + mergeOutcome.setTask(myTask); + mergeOutcome.setHttpStatusCode(200); + + // Act - call both methods + Parameters resultFromOutcome = (Parameters) MergeOperationParametersUtil.buildMergeOperationOutputParameters( + myFhirContext, mergeOutcome, myInputParameters); + + Parameters resultFromComponents = (Parameters) MergeOperationParametersUtil.buildMergeOperationOutputParameters( + myFhirContext, + myOperationOutcome, + myUpdatedPatient, + myTask, + myInputParameters); + + // Assert - both should produce equivalent results + assertThat(resultFromOutcome.getParameter()).hasSameSizeAs(resultFromComponents.getParameter()); + assertThat(resultFromOutcome.getParameter().get(0).getName()) + .isEqualTo(resultFromComponents.getParameter().get(0).getName()); + assertThat(resultFromOutcome.getParameter().get(1).getName()) + .isEqualTo(resultFromComponents.getParameter().get(1).getName()); + assertThat(resultFromOutcome.getParameter().get(2).getName()) + .isEqualTo(resultFromComponents.getParameter().get(2).getName()); + assertThat(resultFromOutcome.getParameter().get(3).getName()) + .isEqualTo(resultFromComponents.getParameter().get(3).getName()); + } +}