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}