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