
001package ca.uhn.fhir.jpa.dao.expunge; 002 003/*- 004 * #%L 005 * HAPI FHIR JPA Server 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 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.DaoConfig; 027import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 028import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 029import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 030import ca.uhn.fhir.jpa.dao.data.IForcedIdDao; 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.IResourceProvenanceDao; 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.entity.ForcedId; 049import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; 050import ca.uhn.fhir.jpa.model.entity.ResourceTable; 051import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; 052import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 053import ca.uhn.fhir.jpa.util.MemoryCacheService; 054import ca.uhn.fhir.model.primitive.IdDt; 055import ca.uhn.fhir.rest.api.server.RequestDetails; 056import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 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.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 ResourceExpungeService implements IResourceExpungeService { 078 private static final Logger ourLog = LoggerFactory.getLogger(ResourceExpungeService.class); 079 080 @Autowired 081 private IForcedIdDao myForcedIdDao; 082 @Autowired 083 private IResourceTableDao myResourceTableDao; 084 @Autowired 085 private IResourceHistoryTableDao myResourceHistoryTableDao; 086 @Autowired 087 private IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao; 088 @Autowired 089 private IResourceIndexedSearchParamStringDao myResourceIndexedSearchParamStringDao; 090 @Autowired 091 private IResourceIndexedSearchParamTokenDao myResourceIndexedSearchParamTokenDao; 092 @Autowired 093 private IResourceIndexedSearchParamDateDao myResourceIndexedSearchParamDateDao; 094 @Autowired 095 private IResourceIndexedSearchParamQuantityDao myResourceIndexedSearchParamQuantityDao; 096 @Autowired 097 private IResourceIndexedSearchParamQuantityNormalizedDao myResourceIndexedSearchParamQuantityNormalizedDao; 098 @Autowired 099 private IResourceIndexedSearchParamCoordsDao myResourceIndexedSearchParamCoordsDao; 100 @Autowired 101 private IResourceIndexedSearchParamNumberDao myResourceIndexedSearchParamNumberDao; 102 @Autowired 103 private IResourceIndexedComboStringUniqueDao myResourceIndexedCompositeStringUniqueDao; 104 @Autowired 105 private IResourceIndexedComboTokensNonUniqueDao myResourceIndexedComboTokensNonUniqueDao; 106 @Autowired 107 private IResourceLinkDao myResourceLinkDao; 108 @Autowired 109 private IResourceTagDao myResourceTagDao; 110 @Autowired 111 private IIdHelperService myIdHelperService; 112 @Autowired 113 private IResourceHistoryTagDao myResourceHistoryTagDao; 114 @Autowired 115 private IInterceptorBroadcaster myInterceptorBroadcaster; 116 @Autowired 117 private DaoRegistry myDaoRegistry; 118 @Autowired 119 private IResourceProvenanceDao myResourceHistoryProvenanceTableDao; 120 @Autowired 121 private ISearchParamPresentDao mySearchParamPresentDao; 122 @Autowired 123 private DaoConfig myDaoConfig; 124 @Autowired 125 private MemoryCacheService myMemoryCacheService; 126 127 @Override 128 @Transactional 129 public List<ResourcePersistentId> findHistoricalVersionsOfNonDeletedResources(String theResourceName, ResourcePersistentId theResourceId, int theRemainingCount) { 130 Pageable page = PageRequest.of(0, theRemainingCount); 131 132 Slice<Long> ids; 133 if (theResourceId != null && theResourceId.getId() != null) { 134 if (theResourceId.getVersion() != null) { 135 ids = toSlice(myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(theResourceId.getIdAsLong(), theResourceId.getVersion())); 136 } else { 137 ids = myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResourceId(page, theResourceId.getIdAsLong()); 138 } 139 } else { 140 if (theResourceName != null) { 141 ids = myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page, theResourceName); 142 } else { 143 ids = myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page); 144 } 145 } 146 147 return ResourcePersistentId.fromLongList(ids.getContent()); 148 } 149 150 @Override 151 @Transactional 152 public List<ResourcePersistentId> findHistoricalVersionsOfDeletedResources(String theResourceName, ResourcePersistentId theResourceId, int theRemainingCount) { 153 Pageable page = PageRequest.of(0, theRemainingCount); 154 Slice<Long> ids; 155 if (theResourceId != null) { 156 ids = myResourceTableDao.findIdsOfDeletedResourcesOfType(page, theResourceId.getIdAsLong(), theResourceName); 157 ourLog.info("Expunging {} deleted resources of type[{}] and ID[{}]", ids.getNumberOfElements(), theResourceName, theResourceId); 158 } else { 159 if (theResourceName != null) { 160 ids = myResourceTableDao.findIdsOfDeletedResourcesOfType(page, theResourceName); 161 ourLog.info("Expunging {} deleted resources of type[{}]", ids.getNumberOfElements(), theResourceName); 162 } else { 163 ids = myResourceTableDao.findIdsOfDeletedResources(page); 164 ourLog.info("Expunging {} deleted resources (all types)", ids.getNumberOfElements()); 165 } 166 } 167 return ResourcePersistentId.fromLongList(ids.getContent()); 168 } 169 170 @Override 171 @Transactional 172 public void expungeCurrentVersionOfResources(RequestDetails theRequestDetails, List<ResourcePersistentId> theResourceIds, AtomicInteger theRemainingCount) { 173 for (ResourcePersistentId next : theResourceIds) { 174 expungeCurrentVersionOfResource(theRequestDetails, next.getIdAsLong(), theRemainingCount); 175 if (theRemainingCount.get() <= 0) { 176 return; 177 } 178 } 179 180 /* 181 * Once this transaction is committed, we will invalidate all memory caches 182 * in order to avoid any caches having references to things that no longer 183 * exist. This is a pretty brute-force way of addressing this, and could probably 184 * be optimized, but expunge is hopefully not frequently called on busy servers 185 * so it shouldn't be too big a deal. 186 */ 187 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization(){ 188 @Override 189 public void afterCommit() { 190 myMemoryCacheService.invalidateAllCaches(); 191 } 192 }); 193 } 194 195 private void expungeHistoricalVersion(RequestDetails theRequestDetails, Long theNextVersionId, AtomicInteger theRemainingCount) { 196 ResourceHistoryTable version = myResourceHistoryTableDao.findById(theNextVersionId).orElseThrow(IllegalArgumentException::new); 197 IdDt id = version.getIdDt(); 198 ourLog.info("Deleting resource version {}", id.getValue()); 199 200 callHooks(theRequestDetails, theRemainingCount, version, id); 201 202 if (version.getProvenance() != null) { 203 myResourceHistoryProvenanceTableDao.deleteByPid(version.getProvenance().getId()); 204 } 205 206 myResourceHistoryTagDao.deleteByPid(version.getId()); 207 myResourceHistoryTableDao.deleteByPid(version.getId()); 208 209 theRemainingCount.decrementAndGet(); 210 } 211 212 private void callHooks(RequestDetails theRequestDetails, AtomicInteger theRemainingCount, ResourceHistoryTable theVersion, IdDt theId) { 213 final AtomicInteger counter = new AtomicInteger(); 214 if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE, myInterceptorBroadcaster, theRequestDetails)) { 215 IFhirResourceDao<?> resourceDao = myDaoRegistry.getResourceDao(theId.getResourceType()); 216 IBaseResource resource = resourceDao.toResource(theVersion, false); 217 HookParams params = new HookParams() 218 .add(AtomicInteger.class, counter) 219 .add(IIdType.class, theId) 220 .add(IBaseResource.class, resource) 221 .add(RequestDetails.class, theRequestDetails) 222 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); 223 CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE, params); 224 } 225 theRemainingCount.addAndGet(-1 * counter.get()); 226 } 227 228 @Override 229 @Transactional 230 public void expungeHistoricalVersionsOfIds(RequestDetails theRequestDetails, List<ResourcePersistentId> theResourceIds, AtomicInteger theRemainingCount) { 231 for (ResourcePersistentId next : theResourceIds) { 232 expungeHistoricalVersionsOfId(theRequestDetails, next.getIdAsLong(), theRemainingCount); 233 if (theRemainingCount.get() <= 0) { 234 return; 235 } 236 } 237 } 238 239 @Override 240 @Transactional 241 public void expungeHistoricalVersions(RequestDetails theRequestDetails, List<ResourcePersistentId> theHistoricalIds, AtomicInteger theRemainingCount) { 242 for (ResourcePersistentId next : theHistoricalIds) { 243 expungeHistoricalVersion(theRequestDetails, next.getIdAsLong(), theRemainingCount); 244 if (theRemainingCount.get() <= 0) { 245 return; 246 } 247 } 248 } 249 250 private void expungeCurrentVersionOfResource(RequestDetails theRequestDetails, Long theResourceId, AtomicInteger theRemainingCount) { 251 ResourceTable resource = myResourceTableDao.findById(theResourceId).orElseThrow(IllegalStateException::new); 252 253 ResourceHistoryTable currentVersion = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(resource.getId(), resource.getVersion()); 254 if (currentVersion != null) { 255 expungeHistoricalVersion(theRequestDetails, currentVersion.getId(), theRemainingCount); 256 } 257 258 ourLog.info("Expunging current version of resource {}", resource.getIdDt().getValue()); 259 260 deleteAllSearchParams(new ResourcePersistentId(resource.getResourceId())); 261 resource.getTags().clear(); 262 263 if (resource.getForcedId() != null) { 264 ForcedId forcedId = resource.getForcedId(); 265 resource.setForcedId(null); 266 myResourceTableDao.saveAndFlush(resource); 267 myForcedIdDao.deleteByPid(forcedId.getId()); 268 } 269 270 myResourceTableDao.deleteByPid(resource.getId()); 271 } 272 273 @Override 274 @Transactional 275 public void deleteAllSearchParams(ResourcePersistentId theResourceId) { 276 ResourceTable resource = myResourceTableDao.findById(theResourceId.getIdAsLong()).orElse(null); 277 278 if (resource == null || resource.isParamsUriPopulated()) { 279 myResourceIndexedSearchParamUriDao.deleteByResourceId(theResourceId.getIdAsLong()); 280 } 281 if (resource == null || resource.isParamsCoordsPopulated()) { 282 myResourceIndexedSearchParamCoordsDao.deleteByResourceId(theResourceId.getIdAsLong()); 283 } 284 if (resource == null || resource.isParamsDatePopulated()) { 285 myResourceIndexedSearchParamDateDao.deleteByResourceId(theResourceId.getIdAsLong()); 286 } 287 if (resource == null || resource.isParamsNumberPopulated()) { 288 myResourceIndexedSearchParamNumberDao.deleteByResourceId(theResourceId.getIdAsLong()); 289 } 290 if (resource == null || resource.isParamsQuantityPopulated()) { 291 myResourceIndexedSearchParamQuantityDao.deleteByResourceId(theResourceId.getIdAsLong()); 292 } 293 if (resource == null || resource.isParamsQuantityNormalizedPopulated()) { 294 myResourceIndexedSearchParamQuantityNormalizedDao.deleteByResourceId(theResourceId.getIdAsLong()); 295 } 296 if (resource == null || resource.isParamsStringPopulated()) { 297 myResourceIndexedSearchParamStringDao.deleteByResourceId(theResourceId.getIdAsLong()); 298 } 299 if (resource == null || resource.isParamsTokenPopulated()) { 300 myResourceIndexedSearchParamTokenDao.deleteByResourceId(theResourceId.getIdAsLong()); 301 } 302 if (resource == null || resource.isParamsComboStringUniquePresent()) { 303 myResourceIndexedCompositeStringUniqueDao.deleteByResourceId(theResourceId.getIdAsLong()); 304 } 305 if (resource == null || resource.isParamsComboTokensNonUniquePresent()) { 306 myResourceIndexedComboTokensNonUniqueDao.deleteByResourceId(theResourceId.getIdAsLong()); 307 } 308 if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.ENABLED) { 309 mySearchParamPresentDao.deleteByResourceId(theResourceId.getIdAsLong()); 310 } 311 if (resource == null || resource.isHasLinks()) { 312 myResourceLinkDao.deleteByResourceId(theResourceId.getIdAsLong()); 313 } 314 315 if (resource == null || resource.isHasTags()) { 316 myResourceTagDao.deleteByResourceId(theResourceId.getIdAsLong()); 317 } 318 } 319 320 private void expungeHistoricalVersionsOfId(RequestDetails theRequestDetails, Long myResourceId, AtomicInteger theRemainingCount) { 321 ResourceTable resource = myResourceTableDao.findById(myResourceId).orElseThrow(IllegalArgumentException::new); 322 323 Pageable page = PageRequest.of(0, theRemainingCount.get()); 324 325 Slice<Long> versionIds = myResourceHistoryTableDao.findForResourceId(page, resource.getId(), resource.getVersion()); 326 ourLog.debug("Found {} versions of resource {} to expunge", versionIds.getNumberOfElements(), resource.getIdDt().getValue()); 327 for (Long nextVersionId : versionIds) { 328 expungeHistoricalVersion(theRequestDetails, nextVersionId, theRemainingCount); 329 if (theRemainingCount.get() <= 0) { 330 return; 331 } 332 } 333 } 334 335 private Slice<Long> toSlice(ResourceHistoryTable myVersion) { 336 Validate.notNull(myVersion); 337 return new SliceImpl<>(Collections.singletonList(myVersion.getId())); 338 } 339}