
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}