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;
021
022import ca.uhn.fhir.batch2.api.IJobCoordinator;
023import ca.uhn.fhir.batch2.jobs.replacereferences.ProvenanceAgentJson;
024import ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesJobParameters;
025import ca.uhn.fhir.batch2.util.Batch2TaskHelper;
026import ca.uhn.fhir.context.FhirContext;
027import ca.uhn.fhir.i18n.Msg;
028import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
029import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
030import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
031import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao;
032import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
033import ca.uhn.fhir.model.primitive.IdDt;
034import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc;
035import ca.uhn.fhir.replacereferences.ReplaceReferencesProvenanceSvc;
036import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest;
037import ca.uhn.fhir.rest.api.server.RequestDetails;
038import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
039import ca.uhn.fhir.util.StopLimitAccumulator;
040import jakarta.annotation.Nonnull;
041import org.hl7.fhir.instance.model.api.IBaseParameters;
042import org.hl7.fhir.instance.model.api.IBaseResource;
043import org.hl7.fhir.instance.model.api.IIdType;
044import org.hl7.fhir.r4.model.Bundle;
045import org.hl7.fhir.r4.model.Parameters;
046import org.hl7.fhir.r4.model.Task;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050import java.util.Date;
051import java.util.List;
052import java.util.stream.Stream;
053
054import static ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx.JOB_REPLACE_REFERENCES;
055import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME;
056import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK;
057
058public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc {
059        private static final Logger ourLog = LoggerFactory.getLogger(ReplaceReferencesSvcImpl.class);
060        public static final String RESOURCE_TYPES_SYSTEM = "http://hl7.org/fhir/ValueSet/resource-types";
061        private final DaoRegistry myDaoRegistry;
062        private final HapiTransactionService myHapiTransactionService;
063        private final IResourceLinkDao myResourceLinkDao;
064        private final IJobCoordinator myJobCoordinator;
065        private final ReplaceReferencesPatchBundleSvc myReplaceReferencesPatchBundleSvc;
066        private final Batch2TaskHelper myBatch2TaskHelper;
067        private final JpaStorageSettings myStorageSettings;
068        private final ReplaceReferencesProvenanceSvc myReplaceReferencesProvenanceSvc;
069        private final FhirContext myFhirContext;
070
071        public ReplaceReferencesSvcImpl(
072                        DaoRegistry theDaoRegistry,
073                        HapiTransactionService theHapiTransactionService,
074                        IResourceLinkDao theResourceLinkDao,
075                        IJobCoordinator theJobCoordinator,
076                        ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc,
077                        Batch2TaskHelper theBatch2TaskHelper,
078                        JpaStorageSettings theStorageSettings,
079                        ReplaceReferencesProvenanceSvc theReplaceReferencesProvenanceSvc) {
080                myDaoRegistry = theDaoRegistry;
081                myHapiTransactionService = theHapiTransactionService;
082                myResourceLinkDao = theResourceLinkDao;
083                myJobCoordinator = theJobCoordinator;
084                myReplaceReferencesPatchBundleSvc = theReplaceReferencesPatchBundleSvc;
085                myBatch2TaskHelper = theBatch2TaskHelper;
086                myStorageSettings = theStorageSettings;
087                myReplaceReferencesProvenanceSvc = theReplaceReferencesProvenanceSvc;
088                myFhirContext = theDaoRegistry.getFhirContext();
089        }
090
091        @Override
092        public IBaseParameters replaceReferences(
093                        ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) {
094                theReplaceReferencesRequest.validateOrThrowInvalidParameterException();
095
096                // Read the source and target resources, this is done for two reasons:
097                // 1. To ensure that the resources exist
098                // 2. To find out the current versions of the resources, which is needed for creating the Provenance resource
099                IBaseResource sourceResource = readResource(theReplaceReferencesRequest.sourceId, theRequestDetails);
100                IBaseResource targetResource = readResource(theReplaceReferencesRequest.targetId, theRequestDetails);
101
102                if (theRequestDetails.isPreferAsync()) {
103                        return replaceReferencesPreferAsync(
104                                        theReplaceReferencesRequest, theRequestDetails, sourceResource, targetResource);
105                } else {
106                        return replaceReferencesPreferSync(
107                                        theReplaceReferencesRequest, theRequestDetails, sourceResource, targetResource);
108                }
109        }
110
111        @Override
112        public Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails) {
113                return myHapiTransactionService
114                                .withRequest(theRequestDetails)
115                                .execute(() -> myResourceLinkDao.countResourcesTargetingFhirTypeAndFhirId(
116                                                theResourceId.getResourceType(), theResourceId.getIdPart()));
117        }
118
119        private IBaseParameters replaceReferencesPreferAsync(
120                        ReplaceReferencesRequest theReplaceReferencesRequest,
121                        RequestDetails theRequestDetails,
122                        IBaseResource theSourceResource,
123                        IBaseResource theTargetResource) {
124
125                ReplaceReferencesJobParameters jobParams = new ReplaceReferencesJobParameters(
126                                theReplaceReferencesRequest,
127                                myStorageSettings.getDefaultTransactionEntriesForWrite(),
128                                theSourceResource.getIdElement().getVersionIdPart(),
129                                theTargetResource.getIdElement().getVersionIdPart(),
130                                ProvenanceAgentJson.from(theReplaceReferencesRequest.provenanceAgents, myFhirContext));
131
132                Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask(
133                                myDaoRegistry.getResourceDao(Task.class),
134                                theRequestDetails,
135                                myJobCoordinator,
136                                JOB_REPLACE_REFERENCES,
137                                jobParams);
138
139                Parameters retval = new Parameters();
140                task.setIdElement(task.getIdElement().toUnqualifiedVersionless());
141                task.getMeta().setVersionId(null);
142                retval.addParameter()
143                                .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK)
144                                .setResource(task);
145                return retval;
146        }
147
148        /**
149         * Try to perform the operation synchronously. If there are more resources to process than the specified resource limit,
150         * throws a PreconditionFailedException.
151         */
152        @Nonnull
153        private IBaseParameters replaceReferencesPreferSync(
154                        ReplaceReferencesRequest theReplaceReferencesRequest,
155                        RequestDetails theRequestDetails,
156                        IBaseResource theSourceResource,
157                        IBaseResource theTargetResource) {
158
159                Date startTime = new Date();
160
161                StopLimitAccumulator<IdDt> accumulator = myHapiTransactionService
162                                .withRequest(theRequestDetails)
163                                .withRequestPartitionId(theReplaceReferencesRequest.partitionId)
164                                .execute(() -> getAllPidsWithLimit(theReplaceReferencesRequest));
165
166                if (accumulator.isTruncated()) {
167                        throw new PreconditionFailedException(Msg.code(2597) + "Number of resources with references to "
168                                        + theReplaceReferencesRequest.sourceId
169                                        + " exceeds the resource-limit "
170                                        + theReplaceReferencesRequest.resourceLimit
171                                        + ". Submit the request asynchronsly by adding the HTTP Header 'Prefer: respond-async'.");
172                }
173
174                Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources(
175                                theReplaceReferencesRequest, accumulator.getItemList(), theRequestDetails);
176
177                if (theReplaceReferencesRequest.createProvenance) {
178                        myReplaceReferencesProvenanceSvc.createProvenance(
179                                        // we need to use versioned ids for the Provenance resource
180                                        theTargetResource.getIdElement().toUnqualified(),
181                                        theSourceResource.getIdElement().toUnqualified(),
182                                        List.of(result),
183                                        startTime,
184                                        theRequestDetails,
185                                        theReplaceReferencesRequest.provenanceAgents);
186                }
187
188                Parameters retval = new Parameters();
189                retval.addParameter()
190                                .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME)
191                                .setResource(result);
192                return retval;
193        }
194
195        private @Nonnull StopLimitAccumulator<IdDt> getAllPidsWithLimit(
196                        ReplaceReferencesRequest theReplaceReferencesRequest) {
197
198                Stream<IdDt> idStream = myResourceLinkDao.streamSourceIdsForTargetFhirId(
199                                theReplaceReferencesRequest.sourceId.getResourceType(),
200                                theReplaceReferencesRequest.sourceId.getIdPart());
201                StopLimitAccumulator<IdDt> accumulator =
202                                StopLimitAccumulator.fromStreamAndLimit(idStream, theReplaceReferencesRequest.resourceLimit);
203                return accumulator;
204        }
205
206        private IBaseResource readResource(IIdType theId, RequestDetails theRequestDetails) {
207                String resourceType = theId.getResourceType();
208                IFhirResourceDao<IBaseResource> resourceDao = myDaoRegistry.getResourceDao(resourceType);
209                return resourceDao.read(theId, theRequestDetails);
210        }
211}