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.ReplaceReferencesJobParameters;
024import ca.uhn.fhir.batch2.util.Batch2TaskHelper;
025import ca.uhn.fhir.i18n.Msg;
026import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
027import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
028import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao;
029import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
030import ca.uhn.fhir.model.primitive.IdDt;
031import ca.uhn.fhir.replacereferences.ReplaceReferencesPatchBundleSvc;
032import ca.uhn.fhir.replacereferences.ReplaceReferencesRequest;
033import ca.uhn.fhir.rest.api.server.RequestDetails;
034import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
035import ca.uhn.fhir.util.StopLimitAccumulator;
036import jakarta.annotation.Nonnull;
037import org.hl7.fhir.instance.model.api.IBaseParameters;
038import org.hl7.fhir.instance.model.api.IIdType;
039import org.hl7.fhir.r4.model.Bundle;
040import org.hl7.fhir.r4.model.Parameters;
041import org.hl7.fhir.r4.model.Task;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044
045import java.util.stream.Stream;
046
047import static ca.uhn.fhir.batch2.jobs.replacereferences.ReplaceReferencesAppCtx.JOB_REPLACE_REFERENCES;
048import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME;
049import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK;
050
051public class ReplaceReferencesSvcImpl implements IReplaceReferencesSvc {
052        private static final Logger ourLog = LoggerFactory.getLogger(ReplaceReferencesSvcImpl.class);
053        public static final String RESOURCE_TYPES_SYSTEM = "http://hl7.org/fhir/ValueSet/resource-types";
054        private final DaoRegistry myDaoRegistry;
055        private final HapiTransactionService myHapiTransactionService;
056        private final IResourceLinkDao myResourceLinkDao;
057        private final IJobCoordinator myJobCoordinator;
058        private final ReplaceReferencesPatchBundleSvc myReplaceReferencesPatchBundleSvc;
059        private final Batch2TaskHelper myBatch2TaskHelper;
060        private final JpaStorageSettings myStorageSettings;
061
062        public ReplaceReferencesSvcImpl(
063                        DaoRegistry theDaoRegistry,
064                        HapiTransactionService theHapiTransactionService,
065                        IResourceLinkDao theResourceLinkDao,
066                        IJobCoordinator theJobCoordinator,
067                        ReplaceReferencesPatchBundleSvc theReplaceReferencesPatchBundleSvc,
068                        Batch2TaskHelper theBatch2TaskHelper,
069                        JpaStorageSettings theStorageSettings) {
070                myDaoRegistry = theDaoRegistry;
071                myHapiTransactionService = theHapiTransactionService;
072                myResourceLinkDao = theResourceLinkDao;
073                myJobCoordinator = theJobCoordinator;
074                myReplaceReferencesPatchBundleSvc = theReplaceReferencesPatchBundleSvc;
075                myBatch2TaskHelper = theBatch2TaskHelper;
076                myStorageSettings = theStorageSettings;
077        }
078
079        @Override
080        public IBaseParameters replaceReferences(
081                        ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) {
082                theReplaceReferencesRequest.validateOrThrowInvalidParameterException();
083
084                if (theRequestDetails.isPreferAsync()) {
085                        return replaceReferencesPreferAsync(theReplaceReferencesRequest, theRequestDetails);
086                } else {
087                        return replaceReferencesPreferSync(theReplaceReferencesRequest, theRequestDetails);
088                }
089        }
090
091        @Override
092        public Integer countResourcesReferencingResource(IIdType theResourceId, RequestDetails theRequestDetails) {
093                return myHapiTransactionService
094                                .withRequest(theRequestDetails)
095                                .execute(() -> myResourceLinkDao.countResourcesTargetingFhirTypeAndFhirId(
096                                                theResourceId.getResourceType(), theResourceId.getIdPart()));
097        }
098
099        private IBaseParameters replaceReferencesPreferAsync(
100                        ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) {
101
102                Task task = myBatch2TaskHelper.startJobAndCreateAssociatedTask(
103                                myDaoRegistry.getResourceDao(Task.class),
104                                theRequestDetails,
105                                myJobCoordinator,
106                                JOB_REPLACE_REFERENCES,
107                                new ReplaceReferencesJobParameters(
108                                                theReplaceReferencesRequest, myStorageSettings.getDefaultTransactionEntriesForWrite()));
109
110                Parameters retval = new Parameters();
111                task.setIdElement(task.getIdElement().toUnqualifiedVersionless());
112                task.getMeta().setVersionId(null);
113                retval.addParameter()
114                                .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_TASK)
115                                .setResource(task);
116                return retval;
117        }
118
119        /**
120         * Try to perform the operation synchronously. However if there is more than a page of results, fall back to asynchronous operation
121         */
122        @Nonnull
123        private IBaseParameters replaceReferencesPreferSync(
124                        ReplaceReferencesRequest theReplaceReferencesRequest, RequestDetails theRequestDetails) {
125
126                // TODO KHS get partition from request
127                StopLimitAccumulator<IdDt> accumulator = myHapiTransactionService
128                                .withRequest(theRequestDetails)
129                                .execute(() -> getAllPidsWithLimit(theReplaceReferencesRequest));
130
131                if (accumulator.isTruncated()) {
132                        throw new PreconditionFailedException(Msg.code(2597) + "Number of resources with references to "
133                                        + theReplaceReferencesRequest.sourceId
134                                        + " exceeds the resource-limit "
135                                        + theReplaceReferencesRequest.resourceLimit
136                                        + ". Submit the request asynchronsly by adding the HTTP Header 'Prefer: respond-async'.");
137                }
138
139                Bundle result = myReplaceReferencesPatchBundleSvc.patchReferencingResources(
140                                theReplaceReferencesRequest, accumulator.getItemList(), theRequestDetails);
141
142                Parameters retval = new Parameters();
143                retval.addParameter()
144                                .setName(OPERATION_REPLACE_REFERENCES_OUTPUT_PARAM_OUTCOME)
145                                .setResource(result);
146                return retval;
147        }
148
149        private @Nonnull StopLimitAccumulator<IdDt> getAllPidsWithLimit(
150                        ReplaceReferencesRequest theReplaceReferencesRequest) {
151
152                Stream<IdDt> idStream = myResourceLinkDao.streamSourceIdsForTargetFhirId(
153                                theReplaceReferencesRequest.sourceId.getResourceType(),
154                                theReplaceReferencesRequest.sourceId.getIdPart());
155                StopLimitAccumulator<IdDt> accumulator =
156                                StopLimitAccumulator.fromStreamAndLimit(idStream, theReplaceReferencesRequest.resourceLimit);
157                return accumulator;
158        }
159}