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.delete; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.interceptor.api.HookParams; 025import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 026import ca.uhn.fhir.interceptor.api.Pointcut; 027import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 028import ca.uhn.fhir.jpa.api.model.DeleteConflict; 029import ca.uhn.fhir.jpa.api.model.DeleteConflictList; 030import ca.uhn.fhir.jpa.dao.BaseStorageDao; 031import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; 032import ca.uhn.fhir.jpa.model.entity.ResourceLink; 033import ca.uhn.fhir.jpa.model.entity.ResourceTable; 034import ca.uhn.fhir.model.primitive.IdDt; 035import ca.uhn.fhir.rest.api.server.RequestDetails; 036import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 037import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; 038import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 039import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 040import ca.uhn.fhir.util.OperationOutcomeUtil; 041import com.google.common.annotations.VisibleForTesting; 042import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 043import org.slf4j.Logger; 044import org.slf4j.LoggerFactory; 045import org.springframework.beans.factory.annotation.Autowired; 046import org.springframework.stereotype.Service; 047 048import java.util.List; 049 050@Service 051public class DeleteConflictService { 052 public static final int FIRST_QUERY_RESULT_COUNT = 1; 053 private static final Logger ourLog = LoggerFactory.getLogger(DeleteConflictService.class); 054 public static int MAX_RETRY_ATTEMPTS = 10; 055 public static String MAX_RETRY_ATTEMPTS_EXCEEDED_MSG = 056 "Requested delete operation stopped before all conflicts were handled. May need to increase the configured Maximum Delete Conflict Query Count."; 057 058 @Autowired 059 protected IResourceLinkDao myResourceLinkDao; 060 061 @Autowired 062 protected IInterceptorBroadcaster myInterceptorBroadcaster; 063 064 @Autowired 065 DeleteConflictFinderService myDeleteConflictFinderService; 066 067 @Autowired 068 JpaStorageSettings myStorageSettings; 069 070 @Autowired 071 private FhirContext myFhirContext; 072 073 private DeleteConflictOutcome findAndHandleConflicts( 074 RequestDetails theRequest, 075 DeleteConflictList theDeleteConflicts, 076 ResourceTable theEntity, 077 boolean theForValidate, 078 int theMinQueryResultCount, 079 TransactionDetails theTransactionDetails) { 080 List<ResourceLink> resultList = myDeleteConflictFinderService.findConflicts(theEntity, theMinQueryResultCount); 081 if (resultList.isEmpty()) { 082 return null; 083 } 084 085 return handleConflicts( 086 theRequest, theDeleteConflicts, theEntity, theForValidate, resultList, theTransactionDetails); 087 } 088 089 private DeleteConflictOutcome handleConflicts( 090 RequestDetails theRequest, 091 DeleteConflictList theDeleteConflicts, 092 ResourceTable theEntity, 093 boolean theForValidate, 094 List<ResourceLink> theResultList, 095 TransactionDetails theTransactionDetails) { 096 if (!myStorageSettings.isEnforceReferentialIntegrityOnDelete() && !theForValidate) { 097 ourLog.debug("Deleting {} resource dependencies which can no longer be satisfied", theResultList.size()); 098 myResourceLinkDao.deleteAll(theResultList); 099 return null; 100 } 101 102 addConflictsToList(theDeleteConflicts, theEntity, theResultList); 103 104 if (theDeleteConflicts.isEmpty()) { 105 return new DeleteConflictOutcome(); 106 } 107 108 // Notify Interceptors about pre-action call 109 IInterceptorBroadcaster compositeBroadcaster = 110 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 111 HookParams hooks = new HookParams() 112 .add(DeleteConflictList.class, theDeleteConflicts) 113 .add(RequestDetails.class, theRequest) 114 .addIfMatchesType(ServletRequestDetails.class, theRequest) 115 .add(TransactionDetails.class, theTransactionDetails); 116 return (DeleteConflictOutcome) 117 compositeBroadcaster.callHooksAndReturnObject(Pointcut.STORAGE_PRESTORAGE_DELETE_CONFLICTS, hooks); 118 } 119 120 private void addConflictsToList( 121 DeleteConflictList theDeleteConflicts, ResourceTable theEntity, List<ResourceLink> theResultList) { 122 for (ResourceLink link : theResultList) { 123 IdDt targetId = theEntity.getIdDt(); 124 IdDt sourceId = link.getSourceResource().getIdDt(); 125 String sourcePath = link.getSourcePath(); 126 if (theDeleteConflicts.isResourceIdMarkedForDeletion(sourceId)) { 127 if (theDeleteConflicts.isResourceIdMarkedForDeletion(targetId)) { 128 continue; 129 } 130 } 131 132 theDeleteConflicts.add(new DeleteConflict(sourceId, sourcePath, targetId)); 133 } 134 } 135 136 public int validateOkToDelete( 137 DeleteConflictList theDeleteConflicts, 138 ResourceTable theEntity, 139 boolean theForValidate, 140 RequestDetails theRequest, 141 TransactionDetails theTransactionDetails) { 142 143 // We want the list of resources that are marked to be the same list even as we 144 // drill into conflict resolution stacks.. this allows us to not get caught by 145 // circular references 146 DeleteConflictList newConflicts = new DeleteConflictList(theDeleteConflicts); 147 148 // In most cases, there will be no hooks, and so we only need to check if there is at least 149 // FIRST_QUERY_RESULT_COUNT conflict and populate that. 150 // Only in the case where there is a hook do we need to go back and collect larger batches of conflicts for 151 // processing. 152 153 DeleteConflictOutcome outcome = findAndHandleConflicts( 154 theRequest, newConflicts, theEntity, theForValidate, FIRST_QUERY_RESULT_COUNT, theTransactionDetails); 155 156 int retryCount = 0; 157 while (outcome != null) { 158 int shouldRetryCount = Math.min(outcome.getShouldRetryCount(), MAX_RETRY_ATTEMPTS); 159 if (!(retryCount < shouldRetryCount)) break; 160 newConflicts = new DeleteConflictList(newConflicts); 161 outcome = findAndHandleConflicts( 162 theRequest, 163 newConflicts, 164 theEntity, 165 theForValidate, 166 myStorageSettings.getMaximumDeleteConflictQueryCount(), 167 theTransactionDetails); 168 ++retryCount; 169 } 170 theDeleteConflicts.addAll(newConflicts); 171 if (retryCount >= MAX_RETRY_ATTEMPTS && !theDeleteConflicts.isEmpty()) { 172 IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(myFhirContext); 173 OperationOutcomeUtil.addIssue( 174 myFhirContext, 175 oo, 176 BaseStorageDao.OO_SEVERITY_ERROR, 177 MAX_RETRY_ATTEMPTS_EXCEEDED_MSG, 178 null, 179 "processing"); 180 throw new ResourceVersionConflictException(Msg.code(821) + MAX_RETRY_ATTEMPTS_EXCEEDED_MSG, oo); 181 } 182 return retryCount; 183 } 184 185 @VisibleForTesting 186 static void setMaxRetryAttempts(Integer theMaxRetryAttempts) { 187 MAX_RETRY_ATTEMPTS = theMaxRetryAttempts; 188 } 189}