001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.provider.merge;
021
022// Created by claude-sonnet-4-5
023
024import ca.uhn.fhir.context.FhirContext;
025import ca.uhn.fhir.model.api.IProvenanceAgent;
026import ca.uhn.fhir.model.primitive.BooleanDt;
027import ca.uhn.fhir.rest.server.provider.ProviderConstants;
028import ca.uhn.fhir.util.CanonicalIdentifier;
029import ca.uhn.fhir.util.ParametersUtil;
030import org.hl7.fhir.instance.model.api.IBase;
031import org.hl7.fhir.instance.model.api.IBaseParameters;
032import org.hl7.fhir.instance.model.api.IBaseReference;
033import org.hl7.fhir.instance.model.api.IBaseResource;
034import org.hl7.fhir.instance.model.api.IPrimitiveType;
035import org.hl7.fhir.r4.model.Identifier;
036import org.hl7.fhir.r4.model.Patient;
037import org.hl7.fhir.r4.model.Resource;
038
039import java.util.List;
040import java.util.Objects;
041import java.util.stream.Collectors;
042
043import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_PARAM_DELETE_SOURCE;
044import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_PARAM_PREVIEW;
045import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_PARAM_RESULT_PATIENT;
046import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT;
047import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT_IDENTIFIER;
048import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT;
049import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT_IDENTIFIER;
050
051/**
052 * Utility class for building FHIR Parameters resources for merge operations.
053 */
054public class MergeOperationParametersUtil {
055
056        private MergeOperationParametersUtil() {
057                // Utility class
058        }
059
060        /**
061         * Builds the output Parameters resource for a Patient $merge operation following the FHIR specification.
062         * <p>
063         * The output Parameters includes:
064         * <ul>
065         *   <li>input - The original input parameters</li>
066         *   <li>outcome - OperationOutcome describing the merge result</li>
067         *   <li>result - Updated target patient (optional, not included in preview mode)</li>
068         *   <li>task - Task resource tracking the merge (optional)</li>
069         * </ul>
070         * </p>
071         *
072         * @param theFhirContext FHIR context for building Parameters resource
073         * @param theMergeOutcome Merge operation outcome containing the result
074         * @param theInputParameters Original input parameters to include in output
075         * @return Parameters resource with merge operation output in FHIR $merge format
076         */
077        public static IBaseParameters buildMergeOperationOutputParameters(
078                        FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome, IBaseResource theInputParameters) {
079
080                // Extract components from MergeOperationOutcome and delegate to overloaded method
081                return buildMergeOperationOutputParameters(
082                                theFhirContext,
083                                theMergeOutcome.getOperationOutcome(),
084                                theMergeOutcome.getUpdatedTargetResource(),
085                                theMergeOutcome.getTask(),
086                                theInputParameters);
087        }
088
089        /**
090         * Builds the output Parameters resource for a Patient $merge operation from individual components.
091         * <p>
092         * This overload is useful when you have the merge result components separately rather than
093         * wrapped in a {@link MergeOperationOutcome} object.
094         * </p>
095         *
096         * @param theFhirContext FHIR context for building Parameters resource
097         * @param theOperationOutcome Operation outcome describing the merge result
098         * @param theUpdatedTargetResource Updated target patient resource (may be null in preview mode)
099         * @param theTask Task resource tracking the merge operation (may be null)
100         * @param theInputParameters Original input parameters to include in output
101         * @return Parameters resource with merge operation output in FHIR $merge format
102         */
103        public static IBaseParameters buildMergeOperationOutputParameters(
104                        FhirContext theFhirContext,
105                        IBaseResource theOperationOutcome,
106                        IBaseResource theUpdatedTargetResource,
107                        IBaseResource theTask,
108                        IBaseResource theInputParameters) {
109
110                IBaseParameters retVal = ParametersUtil.newInstance(theFhirContext);
111
112                // Add input parameters
113                ParametersUtil.addParameterToParameters(
114                                theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT, theInputParameters);
115
116                // Add operation outcome
117                ParametersUtil.addParameterToParameters(
118                                theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME, theOperationOutcome);
119
120                // Add updated target resource if present
121                if (theUpdatedTargetResource != null) {
122                        ParametersUtil.addParameterToParameters(
123                                        theFhirContext,
124                                        retVal,
125                                        ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT,
126                                        theUpdatedTargetResource);
127                }
128
129                // Add task if present
130                if (theTask != null) {
131                        ParametersUtil.addParameterToParameters(
132                                        theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK, theTask);
133                }
134
135                return retVal;
136        }
137
138        /**
139         * Build MergeOperationInputParameters from REST operation parameters.
140         * This method is used by REST providers that receive individual operation parameters.
141         *
142         * @param theSourcePatientIdentifier list of source patient identifiers
143         * @param theTargetPatientIdentifier list of target patient identifiers
144         * @param theSourcePatient source patient reference
145         * @param theTargetPatient target patient reference
146         * @param thePreview preview flag
147         * @param theDeleteSource delete source flag
148         * @param theResultPatient result patient resource
149         * @param theProvenanceAgents provenance agents for audit
150         * @param theOriginalInputParameters original input parameters for provenance
151         * @return MergeOperationInputParameters ready for use with ResourceMergeService
152         */
153        @SuppressWarnings({"rawtypes", "unchecked"})
154        public static MergeOperationInputParameters inputParamsFromOperationParams(
155                        List<?> theSourcePatientIdentifier,
156                        List<?> theTargetPatientIdentifier,
157                        IBaseReference theSourcePatient,
158                        IBaseReference theTargetPatient,
159                        IPrimitiveType<Boolean> thePreview,
160                        IPrimitiveType<Boolean> theDeleteSource,
161                        IBaseResource theResultPatient,
162                        List<IProvenanceAgent> theProvenanceAgents,
163                        IBaseResource theOriginalInputParameters,
164                        int theResourceLimit) {
165
166                MergeOperationInputParameters result = new MergeOperationInputParameters(theResourceLimit);
167
168                // Set identifiers
169                if (theSourcePatientIdentifier != null && !theSourcePatientIdentifier.isEmpty()) {
170                        List sourceIds = theSourcePatientIdentifier.stream()
171                                        .map(id -> CanonicalIdentifier.fromIdentifier((IBase) id))
172                                        .collect(Collectors.toList());
173                        result.setSourceResourceIdentifiers(sourceIds);
174                }
175
176                if (theTargetPatientIdentifier != null && !theTargetPatientIdentifier.isEmpty()) {
177                        List targetIds = theTargetPatientIdentifier.stream()
178                                        .map(id -> CanonicalIdentifier.fromIdentifier((IBase) id))
179                                        .collect(Collectors.toList());
180                        result.setTargetResourceIdentifiers(targetIds);
181                }
182
183                // Set references
184                result.setSourceResource(theSourcePatient);
185                result.setTargetResource(theTargetPatient);
186
187                // Set flags
188                result.setPreview(thePreview != null && thePreview.getValue());
189                result.setDeleteSource(theDeleteSource != null && theDeleteSource.getValue());
190
191                // pass in a copy of the result patient as we don't want it to be modified. It will be
192                // returned back to the client as part of the response.
193                if (theResultPatient != null) {
194                        result.setResultResource(((Patient) theResultPatient).copy());
195                }
196
197                // Set provenance and original parameters
198                result.setProvenanceAgents(theProvenanceAgents);
199                if (theOriginalInputParameters != null) {
200                        result.setOriginalInputParameters(((Resource) theOriginalInputParameters).copy());
201                }
202
203                return result;
204        }
205
206        /**
207         * Build MergeOperationInputParameters from a FHIR Parameters resource.
208         * Extracts all merge operation parameters according to the FHIR spec.
209         *
210         * @param theParameters       FHIR Parameters resource containing merge operation inputs
211         * @param theProvenanceAgents the obtained provenance agents
212         * @return MergeOperationInputParameters ready for use with ResourceMergeService
213         */
214        public static MergeOperationInputParameters inputParamsFromParameters(
215                        FhirContext theFhirContext,
216                        IBaseParameters theParameters,
217                        int theResourceLimit,
218                        List<IProvenanceAgent> theProvenanceAgents) {
219
220                // Extract source-patient-identifier (list of identifiers)
221                List<Identifier> sourceIdentifiers = null;
222                List<IBase> sourceIdentifierParams = ParametersUtil.getNamedParameters(
223                                theFhirContext, theParameters, OPERATION_MERGE_PARAM_SOURCE_PATIENT_IDENTIFIER);
224                if (!sourceIdentifierParams.isEmpty()) {
225                        sourceIdentifiers = sourceIdentifierParams.stream()
226                                        .map(param -> extractIdentifierFromParameter(theFhirContext, param))
227                                        .filter(Objects::nonNull)
228                                        .collect(Collectors.toList());
229                }
230
231                // Extract target-patient-identifier (list of identifiers)
232                List<Identifier> targetIdentifiers = null;
233                List<IBase> targetIdentifierParams = ParametersUtil.getNamedParameters(
234                                theFhirContext, theParameters, OPERATION_MERGE_PARAM_TARGET_PATIENT_IDENTIFIER);
235                if (!targetIdentifierParams.isEmpty()) {
236                        targetIdentifiers = targetIdentifierParams.stream()
237                                        .map(param -> extractIdentifierFromParameter(theFhirContext, param))
238                                        .filter(Objects::nonNull)
239                                        .collect(Collectors.toList());
240                }
241
242                // Extract source-patient reference
243                IBaseReference sourcePatient = null;
244                List<IBaseReference> sourcePatientRefs = ParametersUtil.getNamedParameterReferences(
245                                theFhirContext, theParameters, OPERATION_MERGE_PARAM_SOURCE_PATIENT);
246                if (!sourcePatientRefs.isEmpty()) {
247                        sourcePatient = sourcePatientRefs.get(0);
248                }
249
250                // Extract target-patient reference
251                IBaseReference targetPatient = null;
252                List<IBaseReference> targetPatientRefs = ParametersUtil.getNamedParameterReferences(
253                                theFhirContext, theParameters, OPERATION_MERGE_PARAM_TARGET_PATIENT);
254                if (!targetPatientRefs.isEmpty()) {
255                        targetPatient = targetPatientRefs.get(0);
256                }
257
258                // Extract preview flag
259                IPrimitiveType<Boolean> previewValue = ParametersUtil.getNamedParameterValueAsString(
260                                                theFhirContext, theParameters, OPERATION_MERGE_PARAM_PREVIEW)
261                                .map(b -> new BooleanDt(Boolean.parseBoolean(b)))
262                                .orElse(new BooleanDt(false));
263
264                // Extract delete-source flag
265                IPrimitiveType<Boolean> deleteSourceValue = ParametersUtil.getNamedParameterValueAsString(
266                                                theFhirContext, theParameters, OPERATION_MERGE_PARAM_DELETE_SOURCE)
267                                .map(b -> new BooleanDt(Boolean.parseBoolean(b)))
268                                .orElse(new BooleanDt(false));
269
270                // Extract result-patient
271                IBaseResource resultPatient = ParametersUtil.getNamedParameterResource(
272                                                theFhirContext, theParameters, OPERATION_MERGE_PARAM_RESULT_PATIENT)
273                                .orElse(null);
274
275                return inputParamsFromOperationParams(
276                                sourceIdentifiers,
277                                targetIdentifiers,
278                                sourcePatient,
279                                targetPatient,
280                                previewValue,
281                                deleteSourceValue,
282                                resultPatient,
283                                theProvenanceAgents,
284                                theParameters,
285                                theResourceLimit);
286        }
287
288        /**
289         * Extract Identifier value from a Parameters.parameter element.
290         *
291         * @param theParameter the parameter element
292         * @return the child value or null if not found
293         */
294        private static Identifier extractIdentifierFromParameter(FhirContext theFhirContext, IBase theParameter) {
295                return theFhirContext.newTerser().getSingleValueOrNull(theParameter, "valueIdentifier", Identifier.class);
296        }
297}