
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 022import ca.uhn.fhir.batch2.jobs.merge.MergeResourceHelper; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.context.FhirVersionEnum; 025import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 026import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 027import ca.uhn.fhir.jpa.interceptor.ProvenanceAgentsPointcutUtil; 028import ca.uhn.fhir.jpa.provider.BaseJpaResourceProvider; 029import ca.uhn.fhir.model.api.IProvenanceAgent; 030import ca.uhn.fhir.rest.annotation.Operation; 031import ca.uhn.fhir.rest.annotation.OperationParam; 032import ca.uhn.fhir.rest.server.provider.ProviderConstants; 033import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 034import ca.uhn.fhir.util.CanonicalIdentifier; 035import ca.uhn.fhir.util.ParametersUtil; 036import jakarta.servlet.http.HttpServletRequest; 037import jakarta.servlet.http.HttpServletResponse; 038import org.hl7.fhir.instance.model.api.IBaseParameters; 039import org.hl7.fhir.instance.model.api.IBaseReference; 040import org.hl7.fhir.instance.model.api.IBaseResource; 041import org.hl7.fhir.instance.model.api.IPrimitiveType; 042import org.hl7.fhir.r4.model.Identifier; 043import org.hl7.fhir.r4.model.Patient; 044import org.hl7.fhir.r4.model.Resource; 045 046import java.util.List; 047import java.util.stream.Collectors; 048 049import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; 050 051public class PatientMergeProvider extends BaseJpaResourceProvider<Patient> { 052 053 private final FhirContext myFhirContext; 054 private final ResourceMergeService myResourceMergeService; 055 private final ResourceUndoMergeService myResourceUndoMergeService; 056 private final IInterceptorBroadcaster myInterceptorBroadcaster; 057 058 public PatientMergeProvider( 059 FhirContext theFhirContext, 060 DaoRegistry theDaoRegistry, 061 ResourceMergeService theResourceMergeService, 062 ResourceUndoMergeService theResourceUndoMergeService, 063 IInterceptorBroadcaster theInterceptorBroadcaster) { 064 super(theDaoRegistry.getResourceDao("Patient")); 065 myFhirContext = theFhirContext; 066 assert myFhirContext.getVersion().getVersion() == FhirVersionEnum.R4; 067 myResourceMergeService = theResourceMergeService; 068 myResourceUndoMergeService = theResourceUndoMergeService; 069 myInterceptorBroadcaster = theInterceptorBroadcaster; 070 } 071 072 @Override 073 public Class<Patient> getResourceType() { 074 return Patient.class; 075 } 076 077 /** 078 * /Patient/$merge 079 */ 080 @Operation( 081 name = ProviderConstants.OPERATION_MERGE, 082 canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge") 083 public IBaseParameters patientMerge( 084 HttpServletRequest theServletRequest, 085 HttpServletResponse theServletResponse, 086 ServletRequestDetails theRequestDetails, 087 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT_IDENTIFIER) 088 List<Identifier> theSourcePatientIdentifier, 089 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT_IDENTIFIER) 090 List<Identifier> theTargetPatientIdentifier, 091 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT, max = 1) 092 IBaseReference theSourcePatient, 093 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT, max = 1) 094 IBaseReference theTargetPatient, 095 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_PREVIEW, typeName = "boolean", max = 1) 096 IPrimitiveType<Boolean> thePreview, 097 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_DELETE_SOURCE, typeName = "boolean", max = 1) 098 IPrimitiveType<Boolean> theDeleteSource, 099 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_RESULT_PATIENT, max = 1) 100 IBaseResource theResultPatient, 101 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_BATCH_SIZE, typeName = "unsignedInt") 102 IPrimitiveType<Integer> theResourceLimit) { 103 104 startRequest(theServletRequest); 105 106 try { 107 int resourceLimit = MergeResourceHelper.setResourceLimitFromParameter(myStorageSettings, theResourceLimit); 108 109 List<IProvenanceAgent> provenanceAgents = 110 ProvenanceAgentsPointcutUtil.ifHasCallHooks(theRequestDetails, myInterceptorBroadcaster); 111 112 MergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( 113 theSourcePatientIdentifier, 114 theTargetPatientIdentifier, 115 theSourcePatient, 116 theTargetPatient, 117 thePreview, 118 theDeleteSource, 119 theResultPatient, 120 resourceLimit, 121 provenanceAgents, 122 theRequestDetails.getResource()); 123 124 MergeOperationOutcome mergeOutcome = 125 myResourceMergeService.merge(mergeOperationParameters, theRequestDetails); 126 127 theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); 128 return buildMergeOperationOutputParameters(myFhirContext, mergeOutcome, theRequestDetails.getResource()); 129 } finally { 130 endRequest(theServletRequest); 131 } 132 } 133 134 /** 135 * /Patient/$hapi.fhir.undo-merge 136 */ 137 @Operation(name = ProviderConstants.OPERATION_UNDO_MERGE) 138 public IBaseParameters patientUndoMerge( 139 HttpServletRequest theServletRequest, 140 HttpServletResponse theServletResponse, 141 ServletRequestDetails theRequestDetails, 142 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT_IDENTIFIER) 143 List<Identifier> theSourcePatientIdentifier, 144 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT_IDENTIFIER) 145 List<Identifier> theTargetPatientIdentifier, 146 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT, max = 1) 147 IBaseReference theSourcePatient, 148 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT, max = 1) 149 IBaseReference theTargetPatient) { 150 151 startRequest(theServletRequest); 152 153 try { 154 // create input parameters 155 UndoMergeOperationInputParameters inputParameters = buildUndoMergeOperationInputParameters( 156 theSourcePatientIdentifier, theTargetPatientIdentifier, theSourcePatient, theTargetPatient); 157 158 // now call the undo service with parameters 159 OperationOutcomeWithStatusCode undomergeOutcome = 160 myResourceUndoMergeService.undoMerge(inputParameters, theRequestDetails); 161 theServletResponse.setStatus(undomergeOutcome.getHttpStatusCode()); 162 IBaseParameters retVal = ParametersUtil.newInstance(myFhirContext); 163 164 ParametersUtil.addParameterToParameters( 165 myFhirContext, 166 retVal, 167 ProviderConstants.OPERATION_UNDO_MERGE_OUTCOME, 168 undomergeOutcome.getOperationOutcome()); 169 return retVal; 170 } finally { 171 endRequest(theServletRequest); 172 } 173 } 174 175 private IBaseParameters buildMergeOperationOutputParameters( 176 FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome, IBaseResource theInputParameters) { 177 178 IBaseParameters retVal = ParametersUtil.newInstance(theFhirContext); 179 ParametersUtil.addParameterToParameters( 180 theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT, theInputParameters); 181 182 ParametersUtil.addParameterToParameters( 183 theFhirContext, 184 retVal, 185 ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME, 186 theMergeOutcome.getOperationOutcome()); 187 188 if (theMergeOutcome.getUpdatedTargetResource() != null) { 189 ParametersUtil.addParameterToParameters( 190 theFhirContext, 191 retVal, 192 OPERATION_MERGE_OUTPUT_PARAM_RESULT, 193 theMergeOutcome.getUpdatedTargetResource()); 194 } 195 196 if (theMergeOutcome.getTask() != null) { 197 ParametersUtil.addParameterToParameters( 198 theFhirContext, 199 retVal, 200 ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK, 201 theMergeOutcome.getTask()); 202 } 203 return retVal; 204 } 205 206 private UndoMergeOperationInputParameters buildUndoMergeOperationInputParameters( 207 List<Identifier> theSourcePatientIdentifier, 208 List<Identifier> theTargetPatientIdentifier, 209 IBaseReference theSourcePatient, 210 IBaseReference theTargetPatient) { 211 212 int resourceLimit = myStorageSettings.getInternalSynchronousSearchSize(); 213 214 UndoMergeOperationInputParameters undoMergeOperationParameters = 215 new UndoMergeOperationInputParameters(resourceLimit); 216 217 setCommonMergeOperationInputParameters( 218 undoMergeOperationParameters, 219 theSourcePatientIdentifier, 220 theTargetPatientIdentifier, 221 theSourcePatient, 222 theTargetPatient); 223 224 return undoMergeOperationParameters; 225 } 226 227 private void setCommonMergeOperationInputParameters( 228 MergeOperationsCommonInputParameters theMergeOperationParameters, 229 List<Identifier> theSourcePatientIdentifier, 230 List<Identifier> theTargetPatientIdentifier, 231 IBaseReference theSourcePatient, 232 IBaseReference theTargetPatient) { 233 if (theSourcePatientIdentifier != null) { 234 List<CanonicalIdentifier> sourceResourceIdentifiers = theSourcePatientIdentifier.stream() 235 .map(CanonicalIdentifier::fromIdentifier) 236 .collect(Collectors.toList()); 237 theMergeOperationParameters.setSourceResourceIdentifiers(sourceResourceIdentifiers); 238 } 239 if (theTargetPatientIdentifier != null) { 240 List<CanonicalIdentifier> targetResourceIdentifiers = theTargetPatientIdentifier.stream() 241 .map(CanonicalIdentifier::fromIdentifier) 242 .collect(Collectors.toList()); 243 theMergeOperationParameters.setTargetResourceIdentifiers(targetResourceIdentifiers); 244 } 245 theMergeOperationParameters.setSourceResource(theSourcePatient); 246 theMergeOperationParameters.setTargetResource(theTargetPatient); 247 } 248 249 private MergeOperationInputParameters buildMergeOperationInputParameters( 250 List<Identifier> theSourcePatientIdentifier, 251 List<Identifier> theTargetPatientIdentifier, 252 IBaseReference theSourcePatient, 253 IBaseReference theTargetPatient, 254 IPrimitiveType<Boolean> thePreview, 255 IPrimitiveType<Boolean> theDeleteSource, 256 IBaseResource theResultPatient, 257 int theResourceLimit, 258 List<IProvenanceAgent> theProvenanceAgents, 259 IBaseResource theOriginalInputParameters) { 260 261 MergeOperationInputParameters mergeOperationParameters = new MergeOperationInputParameters(theResourceLimit); 262 263 setCommonMergeOperationInputParameters( 264 mergeOperationParameters, 265 theSourcePatientIdentifier, 266 theTargetPatientIdentifier, 267 theSourcePatient, 268 theTargetPatient); 269 270 mergeOperationParameters.setPreview(thePreview != null && thePreview.getValue()); 271 mergeOperationParameters.setDeleteSource(theDeleteSource != null && theDeleteSource.getValue()); 272 273 if (theResultPatient != null) { 274 // pass in a copy of the result patient as we don't want it to be modified. It will be 275 // returned back to the client as part of the response. 276 mergeOperationParameters.setResultResource(((Patient) theResultPatient).copy()); 277 } 278 279 mergeOperationParameters.setProvenanceAgents(theProvenanceAgents); 280 mergeOperationParameters.setOriginalInputParameters(((Resource) theOriginalInputParameters).copy()); 281 return mergeOperationParameters; 282 } 283}