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