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}