
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; 044 045import java.util.List; 046import java.util.stream.Collectors; 047 048import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_RESULT; 049 050public class PatientMergeProvider extends BaseJpaResourceProvider<Patient> { 051 052 private final FhirContext myFhirContext; 053 private final ResourceMergeService myResourceMergeService; 054 private final IInterceptorBroadcaster myInterceptorBroadcaster; 055 056 public PatientMergeProvider( 057 FhirContext theFhirContext, 058 DaoRegistry theDaoRegistry, 059 ResourceMergeService theResourceMergeService, 060 IInterceptorBroadcaster theInterceptorBroadcaster) { 061 super(theDaoRegistry.getResourceDao("Patient")); 062 myFhirContext = theFhirContext; 063 assert myFhirContext.getVersion().getVersion() == FhirVersionEnum.R4; 064 myResourceMergeService = theResourceMergeService; 065 myInterceptorBroadcaster = theInterceptorBroadcaster; 066 } 067 068 @Override 069 public Class<Patient> getResourceType() { 070 return Patient.class; 071 } 072 073 /** 074 * /Patient/$merge 075 */ 076 @Operation( 077 name = ProviderConstants.OPERATION_MERGE, 078 canonicalUrl = "http://hl7.org/fhir/OperationDefinition/Patient-merge") 079 public IBaseParameters patientMerge( 080 HttpServletRequest theServletRequest, 081 HttpServletResponse theServletResponse, 082 ServletRequestDetails theRequestDetails, 083 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT_IDENTIFIER) 084 List<Identifier> theSourcePatientIdentifier, 085 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT_IDENTIFIER) 086 List<Identifier> theTargetPatientIdentifier, 087 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_SOURCE_PATIENT, max = 1) 088 IBaseReference theSourcePatient, 089 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_TARGET_PATIENT, max = 1) 090 IBaseReference theTargetPatient, 091 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_PREVIEW, typeName = "boolean", max = 1) 092 IPrimitiveType<Boolean> thePreview, 093 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_DELETE_SOURCE, typeName = "boolean", max = 1) 094 IPrimitiveType<Boolean> theDeleteSource, 095 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_RESULT_PATIENT, max = 1) 096 IBaseResource theResultPatient, 097 @OperationParam(name = ProviderConstants.OPERATION_MERGE_PARAM_BATCH_SIZE, typeName = "unsignedInt") 098 IPrimitiveType<Integer> theResourceLimit) { 099 100 startRequest(theServletRequest); 101 102 try { 103 int resourceLimit = MergeResourceHelper.setResourceLimitFromParameter(myStorageSettings, theResourceLimit); 104 105 List<IProvenanceAgent> provenanceAgents = 106 ProvenanceAgentsPointcutUtil.ifHasCallHooks(theRequestDetails, myInterceptorBroadcaster); 107 108 BaseMergeOperationInputParameters mergeOperationParameters = buildMergeOperationInputParameters( 109 theSourcePatientIdentifier, 110 theTargetPatientIdentifier, 111 theSourcePatient, 112 theTargetPatient, 113 thePreview, 114 theDeleteSource, 115 theResultPatient, 116 resourceLimit, 117 provenanceAgents); 118 119 MergeOperationOutcome mergeOutcome = 120 myResourceMergeService.merge(mergeOperationParameters, theRequestDetails); 121 122 theServletResponse.setStatus(mergeOutcome.getHttpStatusCode()); 123 return buildMergeOperationOutputParameters(myFhirContext, mergeOutcome, theRequestDetails.getResource()); 124 } finally { 125 endRequest(theServletRequest); 126 } 127 } 128 129 private IBaseParameters buildMergeOperationOutputParameters( 130 FhirContext theFhirContext, MergeOperationOutcome theMergeOutcome, IBaseResource theInputParameters) { 131 132 IBaseParameters retVal = ParametersUtil.newInstance(theFhirContext); 133 ParametersUtil.addParameterToParameters( 134 theFhirContext, retVal, ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_INPUT, theInputParameters); 135 136 ParametersUtil.addParameterToParameters( 137 theFhirContext, 138 retVal, 139 ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_OUTCOME, 140 theMergeOutcome.getOperationOutcome()); 141 142 if (theMergeOutcome.getUpdatedTargetResource() != null) { 143 ParametersUtil.addParameterToParameters( 144 theFhirContext, 145 retVal, 146 OPERATION_MERGE_OUTPUT_PARAM_RESULT, 147 theMergeOutcome.getUpdatedTargetResource()); 148 } 149 150 if (theMergeOutcome.getTask() != null) { 151 ParametersUtil.addParameterToParameters( 152 theFhirContext, 153 retVal, 154 ProviderConstants.OPERATION_MERGE_OUTPUT_PARAM_TASK, 155 theMergeOutcome.getTask()); 156 } 157 return retVal; 158 } 159 160 private BaseMergeOperationInputParameters buildMergeOperationInputParameters( 161 List<Identifier> theSourcePatientIdentifier, 162 List<Identifier> theTargetPatientIdentifier, 163 IBaseReference theSourcePatient, 164 IBaseReference theTargetPatient, 165 IPrimitiveType<Boolean> thePreview, 166 IPrimitiveType<Boolean> theDeleteSource, 167 IBaseResource theResultPatient, 168 int theResourceLimit, 169 List<IProvenanceAgent> theProvenanceAgents) { 170 BaseMergeOperationInputParameters mergeOperationParameters = 171 new PatientMergeOperationInputParameters(theResourceLimit); 172 if (theSourcePatientIdentifier != null) { 173 List<CanonicalIdentifier> sourceResourceIdentifiers = theSourcePatientIdentifier.stream() 174 .map(CanonicalIdentifier::fromIdentifier) 175 .collect(Collectors.toList()); 176 mergeOperationParameters.setSourceResourceIdentifiers(sourceResourceIdentifiers); 177 } 178 if (theTargetPatientIdentifier != null) { 179 List<CanonicalIdentifier> targetResourceIdentifiers = theTargetPatientIdentifier.stream() 180 .map(CanonicalIdentifier::fromIdentifier) 181 .collect(Collectors.toList()); 182 mergeOperationParameters.setTargetResourceIdentifiers(targetResourceIdentifiers); 183 } 184 mergeOperationParameters.setSourceResource(theSourcePatient); 185 mergeOperationParameters.setTargetResource(theTargetPatient); 186 mergeOperationParameters.setPreview(thePreview != null && thePreview.getValue()); 187 mergeOperationParameters.setDeleteSource(theDeleteSource != null && theDeleteSource.getValue()); 188 189 if (theResultPatient != null) { 190 // pass in a copy of the result patient as we don't want it to be modified. It will be 191 // returned back to the client as part of the response. 192 mergeOperationParameters.setResultResource(((Patient) theResultPatient).copy()); 193 } 194 195 mergeOperationParameters.setProvenanceAgents(theProvenanceAgents); 196 return mergeOperationParameters; 197 } 198}