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