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