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.ResourceHistoryTable;
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.concurrent.atomic.AtomicInteger;
075
076@Service
077public class JpaResourceExpungeService implements IResourceExpungeService<JpaPid> {
078        private static final Logger ourLog = LoggerFactory.getLogger(JpaResourceExpungeService.class);
079
080        @Autowired
081        private IResourceTableDao myResourceTableDao;
082
083        @Autowired
084        private IResourceHistoryTableDao myResourceHistoryTableDao;
085
086        @Autowired
087        private IResourceIndexedSearchParamUriDao myResourceIndexedSearchParamUriDao;
088
089        @Autowired
090        private IResourceIndexedSearchParamStringDao myResourceIndexedSearchParamStringDao;
091
092        @Autowired
093        private IResourceIndexedSearchParamTokenDao myResourceIndexedSearchParamTokenDao;
094
095        @Autowired
096        private IResourceIndexedSearchParamDateDao myResourceIndexedSearchParamDateDao;
097
098        @Autowired
099        private IResourceIndexedSearchParamQuantityDao myResourceIndexedSearchParamQuantityDao;
100
101        @Autowired
102        private IResourceIndexedSearchParamQuantityNormalizedDao myResourceIndexedSearchParamQuantityNormalizedDao;
103
104        @Autowired
105        private IResourceIndexedSearchParamCoordsDao myResourceIndexedSearchParamCoordsDao;
106
107        @Autowired
108        private IResourceIndexedSearchParamNumberDao myResourceIndexedSearchParamNumberDao;
109
110        @Autowired
111        private IResourceIndexedComboStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
112
113        @Autowired
114        private IResourceIndexedComboTokensNonUniqueDao myResourceIndexedComboTokensNonUniqueDao;
115
116        @Autowired
117        private IResourceLinkDao myResourceLinkDao;
118
119        @Autowired
120        private IResourceTagDao myResourceTagDao;
121
122        @Autowired
123        private IIdHelperService myIdHelperService;
124
125        @Autowired
126        private IResourceHistoryTagDao myResourceHistoryTagDao;
127
128        @Autowired
129        private IInterceptorBroadcaster myInterceptorBroadcaster;
130
131        @Autowired
132        private DaoRegistry myDaoRegistry;
133
134        @Autowired
135        private IResourceHistoryProvenanceDao myResourceHistoryProvenanceTableDao;
136
137        @Autowired
138        private ISearchParamPresentDao mySearchParamPresentDao;
139
140        @Autowired
141        private JpaStorageSettings myStorageSettings;
142
143        @Autowired
144        private MemoryCacheService myMemoryCacheService;
145
146        @Autowired
147        private IJpaStorageResourceParser myJpaStorageResourceParser;
148
149        @Override
150        @Transactional
151        public List<JpaPid> findHistoricalVersionsOfNonDeletedResources(
152                        String theResourceName, JpaPid theJpaPid, int theRemainingCount) {
153                if (isEmptyQuery(theRemainingCount)) {
154                        return Collections.EMPTY_LIST;
155                }
156
157                Pageable page = PageRequest.of(0, theRemainingCount);
158
159                Slice<Long> ids;
160                if (theJpaPid != null && theJpaPid.getId() != null) {
161                        if (theJpaPid.getVersion() != null) {
162                                ids = toSlice(myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(
163                                                theJpaPid.getId(), theJpaPid.getVersion()));
164                        } else {
165                                ids = myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResourceId(page, theJpaPid.getId());
166                        }
167                } else {
168                        if (theResourceName != null) {
169                                ids = myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page, theResourceName);
170                        } else {
171                                ids = myResourceHistoryTableDao.findIdsOfPreviousVersionsOfResources(page);
172                        }
173                }
174
175                return JpaPid.fromLongList(ids.getContent());
176        }
177
178        @Override
179        @Transactional
180        public List<JpaPid> findHistoricalVersionsOfDeletedResources(
181                        String theResourceName, JpaPid theResourceId, int theRemainingCount) {
182                if (isEmptyQuery(theRemainingCount)) {
183                        return Collections.EMPTY_LIST;
184                }
185
186                Pageable page = PageRequest.of(0, theRemainingCount);
187                Slice<Long> ids;
188                if (theResourceId != null) {
189                        ids = myResourceTableDao.findIdsOfDeletedResourcesOfType(page, theResourceId.getId(), theResourceName);
190                        ourLog.info(
191                                        "Expunging {} deleted resources of type[{}] and ID[{}]",
192                                        ids.getNumberOfElements(),
193                                        theResourceName,
194                                        theResourceId);
195                } else {
196                        if (theResourceName != null) {
197                                ids = myResourceTableDao.findIdsOfDeletedResourcesOfType(page, theResourceName);
198                                ourLog.info("Expunging {} deleted resources of type[{}]", ids.getNumberOfElements(), theResourceName);
199                        } else {
200                                ids = myResourceTableDao.findIdsOfDeletedResources(page);
201                                ourLog.info("Expunging {} deleted resources (all types)", ids.getNumberOfElements());
202                        }
203                }
204                return JpaPid.fromLongList(ids.getContent());
205        }
206
207        @Override
208        @Transactional
209        public void expungeCurrentVersionOfResources(
210                        RequestDetails theRequestDetails, List<JpaPid> theResourceIds, AtomicInteger theRemainingCount) {
211                for (JpaPid next : theResourceIds) {
212                        expungeCurrentVersionOfResource(theRequestDetails, (next).getId(), theRemainingCount);
213                        if (expungeLimitReached(theRemainingCount)) {
214                                return;
215                        }
216                }
217
218                /*
219                 * Once this transaction is committed, we will invalidate all memory caches
220                 * in order to avoid any caches having references to things that no longer
221                 * exist. This is a pretty brute-force way of addressing this, and could probably
222                 * be optimized, but expunge is hopefully not frequently called on busy servers
223                 * so it shouldn't be too big a deal.
224                 */
225                TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
226                        @Override
227                        public void afterCommit() {
228                                myMemoryCacheService.invalidateAllCaches();
229                        }
230                });
231        }
232
233        private void expungeHistoricalVersion(
234                        RequestDetails theRequestDetails, Long theNextVersionId, AtomicInteger theRemainingCount) {
235                ResourceHistoryTable version =
236                                myResourceHistoryTableDao.findById(theNextVersionId).orElseThrow(IllegalArgumentException::new);
237                IdDt id = version.getIdDt();
238                ourLog.info("Deleting resource version {}", id.getValue());
239
240                callHooks(theRequestDetails, theRemainingCount, version, id);
241
242                if (version.getProvenance() != null) {
243                        myResourceHistoryProvenanceTableDao.deleteByPid(
244                                        version.getProvenance().getId());
245                }
246
247                myResourceHistoryTagDao.deleteByPid(version.getId());
248                myResourceHistoryTableDao.deleteByPid(version.getId());
249
250                theRemainingCount.decrementAndGet();
251        }
252
253        private void callHooks(
254                        RequestDetails theRequestDetails,
255                        AtomicInteger theRemainingCount,
256                        ResourceHistoryTable theVersion,
257                        IdDt theId) {
258                final AtomicInteger counter = new AtomicInteger();
259                if (CompositeInterceptorBroadcaster.hasHooks(
260                                Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE, myInterceptorBroadcaster, theRequestDetails)) {
261                        IBaseResource resource = myJpaStorageResourceParser.toResource(theVersion, false);
262                        HookParams params = new HookParams()
263                                        .add(AtomicInteger.class, counter)
264                                        .add(IIdType.class, theId)
265                                        .add(IBaseResource.class, resource)
266                                        .add(RequestDetails.class, theRequestDetails)
267                                        .addIfMatchesType(ServletRequestDetails.class, theRequestDetails);
268                        CompositeInterceptorBroadcaster.doCallHooks(
269                                        myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_EXPUNGE_RESOURCE, params);
270                }
271                theRemainingCount.addAndGet(-1 * counter.get());
272        }
273
274        @Override
275        @Transactional
276        public void expungeHistoricalVersionsOfIds(
277                        RequestDetails theRequestDetails, List<JpaPid> theResourceIds, AtomicInteger theRemainingCount) {
278                List<Long> pids = JpaPid.toLongList(theResourceIds);
279
280                List<ResourceTable> resourcesToDelete = myResourceTableDao.findAllByIdAndLoadForcedIds(pids);
281                for (ResourceTable next : resourcesToDelete) {
282                        expungeHistoricalVersionsOfId(theRequestDetails, next, theRemainingCount);
283                        if (expungeLimitReached(theRemainingCount)) {
284                                return;
285                        }
286                }
287        }
288
289        @Override
290        @Transactional
291        public void expungeHistoricalVersions(
292                        RequestDetails theRequestDetails, List<JpaPid> theHistoricalIds, AtomicInteger theRemainingCount) {
293                for (JpaPid next : theHistoricalIds) {
294                        expungeHistoricalVersion(theRequestDetails, (next).getId(), theRemainingCount);
295                        if (expungeLimitReached(theRemainingCount)) {
296                                return;
297                        }
298                }
299        }
300
301        private void expungeCurrentVersionOfResource(
302                        RequestDetails theRequestDetails, Long theResourceId, AtomicInteger theRemainingCount) {
303                ResourceTable resource = myResourceTableDao.findById(theResourceId).orElseThrow(IllegalStateException::new);
304
305                ResourceHistoryTable currentVersion = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(
306                                resource.getId(), resource.getVersion());
307                if (currentVersion != null) {
308                        expungeHistoricalVersion(theRequestDetails, currentVersion.getId(), theRemainingCount);
309                }
310
311                ourLog.info(
312                                "Expunging current version of resource {}", resource.getIdDt().getValue());
313
314                deleteAllSearchParams(JpaPid.fromId(resource.getResourceId()));
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}