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}