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