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.dao.expunge; 021 022import ca.uhn.fhir.i18n.Msg; 023import ca.uhn.fhir.interceptor.api.HookParams; 024import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 025import ca.uhn.fhir.interceptor.api.Pointcut; 026import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 027import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser; 028import ca.uhn.fhir.jpa.dao.data.IResourceHistoryProvenanceDao; 029import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao; 030import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTagDao; 031import ca.uhn.fhir.jpa.dao.data.IResourceIndexedComboStringUniqueDao; 032import ca.uhn.fhir.jpa.dao.data.IResourceIndexedComboTokensNonUniqueDao; 033import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamCoordsDao; 034import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamDateDao; 035import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamNumberDao; 036import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamQuantityDao; 037import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamQuantityNormalizedDao; 038import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamStringDao; 039import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamTokenDao; 040import ca.uhn.fhir.jpa.dao.data.IResourceIndexedSearchParamUriDao; 041import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao; 042import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; 043import ca.uhn.fhir.jpa.dao.data.IResourceTagDao; 044import ca.uhn.fhir.jpa.dao.data.ISearchParamPresentDao; 045import ca.uhn.fhir.jpa.model.dao.JpaPid; 046import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity; 047import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; 048import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTablePk; 049import ca.uhn.fhir.jpa.model.entity.ResourceTable; 050import ca.uhn.fhir.jpa.util.MemoryCacheService; 051import ca.uhn.fhir.model.primitive.IdDt; 052import ca.uhn.fhir.rest.api.server.RequestDetails; 053import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 054import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 055import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 056import org.apache.commons.lang3.Validate; 057import org.hl7.fhir.instance.model.api.IBaseResource; 058import org.hl7.fhir.instance.model.api.IIdType; 059import org.slf4j.Logger; 060import org.slf4j.LoggerFactory; 061import org.springframework.beans.factory.annotation.Autowired; 062import org.springframework.dao.DataIntegrityViolationException; 063import org.springframework.data.domain.PageRequest; 064import org.springframework.data.domain.Pageable; 065import org.springframework.data.domain.Slice; 066import org.springframework.data.domain.SliceImpl; 067import org.springframework.stereotype.Service; 068import org.springframework.transaction.annotation.Transactional; 069import org.springframework.transaction.support.TransactionSynchronization; 070import org.springframework.transaction.support.TransactionSynchronizationManager; 071 072import java.util.Collections; 073import java.util.List; 074import java.util.Optional; 075import java.util.concurrent.atomic.AtomicInteger; 076 077@Service 078public class JpaResourceExpungeService implements IResourceExpungeService<JpaPid, ResourceHistoryTablePk> { 079 private static final Logger ourLog = LoggerFactory.getLogger(JpaResourceExpungeService.class); 080 081 @Autowired 082 private IResourceTableDao myResourceTableDao; 083 084 @Autowired 085 private IResourceHistoryTableDao myResourceHistoryTableDao; 086 087 @Autowired 088 private IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao; 089 090 @Autowired 091 private IResourceIndexedSearchParamStringDao myResourceIndexedSearchParamStringDao; 092 093 @Autowired 094 private IResourceIndexedSearchParamTokenDao myResourceIndexedSearchParamTokenDao; 095 096 @Autowired 097 private IResourceIndexedSearchParamDateDao myResourceIndexedSearchParamDateDao; 098 099 @Autowired 100 private IResourceIndexedSearchParamQuantityDao myResourceIndexedSearchParamQuantityDao; 101 102 @Autowired 103 private IResourceIndexedSearchParamQuantityNormalizedDao myResourceIndexedSearchParamQuantityNormalizedDao; 104 105 @Autowired 106 private IResourceIndexedSearchParamCoordsDao myResourceIndexedSearchParamCoordsDao; 107 108 @Autowired 109 private IResourceIndexedSearchParamNumberDao myResourceIndexedSearchParamNumberDao; 110 111 @Autowired 112 private IResourceIndexedComboStringUniqueDao myResourceIndexedCompositeStringUniqueDao; 113 114 @Autowired 115 private IResourceIndexedComboTokensNonUniqueDao myResourceIndexedComboTokensNonUniqueDao; 116 117 @Autowired 118 private IResourceLinkDao myResourceLinkDao; 119 120 @Autowired 121 private IResourceTagDao myResourceTagDao; 122 123 @Autowired 124 private IResourceHistoryTagDao myResourceHistoryTagDao; 125 126 @Autowired 127 private IInterceptorBroadcaster myInterceptorBroadcaster; 128 129 @Autowired 130 private IResourceHistoryProvenanceDao myResourceHistoryProvenanceTableDao; 131 132 @Autowired 133 private ISearchParamPresentDao mySearchParamPresentDao; 134 135 @Autowired 136 private JpaStorageSettings myStorageSettings; 137 138 @Autowired 139 private MemoryCacheService myMemoryCacheService; 140 141 @Autowired 142 private IJpaStorageResourceParser myJpaStorageResourceParser; 143 144 @Override 145 @Transactional 146 public List<ResourceHistoryTablePk> findHistoricalVersionsOfNonDeletedResources( 147 String theResourceName, JpaPid theJpaPid, int theRemainingCount) { 148 if (isEmptyQuery(theRemainingCount)) { 149 return Collections.emptyList(); 150 } 151 152 Pageable page = PageRequest.of(0, theRemainingCount); 153 154 Slice<ResourceHistoryTablePk> ids; 155 if (theJpaPid != null && theJpaPid.getId() != null) { 156 if (theJpaPid.getVersion() != null) { 157 ids = toSlice(myResourceHistoryTableDao.findForIdAndVersion(theJpaPid.toFk(), theJpaPid.getVersion())); 158 } else { 159 ids = myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResourceId(page, theJpaPid); 160 } 161 } else { 162 if (theResourceName != null) { 163 ids = myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page, theResourceName); 164 } else { 165 ids = myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page); 166 } 167 } 168 169 return ids.getContent(); 170 } 171 172 @Override 173 @Transactional 174 public List<JpaPid> findHistoricalVersionsOfDeletedResources( 175 String theResourceName, JpaPid theResourceId, int theRemainingCount) { 176 if (isEmptyQuery(theRemainingCount)) { 177 return Collections.emptyList(); 178 } 179 180 Pageable page = PageRequest.of(0, theRemainingCount); 181 Slice<JpaPid> ids; 182 if (theResourceId != null) { 183 ids = myResourceTableDao.findIdsOfDeletedResourcesOfType(page, theResourceId.getId(), theResourceName); 184 ourLog.info( 185 "Expunging {} deleted resources of type[{}] and ID[{}]", 186 ids.getNumberOfElements(), 187 theResourceName, 188 theResourceId); 189 } else { 190 if (theResourceName != null) { 191 ids = myResourceTableDao.findIdsOfDeletedResourcesOfType(page, theResourceName); 192 ourLog.info("Expunging {} deleted resources of type[{}]", ids.getNumberOfElements(), theResourceName); 193 } else { 194 ids = myResourceTableDao.findIdsOfDeletedResources(page); 195 ourLog.info("Expunging {} deleted resources (all types)", ids.getNumberOfElements()); 196 } 197 } 198 return ids.getContent(); 199 } 200 201 @Override 202 @Transactional 203 public void expungeCurrentVersionOfResources( 204 RequestDetails theRequestDetails, List<JpaPid> theResourceIds, AtomicInteger theRemainingCount) { 205 for (JpaPid next : theResourceIds) { 206 expungeCurrentVersionOfResource(theRequestDetails, next, theRemainingCount); 207 if (expungeLimitReached(theRemainingCount)) { 208 return; 209 } 210 } 211 212 /* 213 * Once this transaction is committed, we will invalidate all memory caches 214 * in order to avoid any caches having references to things that no longer 215 * exist. This is a pretty brute-force way of addressing this, and could probably 216 * be optimized, but expunge is hopefully not frequently called on busy servers 217 * so it shouldn't be too big a deal. 218 */ 219 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { 220 @Override 221 public void afterCommit() { 222 myMemoryCacheService.invalidateAllCaches(); 223 } 224 }); 225 } 226 227 private void expungeHistoricalVersion( 228 RequestDetails theRequestDetails, 229 ResourceHistoryTablePk theNextVersionId, 230 AtomicInteger theRemainingCount) { 231 ResourceHistoryTable version = 232 myResourceHistoryTableDao.findById(theNextVersionId).orElseThrow(IllegalArgumentException::new); 233 IdDt id = version.getIdDt(); 234 ourLog.info("Deleting resource version {}", id.getValue()); 235 236 callHooks(theRequestDetails, theRemainingCount, version, id); 237 238 if (myStorageSettings.isAccessMetaSourceInformationFromProvenanceTable()) { 239 Optional<ResourceHistoryProvenanceEntity> provenanceOpt = 240 myResourceHistoryProvenanceTableDao.findById(theNextVersionId.asIdAndPartitionId()); 241 provenanceOpt.ifPresent(entity -> myResourceHistoryProvenanceTableDao.deleteByPid(entity.getId())); 242 } 243 244 myResourceHistoryTagDao.deleteByPid(version.getId()); 245 myResourceHistoryTableDao.deleteByPid(version.getId()); 246 247 theRemainingCount.decrementAndGet(); 248 } 249 250 private void callHooks( 251 RequestDetails theRequestDetails, 252 AtomicInteger theRemainingCount, 253 ResourceHistoryTable theVersion, 254 IdDt theId) { 255 final AtomicInteger counter = new AtomicInteger(); 256 IInterceptorBroadcaster compositeBroadcaster = 257 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails); 258 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE)) { 259 IBaseResource resource = myJpaStorageResourceParser.toResource(theVersion, false); 260 HookParams params = new HookParams() 261 .add(AtomicInteger.class, counter) 262 .add(IIdType.class, theId) 263 .add(IBaseResource.class, resource) 264 .add(RequestDetails.class, theRequestDetails) 265 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); 266 compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE, params); 267 } 268 theRemainingCount.addAndGet(-1 * counter.get()); 269 } 270 271 @Override 272 @Transactional 273 public void expungeHistoricalVersionsOfIds( 274 RequestDetails theRequestDetails, List<JpaPid> theResourceIds, AtomicInteger theRemainingCount) { 275 List<ResourceTable> resourcesToDelete = myResourceTableDao.findAllByIdAndLoadForcedIds(theResourceIds); 276 for (ResourceTable next : resourcesToDelete) { 277 expungeHistoricalVersionsOfId(theRequestDetails, next, theRemainingCount); 278 if (expungeLimitReached(theRemainingCount)) { 279 return; 280 } 281 } 282 } 283 284 @Override 285 @Transactional 286 public void expungeHistoricalVersions( 287 RequestDetails theRequestDetails, 288 List<ResourceHistoryTablePk> theHistoricalIds, 289 AtomicInteger theRemainingCount) { 290 for (ResourceHistoryTablePk next : theHistoricalIds) { 291 expungeHistoricalVersion(theRequestDetails, next, theRemainingCount); 292 if (expungeLimitReached(theRemainingCount)) { 293 return; 294 } 295 } 296 } 297 298 protected void expungeCurrentVersionOfResource( 299 RequestDetails theRequestDetails, JpaPid theResourceId, AtomicInteger theRemainingCount) { 300 301 ResourceTable resource = myResourceTableDao.findById(theResourceId).orElseThrow(IllegalStateException::new); 302 303 ResourceHistoryTable currentVersion = 304 myResourceHistoryTableDao.findForIdAndVersion(resource.getId().toFk(), resource.getVersion()); 305 if (currentVersion != null) { 306 expungeHistoricalVersion(theRequestDetails, currentVersion.getId(), theRemainingCount); 307 } 308 309 ourLog.info( 310 "Expunging current version of resource {}", resource.getIdDt().getValue()); 311 312 try { 313 if (resource.isHasTags()) { 314 myResourceTagDao.deleteByResourceId(resource.getId()); 315 } 316 317 myResourceTableDao.deleteByPid(resource.getId()); 318 } catch (DataIntegrityViolationException e) { 319 throw new PreconditionFailedException(Msg.code(2415) 320 + "The resource could not be expunged. It is likely due to unfinished asynchronous deletions, please try again later: " 321 + e); 322 } 323 } 324 325 @Override 326 @Transactional 327 public void deleteAllSearchParams(JpaPid theResourceId) { 328 ResourceTable resource = myResourceTableDao.findById(theResourceId).orElse(null); 329 330 if (resource == null || resource.isParamsUriPopulated()) { 331 myResourceIndexedSearchParamUriDao.deleteByResourceId(theResourceId); 332 } 333 if (resource == null || resource.isParamsCoordsPopulated()) { 334 myResourceIndexedSearchParamCoordsDao.deleteByResourceId(theResourceId); 335 } 336 if (resource == null || resource.isParamsDatePopulated()) { 337 myResourceIndexedSearchParamDateDao.deleteByResourceId(theResourceId); 338 } 339 if (resource == null || resource.isParamsNumberPopulated()) { 340 myResourceIndexedSearchParamNumberDao.deleteByResourceId(theResourceId); 341 } 342 if (resource == null || resource.isParamsQuantityPopulated()) { 343 myResourceIndexedSearchParamQuantityDao.deleteByResourceId(theResourceId); 344 } 345 if (resource == null || resource.isParamsQuantityNormalizedPopulated()) { 346 myResourceIndexedSearchParamQuantityNormalizedDao.deleteByResourceId(theResourceId); 347 } 348 if (resource == null || resource.isParamsStringPopulated()) { 349 myResourceIndexedSearchParamStringDao.deleteByResourceId(theResourceId); 350 } 351 if (resource == null || resource.isParamsTokenPopulated()) { 352 myResourceIndexedSearchParamTokenDao.deleteByResourceId(theResourceId); 353 } 354 if (resource == null || resource.isParamsComboStringUniquePresent()) { 355 myResourceIndexedCompositeStringUniqueDao.deleteByResourceId(theResourceId); 356 } 357 if (resource == null || resource.isParamsComboTokensNonUniquePresent()) { 358 myResourceIndexedComboTokensNonUniqueDao.deleteByResourceId(theResourceId); 359 } 360 if (myStorageSettings.getIndexMissingFields() == JpaStorageSettings.IndexEnabledEnum.ENABLED) { 361 mySearchParamPresentDao.deleteByResourceId(theResourceId); 362 } 363 if (resource == null || resource.isHasLinks()) { 364 myResourceLinkDao.deleteByResourceId(theResourceId); 365 } 366 } 367 368 private void expungeHistoricalVersionsOfId( 369 RequestDetails theRequestDetails, ResourceTable theResource, AtomicInteger theRemainingCount) { 370 Pageable page; 371 synchronized (theRemainingCount) { 372 if (expungeLimitReached(theRemainingCount)) { 373 return; 374 } 375 page = PageRequest.of(0, theRemainingCount.get()); 376 } 377 378 Slice<ResourceHistoryTablePk> versionIds = myResourceHistoryTableDao.findForResourceId( 379 page, theResource.getId().toFk(), theResource.getVersion()); 380 ourLog.debug( 381 "Found {} versions of resource {} to expunge", 382 versionIds.getNumberOfElements(), 383 theResource.getIdDt().getValue()); 384 for (ResourceHistoryTablePk nextVersionId : versionIds) { 385 expungeHistoricalVersion(theRequestDetails, nextVersionId, theRemainingCount); 386 if (expungeLimitReached(theRemainingCount)) { 387 return; 388 } 389 } 390 } 391 392 private Slice<ResourceHistoryTablePk> toSlice(ResourceHistoryTable theVersion) { 393 Validate.notNull(theVersion, "theVersion must not be null"); 394 return new SliceImpl<>(Collections.singletonList(theVersion.getId())); 395 } 396 397 private boolean isEmptyQuery(int theCount) { 398 return theCount <= 0; 399 } 400 401 private boolean expungeLimitReached(AtomicInteger theRemainingCount) { 402 return theRemainingCount.get() <= 0; 403 } 404}