
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}