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.api.IJobCoordinator; 023import ca.uhn.fhir.batch2.jobs.merge.MergeJobParameters; 024import ca.uhn.fhir.batch2.jobs.merge.MergeResourceHelper; 025import ca.uhn.fhir.batch2.util.Batch2TaskHelper; 026import ca.uhn.fhir.context.FhirContext; 027import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; 028import ca.uhn.fhir.interceptor.model.RequestPartitionId; 029import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 030import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 031import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 032import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; 033import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 034import ca.uhn.fhir.jpa.provider.IReplaceReferencesSvc; 035import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest; 036import ca.uhn.fhir.rest.api.server.RequestDetails; 037import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 038import ca.uhn.fhir.util.OperationOutcomeUtil; 039import org.hl7.fhir.instance.model.api.IBase; 040import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 041import org.hl7.fhir.r4.model.Patient; 042import org.hl7.fhir.r4.model.Task; 043import org.slf4j.Logger; 044import org.slf4j.LoggerFactory; 045 046import static ca.uhn.fhir.batch2.jobs.merge.MergeAppCtx.JOB_MERGE; 047import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_200_OK; 048import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_202_ACCEPTED; 049import static ca.uhn.fhir.rest.api.Constants.STATUS_HTTP_500_INTERNAL_ERROR; 050 051/** 052 * Service for the FHIR $merge operation. Currently only supports Patient/$merge. The plan is to expand to other resource types. 053 */ 054public class ResourceMergeService { 055 private static final Logger ourLog = LoggerFactory.getLogger(ResourceMergeService.class); 056 057 private final FhirContext myFhirContext; 058 private final JpaStorageSettings myStorageSettings; 059 private final IFhirResourceDao<Patient> myPatientDao; 060 private final IReplaceReferencesSvc myReplaceReferencesSvc; 061 private final IHapiTransactionService myHapiTransactionService; 062 private final IRequestPartitionHelperSvc myRequestPartitionHelperSvc; 063 private final IFhirResourceDao<Task> myTaskDao; 064 private final IJobCoordinator myJobCoordinator; 065 private final MergeResourceHelper myMergeResourceHelper; 066 private final Batch2TaskHelper myBatch2TaskHelper; 067 private final MergeValidationService myMergeValidationService; 068 069 public ResourceMergeService( 070 JpaStorageSettings theStorageSettings, 071 DaoRegistry theDaoRegistry, 072 IReplaceReferencesSvc theReplaceReferencesSvc, 073 IHapiTransactionService theHapiTransactionService, 074 IRequestPartitionHelperSvc theRequestPartitionHelperSvc, 075 IJobCoordinator theJobCoordinator, 076 Batch2TaskHelper theBatch2TaskHelper) { 077 myStorageSettings = theStorageSettings; 078 079 myPatientDao = theDaoRegistry.getResourceDao(Patient.class); 080 myTaskDao = theDaoRegistry.getResourceDao(Task.class); 081 myReplaceReferencesSvc = theReplaceReferencesSvc; 082 myRequestPartitionHelperSvc = theRequestPartitionHelperSvc; 083 myJobCoordinator = theJobCoordinator; 084 myBatch2TaskHelper = theBatch2TaskHelper; 085 myFhirContext = myPatientDao.getContext(); 086 myHapiTransactionService = theHapiTransactionService; 087 myMergeResourceHelper = new MergeResourceHelper(myPatientDao); 088 myMergeValidationService = new MergeValidationService(myFhirContext, theDaoRegistry); 089 } 090 091 /** 092 * Perform the $merge operation. If the number of resources to be changed exceeds the provided batch size, 093 * then switch to async mode. See the <a href="https://build.fhir.org/patient-operation-merge.html">Patient $merge spec</a> 094 * for details on what the difference is between synchronous and asynchronous mode. 095 * 096 * @param theMergeOperationParameters the merge operation parameters 097 * @param theRequestDetails the request details 098 * @return the merge outcome containing OperationOutcome and HTTP status code 099 */ 100 public MergeOperationOutcome merge( 101 BaseMergeOperationInputParameters theMergeOperationParameters, RequestDetails theRequestDetails) { 102 103 MergeOperationOutcome mergeOutcome = new MergeOperationOutcome(); 104 IBaseOperationOutcome operationOutcome = OperationOutcomeUtil.newInstance(myFhirContext); 105 mergeOutcome.setOperationOutcome(operationOutcome); 106 // default to 200 OK, would be changed to another code during processing as required 107 mergeOutcome.setHttpStatusCode(STATUS_HTTP_200_OK); 108 try { 109 validateAndMerge(theMergeOperationParameters, theRequestDetails, mergeOutcome); 110 } catch (Exception e) { 111 ourLog.error("Resource merge failed", e); 112 if (e instanceof BaseServerResponseException) { 113 mergeOutcome.setHttpStatusCode(((BaseServerResponseException) e).getStatusCode()); 114 } else { 115 mergeOutcome.setHttpStatusCode(STATUS_HTTP_500_INTERNAL_ERROR); 116 } 117 OperationOutcomeUtil.addIssue(myFhirContext, operationOutcome, "error", e.getMessage(), null, "exception"); 118 } 119 return mergeOutcome; 120 } 121 122 private void validateAndMerge( 123 BaseMergeOperationInputParameters theMergeOperationParameters, 124 RequestDetails theRequestDetails, 125 MergeOperationOutcome theMergeOutcome) { 126 127 // TODO KHS remove the outparameter and instead accumulate issues in the validation result 128 MergeValidationResult mergeValidationResult = 129 myMergeValidationService.validate(theMergeOperationParameters, theRequestDetails, theMergeOutcome); 130 131 if (mergeValidationResult.isValid) { 132 Patient sourceResource = mergeValidationResult.sourceResource; 133 Patient targetResource = mergeValidationResult.targetResource; 134 135 if (theMergeOperationParameters.getPreview()) { 136 handlePreview( 137 sourceResource, 138 targetResource, 139 theMergeOperationParameters, 140 theRequestDetails, 141 theMergeOutcome); 142 } else { 143 doMerge( 144 theMergeOperationParameters, 145 sourceResource, 146 targetResource, 147 theRequestDetails, 148 theMergeOutcome); 149 } 150 } else { 151 theMergeOutcome.setHttpStatusCode(mergeValidationResult.httpStatusCode); 152 } 153 } 154 155 private void handlePreview( 156 Patient theSourceResource, 157 Patient theTargetResource, 158 BaseMergeOperationInputParameters theMergeOperationParameters, 159 RequestDetails theRequestDetails, 160 MergeOperationOutcome theMergeOutcome) { 161 162 Integer referencingResourceCount = myReplaceReferencesSvc.countResourcesReferencingResource( 163 theSourceResource.getIdElement().toVersionless(), theRequestDetails); 164 165 // in preview mode, we should also return what the target would look like 166 Patient theResultResource = (Patient) theMergeOperationParameters.getResultResource(); 167 Patient targetPatientAsIfUpdated = myMergeResourceHelper.prepareTargetPatientForUpdate( 168 theTargetResource, theSourceResource, theResultResource, theMergeOperationParameters.getDeleteSource()); 169 theMergeOutcome.setUpdatedTargetResource(targetPatientAsIfUpdated); 170 171 // adding +2 because the source and the target resources would be updated as well 172 String diagnosticsMsg = String.format("Merge would update %d resources", referencingResourceCount + 2); 173 String detailsText = "Preview only merge operation - no issues detected"; 174 addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), diagnosticsMsg, detailsText); 175 } 176 177 private void doMerge( 178 BaseMergeOperationInputParameters theMergeOperationParameters, 179 Patient theSourceResource, 180 Patient theTargetResource, 181 RequestDetails theRequestDetails, 182 MergeOperationOutcome theMergeOutcome) { 183 184 RequestPartitionId partitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequest( 185 theRequestDetails, ReadPartitionIdRequestDetails.forRead(theTargetResource.getIdElement())); 186 187 if (theRequestDetails.isPreferAsync()) { 188 doMergeAsync( 189 theMergeOperationParameters, 190 theSourceResource, 191 theTargetResource, 192 theRequestDetails, 193 theMergeOutcome, 194 partitionId); 195 } else { 196 doMergeSync( 197 theMergeOperationParameters, 198 theSourceResource, 199 theTargetResource, 200 theRequestDetails, 201 theMergeOutcome, 202 partitionId); 203 } 204 } 205 206 private void doMergeSync( 207 BaseMergeOperationInputParameters theMergeOperationParameters, 208 Patient theSourceResource, 209 Patient theTargetResource, 210 RequestDetails theRequestDetails, 211 MergeOperationOutcome theMergeOutcome, 212 RequestPartitionId partitionId) { 213 214 ReplaceReferencesRequest replaceReferencesRequest = new ReplaceReferencesRequest( 215 theSourceResource.getIdElement(), 216 theTargetResource.getIdElement(), 217 theMergeOperationParameters.getResourceLimit(), 218 partitionId); 219 220 myReplaceReferencesSvc.replaceReferences(replaceReferencesRequest, theRequestDetails); 221 222 Patient updatedTarget = myMergeResourceHelper.updateMergedResourcesAfterReferencesReplaced( 223 myHapiTransactionService, 224 theSourceResource, 225 theTargetResource, 226 (Patient) theMergeOperationParameters.getResultResource(), 227 theMergeOperationParameters.getDeleteSource(), 228 theRequestDetails); 229 theMergeOutcome.setUpdatedTargetResource(updatedTarget); 230 231 String detailsText = "Merge operation completed successfully."; 232 addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); 233 } 234 235 private void doMergeAsync( 236 BaseMergeOperationInputParameters theMergeOperationParameters, 237 Patient theSourceResource, 238 Patient theTargetResource, 239 RequestDetails theRequestDetails, 240 MergeOperationOutcome theMergeOutcome, 241 RequestPartitionId thePartitionId) { 242 243 MergeJobParameters mergeJobParameters = theMergeOperationParameters.asMergeJobParameters( 244 myFhirContext, myStorageSettings, theSourceResource, theTargetResource, thePartitionId); 245 246 Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask( 247 myTaskDao, theRequestDetails, myJobCoordinator, JOB_MERGE, mergeJobParameters); 248 249 task.setIdElement(task.getIdElement().toUnqualifiedVersionless()); 250 task.getMeta().setVersionId(null); 251 theMergeOutcome.setTask(task); 252 theMergeOutcome.setHttpStatusCode(STATUS_HTTP_202_ACCEPTED); 253 254 String detailsText = "Merge request is accepted, and will be processed asynchronously. See" 255 + " task resource returned in this response for details."; 256 addInfoToOperationOutcome(theMergeOutcome.getOperationOutcome(), null, detailsText); 257 } 258 259 private void addInfoToOperationOutcome( 260 IBaseOperationOutcome theOutcome, String theDiagnosticMsg, String theDetailsText) { 261 IBase issue = 262 OperationOutcomeUtil.addIssue(myFhirContext, theOutcome, "information", theDiagnosticMsg, null, null); 263 OperationOutcomeUtil.addDetailsToIssue(myFhirContext, issue, null, null, theDetailsText); 264 } 265}