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                HookParams hooks = new HookParams()
128                                .add(AtomicInteger.class, counter)
129                                .add(RequestDetails.class, theRequest)
130                                .addIfMatchesType(ServletRequestDetails.class, theRequest);
131                CompositeInterceptorBroadcaster.doCallHooks(
132                                myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESTORAGE_EXPUNGE_EVERYTHING, hooks);
133
134                ourLog.info("BEGINNING GLOBAL $expunge");
135                Propagation propagation = Propagation.REQUIRES_NEW;
136                RequestPartitionId requestPartitionId =
137                                myRequestPartitionHelperSvc.determineReadPartitionForRequestForServerOperation(
138                                                theRequest, ProviderConstants.OPERATION_EXPUNGE);
139
140                deleteAll(theRequest, propagation, requestPartitionId, counter);
141
142                purgeAllCaches();
143
144                ourLog.info("COMPLETED GLOBAL $expunge - Deleted {} rows", counter.get());
145        }
146
147        protected void deleteAll(
148                        @Nullable RequestDetails theRequest,
149                        Propagation propagation,
150                        RequestPartitionId requestPartitionId,
151                        AtomicInteger counter) {
152                myTxService
153                                .withRequest(theRequest)
154                                .withPropagation(propagation)
155                                .withRequestPartitionId(requestPartitionId)
156                                .execute(() -> {
157                                        counter.addAndGet(doExpungeEverythingQuery(
158                                                        "UPDATE " + TermCodeSystem.class.getSimpleName() + " d SET d.myCurrentVersion = null"));
159                                });
160                counter.addAndGet(
161                                expungeEverythingByTypeWithoutPurging(theRequest, Batch2WorkChunkEntity.class, requestPartitionId));
162                counter.addAndGet(
163                                expungeEverythingByTypeWithoutPurging(theRequest, Batch2JobInstanceEntity.class, requestPartitionId));
164                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
165                                theRequest, NpmPackageVersionResourceEntity.class, requestPartitionId));
166                counter.addAndGet(
167                                expungeEverythingByTypeWithoutPurging(theRequest, NpmPackageVersionEntity.class, requestPartitionId));
168                counter.addAndGet(
169                                expungeEverythingByTypeWithoutPurging(theRequest, NpmPackageEntity.class, requestPartitionId));
170                counter.addAndGet(
171                                expungeEverythingByTypeWithoutPurging(theRequest, SearchParamPresentEntity.class, requestPartitionId));
172                counter.addAndGet(
173                                expungeEverythingByTypeWithoutPurging(theRequest, BulkImportJobFileEntity.class, requestPartitionId));
174                counter.addAndGet(
175                                expungeEverythingByTypeWithoutPurging(theRequest, BulkImportJobEntity.class, requestPartitionId));
176                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
177                                theRequest, ResourceIndexedSearchParamDate.class, requestPartitionId));
178                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
179                                theRequest, ResourceIndexedSearchParamNumber.class, requestPartitionId));
180                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
181                                theRequest, ResourceIndexedSearchParamQuantity.class, requestPartitionId));
182                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
183                                theRequest, ResourceIndexedSearchParamQuantityNormalized.class, requestPartitionId));
184                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
185                                theRequest, ResourceIndexedSearchParamString.class, requestPartitionId));
186                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
187                                theRequest, ResourceIndexedSearchParamToken.class, requestPartitionId));
188                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
189                                theRequest, ResourceIndexedSearchParamUri.class, requestPartitionId));
190                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
191                                theRequest, ResourceIndexedSearchParamCoords.class, requestPartitionId));
192                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
193                                theRequest, ResourceIndexedComboStringUnique.class, requestPartitionId));
194                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
195                                theRequest, ResourceIndexedComboTokenNonUnique.class, requestPartitionId));
196                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceLink.class, requestPartitionId));
197                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SearchResult.class, requestPartitionId));
198                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, SearchInclude.class, requestPartitionId));
199                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
200                                theRequest, TermValueSetConceptDesignation.class, requestPartitionId));
201                counter.addAndGet(
202                                expungeEverythingByTypeWithoutPurging(theRequest, TermValueSetConcept.class, requestPartitionId));
203                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermValueSet.class, requestPartitionId));
204                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
205                                theRequest, TermConceptParentChildLink.class, requestPartitionId));
206                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
207                                theRequest, TermConceptMapGroupElementTarget.class, requestPartitionId));
208                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
209                                theRequest, TermConceptMapGroupElement.class, requestPartitionId));
210                counter.addAndGet(
211                                expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMapGroup.class, requestPartitionId));
212                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConceptMap.class, requestPartitionId));
213                counter.addAndGet(
214                                expungeEverythingByTypeWithoutPurging(theRequest, TermConceptProperty.class, requestPartitionId));
215                counter.addAndGet(
216                                expungeEverythingByTypeWithoutPurging(theRequest, TermConceptDesignation.class, requestPartitionId));
217                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermConcept.class, requestPartitionId));
218                myTxService
219                                .withRequest(theRequest)
220                                .withPropagation(propagation)
221                                .withRequestPartitionId(requestPartitionId)
222                                .execute(() -> {
223                                        for (TermCodeSystem next : myEntityManager
224                                                        .createQuery("SELECT c FROM " + TermCodeSystem.class.getName() + " c", TermCodeSystem.class)
225                                                        .getResultList()) {
226                                                next.setCurrentVersion(null);
227                                                myEntityManager.merge(next);
228                                        }
229                                });
230                counter.addAndGet(
231                                expungeEverythingByTypeWithoutPurging(theRequest, TermCodeSystemVersion.class, requestPartitionId));
232                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TermCodeSystem.class, requestPartitionId));
233                counter.addAndGet(
234                                expungeEverythingByTypeWithoutPurging(theRequest, SubscriptionTable.class, requestPartitionId));
235                counter.addAndGet(
236                                expungeEverythingByTypeWithoutPurging(theRequest, ResourceHistoryTag.class, requestPartitionId));
237                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceTag.class, requestPartitionId));
238                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, TagDefinition.class, requestPartitionId));
239                counter.addAndGet(expungeEverythingByTypeWithoutPurging(
240                                theRequest, ResourceHistoryProvenanceEntity.class, requestPartitionId));
241                counter.addAndGet(
242                                expungeEverythingByTypeWithoutPurging(theRequest, ResourceHistoryTable.class, requestPartitionId));
243                counter.addAndGet(
244                                expungeEverythingByTypeWithoutPurging(theRequest, ResourceSearchUrlEntity.class, requestPartitionId));
245
246                int counterBefore = counter.get();
247                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, ResourceTable.class, requestPartitionId));
248                counter.addAndGet(expungeEverythingByTypeWithoutPurging(theRequest, PartitionEntity.class, requestPartitionId));
249
250                deletedResourceEntityCount = counter.get() - counterBefore;
251
252                myTxService
253                                .withRequest(theRequest)
254                                .withPropagation(propagation)
255                                .withRequestPartitionId(requestPartitionId)
256                                .execute(() -> {
257                                        counter.addAndGet(doExpungeEverythingQuery("DELETE from " + Search.class.getSimpleName() + " d"));
258                                });
259        }
260
261        @Override
262        public int getExpungeDeletedEntityCount() {
263                return deletedResourceEntityCount;
264        }
265
266        private void purgeAllCaches() {
267                myMemoryCacheService.invalidateAllCaches();
268        }
269
270        protected <T> int expungeEverythingByTypeWithoutPurging(
271                        RequestDetails theRequest, Class<T> theEntityType, RequestPartitionId theRequestPartitionId) {
272                HapiTransactionService.noTransactionAllowed();
273
274                int outcome = 0;
275                while (true) {
276                        StopWatch sw = new StopWatch();
277
278                        int count = myTxService
279                                        .withRequest(theRequest)
280                                        .withPropagation(Propagation.REQUIRES_NEW)
281                                        .withRequestPartitionId(theRequestPartitionId)
282                                        .execute(() -> {
283
284                                                /*
285                                                 * This method uses a nice efficient mechanism where we figure out the PID datatype
286                                                 * and load only the PIDs and delete by PID for all resource types except ResourceTable.
287                                                 * We delete ResourceTable using the entitymanager so that Hibernate Search knows to
288                                                 * delete the corresponding records it manages in ElasticSearch. See
289                                                 * FhirResourceDaoR4SearchWithElasticSearchIT for a test that fails without the
290                                                 * block below.
291                                                 */
292                                                if (ResourceTable.class.equals(theEntityType)) {
293                                                        CriteriaBuilder cb = myEntityManager.getCriteriaBuilder();
294                                                        CriteriaQuery<?> cq = cb.createQuery(theEntityType);
295                                                        cq.from(theEntityType);
296                                                        TypedQuery<?> query = myEntityManager.createQuery(cq);
297                                                        query.setMaxResults(800);
298                                                        List<?> results = query.getResultList();
299                                                        for (Object result : results) {
300                                                                myEntityManager.remove(result);
301                                                        }
302                                                        return results.size();
303                                                }
304
305                                                Metamodel metamodel = myEntityManager.getMetamodel();
306                                                EntityType<T> entity = metamodel.entity(theEntityType);
307                                                Set<SingularAttribute<? super T, ?>> singularAttributes = entity.getSingularAttributes();
308                                                String idProperty = null;
309                                                for (SingularAttribute<? super T, ?> singularAttribute : singularAttributes) {
310                                                        if (singularAttribute.isId()) {
311                                                                idProperty = singularAttribute.getName();
312                                                                break;
313                                                        }
314                                                }
315
316                                                Query nativeQuery = myEntityManager.createQuery(
317                                                                "SELECT " + idProperty + " FROM " + theEntityType.getSimpleName());
318                                                nativeQuery.setMaxResults(800);
319                                                List pids = nativeQuery.getResultList();
320
321                                                nativeQuery = myEntityManager.createQuery("DELETE FROM " + theEntityType.getSimpleName()
322                                                                + " WHERE " + idProperty + " IN (:pids)");
323                                                nativeQuery.setParameter("pids", pids);
324                                                nativeQuery.executeUpdate();
325                                                return pids.size();
326                                        });
327
328                        outcome += count;
329                        if (count == 0) {
330                                break;
331                        }
332
333                        ourLog.info("Have deleted {} entities of type {} in {}", outcome, theEntityType.getSimpleName(), sw);
334                }
335                return outcome;
336        }
337
338        @Override
339        public int expungeEverythingByType(Class<?> theEntityType) {
340                int result = expungeEverythingByTypeWithoutPurging(null, theEntityType, RequestPartitionId.allPartitions());
341                purgeAllCaches();
342                return result;
343        }
344
345        @Override
346        public int expungeEverythingMdmLinks() {
347                return expungeEverythingByType(MdmLink.class);
348        }
349
350        private int doExpungeEverythingQuery(String theQuery) {
351                StopWatch sw = new StopWatch();
352                int outcome = myEntityManager.createQuery(theQuery).executeUpdate();
353                ourLog.debug("SqlQuery affected {} rows in {}: {}", outcome, sw, theQuery);
354                return outcome;
355        }
356}