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