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}