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}