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.interceptor.api.HookParams;
023import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
024import ca.uhn.fhir.interceptor.api.Pointcut;
025import ca.uhn.fhir.interceptor.model.RequestPartitionId;
026import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
027import ca.uhn.fhir.jpa.entity.Batch2JobInstanceEntity;
028import ca.uhn.fhir.jpa.entity.Batch2WorkChunkEntity;
029import ca.uhn.fhir.jpa.entity.BulkImportJobEntity;
030import ca.uhn.fhir.jpa.entity.BulkImportJobFileEntity;
031import ca.uhn.fhir.jpa.entity.MdmLink;
032import ca.uhn.fhir.jpa.entity.PartitionEntity;
033import ca.uhn.fhir.jpa.entity.Search;
034import ca.uhn.fhir.jpa.entity.SearchInclude;
035import ca.uhn.fhir.jpa.entity.SearchResult;
036import ca.uhn.fhir.jpa.entity.SubscriptionTable;
037import ca.uhn.fhir.jpa.entity.TermCodeSystem;
038import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
039import ca.uhn.fhir.jpa.entity.TermConcept;
040import ca.uhn.fhir.jpa.entity.TermConceptDesignation;
041import ca.uhn.fhir.jpa.entity.TermConceptMap;
042import ca.uhn.fhir.jpa.entity.TermConceptMapGroup;
043import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElement;
044import ca.uhn.fhir.jpa.entity.TermConceptMapGroupElementTarget;
045import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink;
046import ca.uhn.fhir.jpa.entity.TermConceptProperty;
047import ca.uhn.fhir.jpa.entity.TermValueSet;
048import ca.uhn.fhir.jpa.entity.TermValueSetConcept;
049import ca.uhn.fhir.jpa.entity.TermValueSetConceptDesignation;
050import ca.uhn.fhir.jpa.model.entity.NpmPackageEntity;
051import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity;
052import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionResourceEntity;
053import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity;
054import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
055import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTag;
056import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique;
057import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique;
058import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamCoords;
059import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate;
060import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamNumber;
061import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity;
062import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantityNormalized;
063import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
064import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
065import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
066import ca.uhn.fhir.jpa.model.entity.ResourceLink;
067import ca.uhn.fhir.jpa.model.entity.ResourceSearchUrlEntity;
068import ca.uhn.fhir.jpa.model.entity.ResourceTable;
069import ca.uhn.fhir.jpa.model.entity.ResourceTag;
070import ca.uhn.fhir.jpa.model.entity.SearchParamPresentEntity;
071import ca.uhn.fhir.jpa.model.entity.TagDefinition;
072import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
073import ca.uhn.fhir.jpa.util.MemoryCacheService;
074import ca.uhn.fhir.rest.api.server.RequestDetails;
075import ca.uhn.fhir.rest.server.provider.ProviderConstants;
076import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
077import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
078import ca.uhn.fhir.util.StopWatch;
079import jakarta.annotation.Nullable;
080import jakarta.persistence.EntityManager;
081import jakarta.persistence.PersistenceContext;
082import jakarta.persistence.PersistenceContextType;
083import jakarta.persistence.Query;
084import jakarta.persistence.TypedQuery;
085import jakarta.persistence.criteria.CriteriaBuilder;
086import jakarta.persistence.criteria.CriteriaQuery;
087import jakarta.persistence.metamodel.EntityType;
088import jakarta.persistence.metamodel.Metamodel;
089import jakarta.persistence.metamodel.SingularAttribute;
090import org.slf4j.Logger;
091import org.slf4j.LoggerFactory;
092import org.springframework.beans.factory.annotation.Autowired;
093import org.springframework.stereotype.Service;
094import org.springframework.transaction.annotation.Propagation;
095
096import java.util.List;
097import java.util.Set;
098import java.util.concurrent.atomic.AtomicInteger;
099
100@Service
101public class ExpungeEverythingService implements IExpungeEverythingService {
102        private static final Logger ourLog = LoggerFactory.getLogger(ExpungeEverythingService.class);
103
104        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
105        protected EntityManager myEntityManager;
106
107        @Autowired
108        protected IInterceptorBroadcaster myInterceptorBroadcaster;
109
110        @Autowired
111        private HapiTransactionService myTxService;
112
113        @Autowired
114        private MemoryCacheService myMemoryCacheService;
115
116        @Autowired
117        private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
118
119        private int deletedResourceEntityCount;
120
121        @Override
122        public void expungeEverything(@Nullable RequestDetails theRequest) {
123
124                final AtomicInteger counter = new AtomicInteger();
125
126                // Notify Interceptors about pre-action call
127                IInterceptorBroadcaster compositeBroadcaster =
128                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest);
129                if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING)) {
130                        HookParams hooks = new HookParams()
131                                        .add(AtomicInteger.class, counter)
132                                        .add(RequestDetails.class, theRequest)
133                                        .addIfMatchesType(ServletRequestDetails.class, theRequest);
134                        compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING, hooks);
135                }
136
137                ourLog.info("BEGINNING GLOBAL $expunge");
138                Propagation propagation = Propagation.REQUIRES_NEW;
139                RequestPartitionId requestPartitionId =
140                                myRequestPartitionHelperSvc.determineReadPartitionForRequestForServerOperation(
141                                                theRequest, ProviderConstants.OPERATION_EXPUNGE);
142
143                deleteAll(theRequest, propagation, requestPartitionId, counter);
144
145                purgeAllCaches();
146
147                ourLog.info("COMPLETED GLOBAL $expunge - Deleted {} rows", counter.get());
148        }
149
150        protected void deleteAll(
151                        @Nullable RequestDetails theRequest,
152                        Propagation propagation,
153                        RequestPartitionId requestPartitionId,
154                        AtomicInteger counter) {
155                myTxService
156                                .withRequest(theRequest)
157                                .withPropagation(propagation)
158                                .withRequestPartitionId(requestPartitionId)
159                                .execute(() -> {
160                                        counter.addAndGet(doExpungeEverythingQuery(
161                                                        "UPDATE " + TermCodeSystem.class.getSimpleName() + " d SET d.myCurrentVersion = null"));
162                                });
163                counter.addAndGet(
164                                expungeEverythingByTypeWithoutPurging(theRequest, Batch2WorkChunkEntity.class, requestPartitionId));
165                counter.addAndGet(
166                                expungeEverythingByTypeWithoutPurging(theRequest, Batch2JobInstanceEntity.class, requestPartitionId));
167                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
168                                theRequest, NpmPackageVersionResourceEntity.class, requestPartitionId));
169                counter.addAndGet(
170                                expungeEverythingByTypeWithoutPurging(theRequest, NpmPackageVersionEntity.class, requestPartitionId));
171                counter.addAndGet(
172                                expungeEverythingByTypeWithoutPurging(theRequest, NpmPackageEntity.class, requestPartitionId));
173                counter.addAndGet(
174                                expungeEverythingByTypeWithoutPurging(theRequest, SearchParamPresentEntity.class, requestPartitionId));
175                counter.addAndGet(
176                                expungeEverythingByTypeWithoutPurging(theRequest, BulkImportJobFileEntity.class, requestPartitionId));
177                counter.addAndGet(
178                                expungeEverythingByTypeWithoutPurging(theRequest, BulkImportJobEntity.class, requestPartitionId));
179                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
180                                theRequest, ResourceIndexedSearchParamDate.class, requestPartitionId));
181                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
182                                theRequest, ResourceIndexedSearchParamNumber.class, requestPartitionId));
183                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
184                                theRequest, ResourceIndexedSearchParamQuantity.class, requestPartitionId));
185                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
186                                theRequest, ResourceIndexedSearchParamQuantityNormalized.class, requestPartitionId));
187                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
188                                theRequest, ResourceIndexedSearchParamString.class, requestPartitionId));
189                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
190                                theRequest, ResourceIndexedSearchParamToken.class, requestPartitionId));
191                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
192                                theRequest, ResourceIndexedSearchParamUri.class, requestPartitionId));
193                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
194                                theRequest, ResourceIndexedSearchParamCoords.class, requestPartitionId));
195                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
196                                theRequest, ResourceIndexedComboStringUnique.class, requestPartitionId));
197                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
198                                theRequest, ResourceIndexedComboTokenNonUnique.class, requestPartitionId));
199                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceLink.class, requestPartitionId));
200                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SearchResult.class, requestPartitionId));
201                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SearchInclude.class, requestPartitionId));
202                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
203                                theRequest, TermValueSetConceptDesignation.class, requestPartitionId));
204                counter.addAndGet(
205                                expungeEverythingByTypeWithoutPurging(theRequest, TermValueSetConcept.class, requestPartitionId));
206                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermValueSet.class, requestPartitionId));
207                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
208                                theRequest, TermConceptParentChildLink.class, requestPartitionId));
209                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
210                                theRequest, TermConceptMapGroupElementTarget.class, requestPartitionId));
211                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
212                                theRequest, TermConceptMapGroupElement.class, requestPartitionId));
213                counter.addAndGet(
214                                expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMapGroup.class, requestPartitionId));
215                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMap.class, requestPartitionId));
216                counter.addAndGet(
217                                expungeEverythingByTypeWithoutPurging(theRequest, TermConceptProperty.class, requestPartitionId));
218                counter.addAndGet(
219                                expungeEverythingByTypeWithoutPurging(theRequest, TermConceptDesignation.class, requestPartitionId));
220                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConcept.class, requestPartitionId));
221                myTxService
222                                .withRequest(theRequest)
223                                .withPropagation(propagation)
224                                .withRequestPartitionId(requestPartitionId)
225                                .execute(() -> {
226                                        for (TermCodeSystem next : myEntityManager
227                                                        .createQuery("SELECT c FROM " + TermCodeSystem.class.getName() + " c", TermCodeSystem.class)
228                                                        .getResultList()) {
229                                                next.setCurrentVersion(null);
230                                                myEntityManager.merge(next);
231                                        }
232                                });
233                counter.addAndGet(
234                                expungeEverythingByTypeWithoutPurging(theRequest, TermCodeSystemVersion.class, requestPartitionId));
235                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermCodeSystem.class, requestPartitionId));
236                counter.addAndGet(
237                                expungeEverythingByTypeWithoutPurging(theRequest, SubscriptionTable.class, requestPartitionId));
238                counter.addAndGet(
239                                expungeEverythingByTypeWithoutPurging(theRequest, ResourceHistoryTag.class, requestPartitionId));
240                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceTag.class, requestPartitionId));
241                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TagDefinition.class, requestPartitionId));
242                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
243                                theRequest, ResourceHistoryProvenanceEntity.class, requestPartitionId));
244                counter.addAndGet(
245                                expungeEverythingByTypeWithoutPurging(theRequest, ResourceHistoryTable.class, requestPartitionId));
246                counter.addAndGet(
247                                expungeEverythingByTypeWithoutPurging(theRequest, ResourceSearchUrlEntity.class, requestPartitionId));
248
249                int counterBefore = counter.get();
250                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceTable.class, requestPartitionId));
251                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, PartitionEntity.class, requestPartitionId));
252
253                deletedResourceEntityCount = counter.get() - counterBefore;
254
255                myTxService
256                                .withRequest(theRequest)
257                                .withPropagation(propagation)
258                                .withRequestPartitionId(requestPartitionId)
259                                .execute(() -> {
260                                        counter.addAndGet(doExpungeEverythingQuery("DELETE from " + Search.class.getSimpleName() + " d"));
261                                });
262        }
263
264        @Override
265        public int getExpungeDeletedEntityCount() {
266                return deletedResourceEntityCount;
267        }
268
269        private void purgeAllCaches() {
270                myMemoryCacheService.invalidateAllCaches();
271        }
272
273        protected <T> int expungeEverythingByTypeWithoutPurging(
274                        RequestDetails theRequest, Class<T> theEntityType, RequestPartitionId theRequestPartitionId) {
275                HapiTransactionService.noTransactionAllowed();
276
277                int outcome = 0;
278                while (true) {
279                        StopWatch sw = new StopWatch();
280
281                        int count = myTxService
282                                        .withRequest(theRequest)
283                                        .withPropagation(Propagation.REQUIRES_NEW)
284                                        .withRequestPartitionId(theRequestPartitionId)
285                                        .execute(() -> {
286
287                                                /*
288                                                 * This method uses a nice efficient mechanism where we figure out the PID datatype
289                                                 * and load only the PIDs and delete by PID for all resource types except ResourceTable.
290                                                 * We delete ResourceTable using the entitymanager so that Hibernate Search knows to
291                                                 * delete the corresponding records it manages in ElasticSearch. See
292                                                 * FhirResourceDaoR4SearchWithElasticSearchIT for a test that fails without the
293                                                 * block below.
294                                                 */
295                                                if (ResourceTable.class.equals(theEntityType)) {
296                                                        CriteriaBuilder cb = myEntityManager.getCriteriaBuilder();
297                                                        CriteriaQuery<?> cq = cb.createQuery(theEntityType);
298                                                        cq.from(theEntityType);
299                                                        TypedQuery<?> query = myEntityManager.createQuery(cq);
300                                                        query.setMaxResults(800);
301                                                        List<?> results = query.getResultList();
302                                                        for (Object result : results) {
303                                                                myEntityManager.remove(result);
304                                                        }
305                                                        return results.size();
306                                                }
307
308                                                Metamodel metamodel = myEntityManager.getMetamodel();
309                                                EntityType<T> entity = metamodel.entity(theEntityType);
310                                                Set<SingularAttribute<? super T, ?>> singularAttributes = entity.getSingularAttributes();
311                                                String idProperty = null;
312                                                for (SingularAttribute<? super T, ?> singularAttribute : singularAttributes) {
313                                                        if (singularAttribute.isId()) {
314                                                                idProperty = singularAttribute.getName();
315                                                                break;
316                                                        }
317                                                }
318
319                                                Query nativeQuery = myEntityManager.createQuery(
320                                                                "SELECT " + idProperty + " FROM " + theEntityType.getSimpleName());
321                                                nativeQuery.setMaxResults(800);
322                                                List pids = nativeQuery.getResultList();
323
324                                                if (!pids.isEmpty()) {
325                                                        nativeQuery = myEntityManager.createQuery("DELETE FROM " + theEntityType.getSimpleName()
326                                                                        + " WHERE " + idProperty + " IN (:pids)");
327                                                        nativeQuery.setParameter("pids", pids);
328                                                        nativeQuery.executeUpdate();
329                                                }
330                                                return pids.size();
331                                        });
332
333                        outcome += count;
334                        if (count == 0) {
335                                break;
336                        }
337
338                        ourLog.info("Have deleted {} entities of type {} in {}", outcome, theEntityType.getSimpleName(), sw);
339                }
340                return outcome;
341        }
342
343        @Override
344        public int expungeEverythingByType(Class<?> theEntityType) {
345                int result = expungeEverythingByTypeWithoutPurging(null, theEntityType, RequestPartitionId.allPartitions());
346                purgeAllCaches();
347                return result;
348        }
349
350        @Override
351        public int expungeEverythingMdmLinks() {
352                return expungeEverythingByType(MdmLink.class);
353        }
354
355        private int doExpungeEverythingQuery(String theQuery) {
356                StopWatch sw = new StopWatch();
357                int outcome = myEntityManager.createQuery(theQuery).executeUpdate();
358                ourLog.debug("SqlQuery affected {} rows in {}: {}", outcome, sw, theQuery);
359                return outcome;
360        }
361}