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}