
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}