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