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        protected 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                try {
315                        if (resource.isHasTags()) {
316                                myResourceTagDao.deleteByResourceId(resource.getId());
317                        }
318
319                        myResourceTableDao.deleteByPid(resource.getId());
320                } catch (DataIntegrityViolationException e) {
321                        throw new PreconditionFailedException(Msg.code(2415)
322                                        + "The resource could not be expunged. It is likely due to unfinished asynchronous deletions, please try again later: "
323                                        + e);
324                }
325        }
326
327        @Override
328        @Transactional
329        public void deleteAllSearchParams(JpaPid theResourceId) {
330                Long theResourceLongId = theResourceId.getId();
331                ResourceTable resource = myResourceTableDao.findById(theResourceLongId).orElse(null);
332
333                if (resource == null || resource.isParamsUriPopulated()) {
334                        myResourceIndexedSearchParamUriDao.deleteByResourceId(theResourceLongId);
335                }
336                if (resource == null || resource.isParamsCoordsPopulated()) {
337                        myResourceIndexedSearchParamCoordsDao.deleteByResourceId(theResourceLongId);
338                }
339                if (resource == null || resource.isParamsDatePopulated()) {
340                        myResourceIndexedSearchParamDateDao.deleteByResourceId(theResourceLongId);
341                }
342                if (resource == null || resource.isParamsNumberPopulated()) {
343                        myResourceIndexedSearchParamNumberDao.deleteByResourceId(theResourceLongId);
344                }
345                if (resource == null || resource.isParamsQuantityPopulated()) {
346                        myResourceIndexedSearchParamQuantityDao.deleteByResourceId(theResourceLongId);
347                }
348                if (resource == null || resource.isParamsQuantityNormalizedPopulated()) {
349                        myResourceIndexedSearchParamQuantityNormalizedDao.deleteByResourceId(theResourceLongId);
350                }
351                if (resource == null || resource.isParamsStringPopulated()) {
352                        myResourceIndexedSearchParamStringDao.deleteByResourceId(theResourceLongId);
353                }
354                if (resource == null || resource.isParamsTokenPopulated()) {
355                        myResourceIndexedSearchParamTokenDao.deleteByResourceId(theResourceLongId);
356                }
357                if (resource == null || resource.isParamsComboStringUniquePresent()) {
358                        myResourceIndexedCompositeStringUniqueDao.deleteByResourceId(theResourceLongId);
359                }
360                if (resource == null || resource.isParamsComboTokensNonUniquePresent()) {
361                        myResourceIndexedComboTokensNonUniqueDao.deleteByResourceId(theResourceLongId);
362                }
363                if (myStorageSettings.getIndexMissingFields() == JpaStorageSettings.IndexEnabledEnum.ENABLED) {
364                        mySearchParamPresentDao.deleteByResourceId(theResourceLongId);
365                }
366                if (resource == null || resource.isHasLinks()) {
367                        myResourceLinkDao.deleteByResourceId(theResourceLongId);
368                }
369        }
370
371        private void expungeHistoricalVersionsOfId(
372                        RequestDetails theRequestDetails, ResourceTable theResource, AtomicInteger theRemainingCount) {
373                Pageable page;
374                synchronized (theRemainingCount) {
375                        if (expungeLimitReached(theRemainingCount)) {
376                                return;
377                        }
378                        page = PageRequest.of(0, theRemainingCount.get());
379                }
380
381                Slice<Long> versionIds =
382                                myResourceHistoryTableDao.findForResourceId(page, theResource.getId(), theResource.getVersion());
383                ourLog.debug(
384                                "Found {} versions of resource {} to expunge",
385                                versionIds.getNumberOfElements(),
386                                theResource.getIdDt().getValue());
387                for (Long nextVersionId : versionIds) {
388                        expungeHistoricalVersion(theRequestDetails, nextVersionId, theRemainingCount);
389                        if (expungeLimitReached(theRemainingCount)) {
390                                return;
391                        }
392                }
393        }
394
395        private Slice<Long> toSlice(ResourceHistoryTable myVersion) {
396                Validate.notNull(myVersion);
397                return new SliceImpl<>(Collections.singletonList(myVersion.getId()));
398        }
399
400        private boolean isEmptyQuery(int theCount) {
401                return theCount <= 0;
402        }
403
404        private boolean expungeLimitReached(AtomicInteger theRemainingCount) {
405                return theRemainingCount.get() <= 0;
406        }
407}