001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 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 HookParams hooks = new HookParams() 110 .add(DeleteConflictList.class, theDeleteConflicts) 111 .add(RequestDetails.class, theRequest) 112 .addIfMatchesType(ServletRequestDetails.class, theRequest) 113 .add(TransactionDetails.class, theTransactionDetails); 114 return (DeleteConflictOutcome) CompositeInterceptorBroadcaster.doCallHooksAndReturnObject( 115 myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESTORAGE_DELETE_CONFLICTS, hooks); 116 } 117 118 private void addConflictsToList( 119 DeleteConflictList theDeleteConflicts, ResourceTable theEntity, List<ResourceLink> theResultList) { 120 for (ResourceLink link : theResultList) { 121 IdDt targetId = theEntity.getIdDt(); 122 IdDt sourceId = link.getSourceResource().getIdDt(); 123 String sourcePath = link.getSourcePath(); 124 if (theDeleteConflicts.isResourceIdMarkedForDeletion(sourceId)) { 125 if (theDeleteConflicts.isResourceIdMarkedForDeletion(targetId)) { 126 continue; 127 } 128 } 129 130 theDeleteConflicts.add(new DeleteConflict(sourceId, sourcePath, targetId)); 131 } 132 } 133 134 public int validateOkToDelete( 135 DeleteConflictList theDeleteConflicts, 136 ResourceTable theEntity, 137 boolean theForValidate, 138 RequestDetails theRequest, 139 TransactionDetails theTransactionDetails) { 140 141 // We want the list of resources that are marked to be the same list even as we 142 // drill into conflict resolution stacks.. this allows us to not get caught by 143 // circular references 144 DeleteConflictList newConflicts = new DeleteConflictList(theDeleteConflicts); 145 146 // In most cases, there will be no hooks, and so we only need to check if there is at least 147 // FIRST_QUERY_RESULT_COUNT conflict and populate that. 148 // Only in the case where there is a hook do we need to go back and collect larger batches of conflicts for 149 // processing. 150 151 DeleteConflictOutcome outcome = findAndHandleConflicts( 152 theRequest, newConflicts, theEntity, theForValidate, FIRST_QUERY_RESULT_COUNT, theTransactionDetails); 153 154 int retryCount = 0; 155 while (outcome != null) { 156 int shouldRetryCount = Math.min(outcome.getShouldRetryCount(), MAX_RETRY_ATTEMPTS); 157 if (!(retryCount < shouldRetryCount)) break; 158 newConflicts = new DeleteConflictList(newConflicts); 159 outcome = findAndHandleConflicts( 160 theRequest, 161 newConflicts, 162 theEntity, 163 theForValidate, 164 myStorageSettings.getMaximumDeleteConflictQueryCount(), 165 theTransactionDetails); 166 ++retryCount; 167 } 168 theDeleteConflicts.addAll(newConflicts); 169 if (retryCount >= MAX_RETRY_ATTEMPTS && !theDeleteConflicts.isEmpty()) { 170 IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(myFhirContext); 171 OperationOutcomeUtil.addIssue( 172 myFhirContext, 173 oo, 174 BaseStorageDao.OO_SEVERITY_ERROR, 175 MAX_RETRY_ATTEMPTS_EXCEEDED_MSG, 176 null, 177 "processing"); 178 throw new ResourceVersionConflictException(Msg.code(821) + MAX_RETRY_ATTEMPTS_EXCEEDED_MSG, oo); 179 } 180 return retryCount; 181 } 182 183 @VisibleForTesting 184 static void setMaxRetryAttempts(Integer theMaxRetryAttempts) { 185 MAX_RETRY_ATTEMPTS = theMaxRetryAttempts; 186 } 187}