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());
+ }
+}