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