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;
021
022import ca.uhn.fhir.batch2.api.IJobCoordinator;
023import ca.uhn.fhir.batch2.api.IJobPartitionProvider;
024import ca.uhn.fhir.batch2.jobs.reindex.ReindexJobParameters;
025import ca.uhn.fhir.batch2.model.JobInstanceStartRequest;
026import ca.uhn.fhir.context.FhirVersionEnum;
027import ca.uhn.fhir.context.RuntimeResourceDefinition;
028import ca.uhn.fhir.i18n.Msg;
029import ca.uhn.fhir.interceptor.api.HookParams;
030import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
031import ca.uhn.fhir.interceptor.api.Pointcut;
032import ca.uhn.fhir.interceptor.executor.InterceptorService;
033import ca.uhn.fhir.interceptor.model.RequestPartitionId;
034import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
035import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
036import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
037import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
038import ca.uhn.fhir.jpa.api.dao.ReindexOutcome;
039import ca.uhn.fhir.jpa.api.dao.ReindexParameters;
040import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
041import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
042import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
043import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
044import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
045import ca.uhn.fhir.jpa.api.model.LazyDaoMethodOutcome;
046import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
047import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode;
048import ca.uhn.fhir.jpa.dao.data.IResourceHistoryProvenanceDao;
049import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
050import ca.uhn.fhir.jpa.delete.DeleteConflictUtil;
051import ca.uhn.fhir.jpa.interceptor.PatientCompartmentEnforcingInterceptor;
052import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
053import ca.uhn.fhir.jpa.model.dao.JpaPid;
054import ca.uhn.fhir.jpa.model.dao.JpaPidFk;
055import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
056import ca.uhn.fhir.jpa.model.entity.BaseTag;
057import ca.uhn.fhir.jpa.model.entity.EntityIndexStatusEnum;
058import ca.uhn.fhir.jpa.model.entity.IdAndPartitionId;
059import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
060import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
061import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity;
062import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
063import ca.uhn.fhir.jpa.model.entity.ResourceTable;
064import ca.uhn.fhir.jpa.model.entity.TagDefinition;
065import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
066import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
067import ca.uhn.fhir.jpa.model.util.JpaConstants;
068import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
069import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
070import ca.uhn.fhir.jpa.search.PersistedJpaBundleProviderFactory;
071import ca.uhn.fhir.jpa.search.ResourceSearchUrlSvc;
072import ca.uhn.fhir.jpa.search.builder.SearchBuilder;
073import ca.uhn.fhir.jpa.search.builder.StorageInterceptorHooksFacade;
074import ca.uhn.fhir.jpa.search.cache.SearchCacheStatusEnum;
075import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
076import ca.uhn.fhir.jpa.searchparam.ResourceSearch;
077import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
078import ca.uhn.fhir.jpa.update.UpdateParameters;
079import ca.uhn.fhir.jpa.util.MemoryCacheService;
080import ca.uhn.fhir.jpa.util.QueryChunker;
081import ca.uhn.fhir.model.api.IQueryParameterType;
082import ca.uhn.fhir.model.api.StorageResponseCodeEnum;
083import ca.uhn.fhir.model.dstu2.resource.BaseResource;
084import ca.uhn.fhir.model.dstu2.resource.ListResource;
085import ca.uhn.fhir.model.primitive.IdDt;
086import ca.uhn.fhir.rest.api.CacheControlDirective;
087import ca.uhn.fhir.rest.api.Constants;
088import ca.uhn.fhir.rest.api.EncodingEnum;
089import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum;
090import ca.uhn.fhir.rest.api.MethodOutcome;
091import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
092import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
093import ca.uhn.fhir.rest.api.ValidationModeEnum;
094import ca.uhn.fhir.rest.api.server.IBundleProvider;
095import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
096import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
097import ca.uhn.fhir.rest.api.server.RequestDetails;
098import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails;
099import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
100import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
101import ca.uhn.fhir.rest.api.server.storage.IDeleteExpungeJobSubmitter;
102import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
103import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
104import ca.uhn.fhir.rest.param.HasParam;
105import ca.uhn.fhir.rest.param.HistorySearchDateRangeParam;
106import ca.uhn.fhir.rest.server.IPagingProvider;
107import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
108import ca.uhn.fhir.rest.server.RestfulServerUtils;
109import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
110import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
111import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
112import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
113import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
114import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
115import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
116import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
117import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
118import ca.uhn.fhir.util.ReflectionUtil;
119import ca.uhn.fhir.util.StopWatch;
120import ca.uhn.fhir.util.UrlUtil;
121import ca.uhn.fhir.validation.FhirValidator;
122import ca.uhn.fhir.validation.IInstanceValidatorModule;
123import ca.uhn.fhir.validation.IValidationContext;
124import ca.uhn.fhir.validation.IValidatorModule;
125import ca.uhn.fhir.validation.ValidationOptions;
126import ca.uhn.fhir.validation.ValidationResult;
127import com.google.common.annotations.VisibleForTesting;
128import jakarta.annotation.Nonnull;
129import jakarta.annotation.Nullable;
130import jakarta.annotation.PostConstruct;
131import jakarta.persistence.LockModeType;
132import jakarta.persistence.NoResultException;
133import jakarta.persistence.TypedQuery;
134import jakarta.servlet.http.HttpServletResponse;
135import org.apache.commons.lang3.Validate;
136import org.apache.commons.lang3.function.TriFunction;
137import org.hl7.fhir.instance.model.api.IBaseCoding;
138import org.hl7.fhir.instance.model.api.IBaseMetaType;
139import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
140import org.hl7.fhir.instance.model.api.IBaseResource;
141import org.hl7.fhir.instance.model.api.IIdType;
142import org.hl7.fhir.instance.model.api.IPrimitiveType;
143import org.hl7.fhir.r4.model.Parameters;
144import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
145import org.springframework.beans.factory.annotation.Autowired;
146import org.springframework.data.domain.PageRequest;
147import org.springframework.data.domain.Pageable;
148import org.springframework.data.domain.Slice;
149import org.springframework.data.domain.Sort;
150import org.springframework.transaction.PlatformTransactionManager;
151import org.springframework.transaction.annotation.Propagation;
152import org.springframework.transaction.annotation.Transactional;
153import org.springframework.transaction.support.TransactionSynchronization;
154import org.springframework.transaction.support.TransactionSynchronizationManager;
155import org.springframework.transaction.support.TransactionTemplate;
156
157import java.io.IOException;
158import java.util.ArrayList;
159import java.util.Collection;
160import java.util.Date;
161import java.util.HashSet;
162import java.util.List;
163import java.util.Map;
164import java.util.Objects;
165import java.util.Optional;
166import java.util.Set;
167import java.util.UUID;
168import java.util.concurrent.Callable;
169import java.util.function.Supplier;
170import java.util.stream.Collectors;
171import java.util.stream.Stream;
172
173import static ca.uhn.fhir.batch2.jobs.reindex.ReindexUtils.JOB_REINDEX;
174import static ca.uhn.fhir.jpa.model.util.JpaConstants.SINGLE_RESULT;
175import static java.util.Objects.isNull;
176import static org.apache.commons.lang3.StringUtils.isBlank;
177import static org.apache.commons.lang3.StringUtils.isNotBlank;
178
179public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends BaseHapiFhirDao<T>
180                implements IFhirResourceDao<T> {
181
182        public static final String BASE_RESOURCE_NAME = "resource";
183        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirResourceDao.class);
184
185        @Autowired
186        protected IInterceptorBroadcaster myInterceptorBroadcaster;
187
188        @Autowired
189        protected PlatformTransactionManager myPlatformTransactionManager;
190
191        @Autowired(required = false)
192        protected IFulltextSearchSvc mySearchDao;
193
194        @Autowired
195        protected HapiTransactionService myTransactionService;
196
197        @Autowired
198        private MatchResourceUrlService<JpaPid> myMatchResourceUrlService;
199
200        @Autowired
201        private SearchBuilderFactory<JpaPid> mySearchBuilderFactory;
202
203        @Autowired
204        private DaoRegistry myDaoRegistry;
205
206        @Autowired
207        private IRequestPartitionHelperSvc myRequestPartitionHelperService;
208
209        @Autowired
210        private IJobPartitionProvider myJobPartitionProvider;
211
212        @Autowired
213        private MatchUrlService myMatchUrlService;
214
215        @Autowired
216        private IDeleteExpungeJobSubmitter myDeleteExpungeJobSubmitter;
217
218        @Autowired
219        private IJobCoordinator myJobCoordinator;
220
221        @Autowired
222        private IResourceHistoryProvenanceDao myResourceHistoryProvenanceDao;
223
224        private IInstanceValidatorModule myInstanceValidator;
225        private String myResourceName;
226        private Class<T> myResourceType;
227
228        @Autowired
229        private PersistedJpaBundleProviderFactory myPersistedJpaBundleProviderFactory;
230
231        @Autowired
232        private MemoryCacheService myMemoryCacheService;
233
234        private TransactionTemplate myTxTemplate;
235
236        @Autowired
237        private ResourceSearchUrlSvc myResourceSearchUrlSvc;
238
239        @Autowired
240        private IFhirSystemDao<?, ?> mySystemDao;
241
242        private StorageInterceptorHooksFacade myStorageInterceptorHooks;
243
244        @Nullable
245        public static <T extends IBaseResource> T invokeStoragePreShowResources(
246                        IInterceptorBroadcaster theInterceptorBroadcaster, RequestDetails theRequest, T retVal) {
247                IInterceptorBroadcaster compositeBroadcaster =
248                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(theInterceptorBroadcaster, theRequest);
249                if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) {
250                        SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(retVal);
251                        HookParams params = new HookParams()
252                                        .add(IPreResourceShowDetails.class, showDetails)
253                                        .add(RequestDetails.class, theRequest)
254                                        .addIfMatchesType(ServletRequestDetails.class, theRequest);
255                        compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params);
256                        //noinspection unchecked
257                        retVal = (T) showDetails.getResource(
258                                        0); // TODO GGG/JA : getting resource 0 is interesting. We apparently allow null values in the list.
259                        // Should we?
260                        return retVal;
261                } else {
262                        return retVal;
263                }
264        }
265
266        public static void invokeStoragePreAccessResources(
267                        IInterceptorBroadcaster theInterceptorBroadcaster,
268                        RequestDetails theRequest,
269                        IIdType theId,
270                        IBaseResource theResource) {
271                IInterceptorBroadcaster compositeBroadcaster =
272                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(theInterceptorBroadcaster, theRequest);
273                if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) {
274                        SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(theResource);
275                        HookParams params = new HookParams()
276                                        .add(IPreResourceAccessDetails.class, accessDetails)
277                                        .add(RequestDetails.class, theRequest)
278                                        .addIfMatchesType(ServletRequestDetails.class, theRequest);
279                        compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params);
280                        if (accessDetails.isDontReturnResourceAtIndex(0)) {
281                                throw new ResourceNotFoundException(Msg.code(1995) + "Resource " + theId + " is not known");
282                        }
283                }
284        }
285
286        @Override
287        protected HapiTransactionService getTransactionService() {
288                return myTransactionService;
289        }
290
291        @VisibleForTesting
292        public void setTransactionService(HapiTransactionService theTransactionService) {
293                myTransactionService = theTransactionService;
294        }
295
296        @Override
297        protected MatchResourceUrlService getMatchResourceUrlService() {
298                return myMatchResourceUrlService;
299        }
300
301        @Override
302        protected IStorageResourceParser getStorageResourceParser() {
303                return myJpaStorageResourceParser;
304        }
305
306        @Override
307        protected IDeleteExpungeJobSubmitter getDeleteExpungeJobSubmitter() {
308                return myDeleteExpungeJobSubmitter;
309        }
310
311        @Override
312        protected IRequestPartitionHelperSvc getRequestPartitionHelperService() {
313                return myRequestPartitionHelperService;
314        }
315
316        @Override
317        protected MatchUrlService getMatchUrlService() {
318                return myMatchUrlService;
319        }
320
321        /**
322         * @deprecated Use {@link #create(T, RequestDetails)} instead
323         */
324        @Override
325        public DaoMethodOutcome create(final T theResource) {
326                return create(theResource, null, true, null, new TransactionDetails());
327        }
328
329        @Override
330        public DaoMethodOutcome create(final T theResource, RequestDetails theRequestDetails) {
331                return create(theResource, null, true, theRequestDetails, new TransactionDetails());
332        }
333
334        /**
335         * @deprecated Use {@link #create(T, String, RequestDetails)} instead
336         */
337        @Override
338        public DaoMethodOutcome create(final T theResource, String theIfNoneExist) {
339                return create(theResource, theIfNoneExist, null);
340        }
341
342        @Override
343        public DaoMethodOutcome create(final T theResource, String theIfNoneExist, RequestDetails theRequestDetails) {
344                return create(theResource, theIfNoneExist, true, theRequestDetails, new TransactionDetails());
345        }
346
347        @Override
348        public DaoMethodOutcome create(
349                        T theResource,
350                        String theIfNoneExist,
351                        boolean thePerformIndexing,
352                        RequestDetails theRequestDetails,
353                        @Nonnull TransactionDetails theTransactionDetails) {
354
355                validateResourceIsNotNullForClientRequest(theResource);
356
357                assignServerAssignedIdFromUserDataIfRequired(theResource);
358                assignServerAssignedUuidIfRequired(theResource);
359
360                RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(
361                                theRequestDetails, theResource, getResourceName());
362
363                return myTransactionService
364                                .withRequest(theRequestDetails)
365                                .withTransactionDetails(theTransactionDetails)
366                                .withRequestPartitionId(requestPartitionId)
367                                .execute(tx -> doCreateForPost(
368                                                theResource,
369                                                theIfNoneExist,
370                                                thePerformIndexing,
371                                                theTransactionDetails,
372                                                theRequestDetails,
373                                                requestPartitionId));
374        }
375
376        @VisibleForTesting
377        public void setRequestPartitionHelperService(IRequestPartitionHelperSvc theRequestPartitionHelperService) {
378                myRequestPartitionHelperService = theRequestPartitionHelperService;
379        }
380
381        /**
382         * Called for FHIR create (POST) operations
383         */
384        protected DaoMethodOutcome doCreateForPost(
385                        T theResource,
386                        String theIfNoneExist,
387                        boolean thePerformIndexing,
388                        TransactionDetails theTransactionDetails,
389                        RequestDetails theRequestDetails,
390                        RequestPartitionId theRequestPartitionId) {
391                validateResourceIsNotNullForClientRequest(theResource);
392
393                if (isNotBlank(theResource.getIdElement().getIdPart()) && !isResourceIdServerAssigned(theResource)) {
394                        if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
395                                String message = getMessageSanitized(
396                                                "failedToCreateWithClientAssignedId",
397                                                theResource.getIdElement().getIdPart());
398                                throw new InvalidRequestException(
399                                                Msg.code(957) + message, createErrorOperationOutcome(message, "processing"));
400                        } else {
401                                // As of DSTU3, ID and version in the body should be ignored for a create/update
402                                theResource.setId("");
403                        }
404                }
405
406                assignServerAssignedUuidIfRequired(theResource);
407
408                return doCreateForPostOrPut(
409                                theRequestDetails,
410                                theResource,
411                                theIfNoneExist,
412                                true,
413                                thePerformIndexing,
414                                theRequestPartitionId,
415                                RestOperationTypeEnum.CREATE,
416                                theTransactionDetails);
417        }
418
419        /**
420         * Tests whether a resource is non-null, and throws a {@link InvalidRequestException}
421         * with a message explaining that the body must not be missing on a create/update
422         * request.
423         */
424        private void validateResourceIsNotNullForClientRequest(T theResource) {
425                if (theResource == null) {
426                        String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "missingBody");
427                        throw new InvalidRequestException(Msg.code(956) + msg);
428                }
429        }
430
431        /**
432         * If the resource being created has a UserData value with key
433         * {@link JpaConstants#RESOURCE_ID_SERVER_ASSIGNED_VALUE}, this value will be
434         * used as the resource's server-assigned FHIR ID. No duplication checking is
435         * performed on the ID, so this feature should only be used if you are certain
436         * that the ID is unique and not previously used for any other resource.
437         * This feature is not exposed to the FHIR REST client API at all, and is
438         * only intended for interceptor code that carefully considers the implications
439         * of its use.
440         *
441         * @since 8.6.0
442         */
443        private void assignServerAssignedIdFromUserDataIfRequired(T theResource) {
444                String serverAssignedIdFromMetadata =
445                                (String) theResource.getUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED_VALUE);
446                if (serverAssignedIdFromMetadata != null) {
447                        theResource.setUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED, Boolean.TRUE);
448                        theResource.setId(serverAssignedIdFromMetadata);
449                        verifyResourceIdIsValid(theResource);
450                }
451        }
452
453        private void assignServerAssignedUuidIfRequired(T theResource) {
454                if (getStorageSettings().getResourceServerIdStrategy() == JpaStorageSettings.IdStrategyEnum.UUID) {
455                        if (!isResourceIdServerAssigned(theResource)) {
456                                theResource.setId(UUID.randomUUID().toString());
457                                theResource.setUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED, Boolean.TRUE);
458                        }
459                }
460        }
461
462        /**
463         * Called both for FHIR create (POST) operations (via {@link #doCreateForPost(IBaseResource, String, boolean, TransactionDetails, RequestDetails, RequestPartitionId)}
464         * as well as for FHIR update (PUT) where we're doing a create-with-client-assigned-ID (via {@link #doUpdate(IBaseResource, String, boolean, boolean, RequestDetails, TransactionDetails, RequestPartitionId)}.
465         */
466        private DaoMethodOutcome doCreateForPostOrPut(
467                        RequestDetails theRequest,
468                        T theResource,
469                        String theMatchUrl,
470                        boolean theProcessMatchUrl,
471                        boolean thePerformIndexing,
472                        RequestPartitionId theRequestPartitionId,
473                        RestOperationTypeEnum theOperationType,
474                        TransactionDetails theTransactionDetails) {
475                StopWatch w = new StopWatch();
476
477                preProcessResourceForStorage(theResource);
478                preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing);
479
480                ResourceTable entity = new ResourceTable();
481                entity.setResourceType(toResourceName(theResource));
482                entity.setResourceTypeId(myResourceTypeCacheSvc.getResourceTypeId(toResourceName(theResource)));
483
484                RequestPartitionId targetWritePartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(
485                                theRequest, theResource, entity.getResourceType());
486                if (!theRequestPartitionId.contains(targetWritePartitionId)) {
487                        throw new InternalErrorException(Msg.code(2636) + "Active partition " + theRequestPartitionId
488                                        + " does not contain target write partition " + targetWritePartitionId);
489                }
490
491                entity.setPartitionId(PartitionablePartitionId.toStoragePartition(targetWritePartitionId, myPartitionSettings));
492                entity.setCreatedByMatchUrl(theMatchUrl);
493                entity.initializeVersion();
494
495                if (isNotBlank(theMatchUrl) && theProcessMatchUrl) {
496                        Set<JpaPid> match = myMatchResourceUrlService.processMatchUrl(
497                                        theMatchUrl, myResourceType, theTransactionDetails, theRequest, theRequestPartitionId);
498                        ourLog.trace("Resolving match URL {} found: {}", theMatchUrl, match);
499                        if (match.size() > 1) {
500                                String msg = getContext()
501                                                .getLocalizer()
502                                                .getMessageSanitized(
503                                                                BaseStorageDao.class,
504                                                                "transactionOperationWithMultipleMatchFailure",
505                                                                "CREATE",
506                                                                myResourceName,
507                                                                theMatchUrl,
508                                                                match.size());
509                                throw new PreconditionFailedException(Msg.code(958) + msg);
510                        } else if (match.size() == 1) {
511
512                                /*
513                                 * Ok, so we've found a single PID that matches the conditional URL.
514                                 * That's good, there are two possibilities below.
515                                 */
516
517                                JpaPid pid = match.iterator().next();
518                                if (theTransactionDetails.getDeletedResourceIds().contains(pid)) {
519
520                                        /*
521                                         * If the resource matching the given match URL has already been
522                                         * deleted within this transaction. This is a really rare case, since
523                                         * it means the client has performed a FHIR transaction with both
524                                         * a delete and a create on the same conditional URL. This is rare
525                                         * but allowed, and means that it's now ok to create a new one resource
526                                         * matching the conditional URL since we'll be deleting any existing
527                                         * index rows on the existing resource as a part of this transaction.
528                                         * We can also un-resolve the previous match URL in the TransactionDetails
529                                         * since we'll resolve it to the new resource ID below
530                                         */
531
532                                        myMatchResourceUrlService.unresolveMatchUrl(theTransactionDetails, getResourceName(), theMatchUrl);
533
534                                } else {
535
536                                        /*
537                                         * This is the normal path where the conditional URL matched exactly
538                                         * one resource, so we won't be creating anything but instead
539                                         * just returning the existing ID. We now have a PID for the matching
540                                         * resource, but we haven't loaded anything else (e.g. the forced ID
541                                         * or the resource body aren't yet loaded from the DB). We're going to
542                                         * return a LazyDaoOutcome with two lazy loaded providers for loading the
543                                         * entity and the forced ID since we can avoid these extra SQL loads
544                                         * unless we know we're actually going to use them. For example, if
545                                         * the client has specified "Prefer: return=minimal" then we won't be
546                                         * needing the load the body.
547                                         */
548
549                                        Supplier<LazyDaoMethodOutcome.EntityAndResource> entitySupplier = () -> myTxTemplate.execute(tx -> {
550                                                ResourceTable foundEntity = myEntityManager.find(ResourceTable.class, pid);
551                                                IBaseResource resource = myJpaStorageResourceParser.toResource(foundEntity, false);
552                                                theResource.setId(resource.getIdElement().getValue());
553                                                return new LazyDaoMethodOutcome.EntityAndResource(foundEntity, resource);
554                                        });
555                                        Supplier<IIdType> idSupplier = () -> myTxTemplate.execute(tx -> {
556                                                IIdType retVal = myIdHelperService.translatePidIdToForcedId(myFhirContext, myResourceName, pid);
557                                                if (!retVal.hasVersionIdPart()) {
558                                                        Long version = pid.getVersion();
559                                                        if (version == null) {
560                                                                version = myMemoryCacheService.getIfPresent(
561                                                                                MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid);
562                                                        }
563                                                        if (version == null) {
564                                                                version = myResourceTableDao.findCurrentVersionByPid(pid);
565                                                                if (version != null) {
566                                                                        myMemoryCacheService.putAfterCommit(
567                                                                                        MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION,
568                                                                                        pid,
569                                                                                        version);
570                                                                }
571                                                        }
572                                                        if (version != null) {
573                                                                retVal = myFhirContext
574                                                                                .getVersion()
575                                                                                .newIdType()
576                                                                                .setParts(
577                                                                                                retVal.getBaseUrl(),
578                                                                                                retVal.getResourceType(),
579                                                                                                retVal.getIdPart(),
580                                                                                                Long.toString(version));
581                                                        }
582                                                }
583                                                return retVal;
584                                        });
585
586                                        DaoMethodOutcome outcome = toMethodOutcomeLazy(theRequest, pid, entitySupplier, idSupplier)
587                                                        .setCreated(false)
588                                                        .setNop(true);
589                                        StorageResponseCodeEnum responseCode =
590                                                        StorageResponseCodeEnum.SUCCESSFUL_CREATE_WITH_CONDITIONAL_MATCH;
591                                        String msg = getContext()
592                                                        .getLocalizer()
593                                                        .getMessageSanitized(
594                                                                        BaseStorageDao.class,
595                                                                        "successfulCreateConditionalWithMatch",
596                                                                        w.getMillisAndRestart(),
597                                                                        UrlUtil.sanitizeUrlPart(theMatchUrl));
598                                        outcome.setOperationOutcome(createInfoOperationOutcome(msg, responseCode));
599                                        return outcome;
600                                }
601                        }
602                }
603
604                boolean isClientAssignedId = storeNonPidResourceId(theResource, entity);
605
606                HookParams hookParams;
607
608                // Notify interceptor for accepting/rejecting client assigned ids
609                if (isClientAssignedId) {
610                        hookParams = new HookParams().add(IBaseResource.class, theResource).add(RequestDetails.class, theRequest);
611                        doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_CLIENT_ASSIGNED_ID, hookParams);
612                }
613
614                // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED
615                hookParams = new HookParams()
616                                .add(IBaseResource.class, theResource)
617                                .add(RequestDetails.class, theRequest)
618                                .addIfMatchesType(ServletRequestDetails.class, theRequest)
619                                .add(RequestPartitionId.class, theRequestPartitionId)
620                                .add(TransactionDetails.class, theTransactionDetails);
621                doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, hookParams);
622
623                if (isClientAssignedId) {
624                        validateResourceIdCreation(theResource, theRequest);
625                }
626
627                if (theMatchUrl != null) {
628                        // Note: We actually create the search URL below by calling enforceMatchUrlResourceUniqueness
629                        // since we can't do that until we know the assigned PID, but we set this flag up here
630                        // because we need to set it before we persist the ResourceTable entity in order to
631                        // avoid triggering an extra DB update
632                        entity.setSearchUrlPresent(true);
633                }
634
635                // Perform actual DB update
636                // this call will also update the metadata
637                ResourceTable updatedEntity = updateEntity(
638                                theRequest,
639                                theResource,
640                                entity,
641                                null,
642                                thePerformIndexing,
643                                false,
644                                theTransactionDetails,
645                                false,
646                                thePerformIndexing);
647
648                // Store the resource forced ID if necessary
649                JpaPid jpaPid = updatedEntity.getPersistentId();
650
651                // Populate the resource with its actual final stored ID from the entity
652                theResource.setId(entity.getIdDt());
653
654                // Pre-cache the resource ID
655                jpaPid.setAssociatedResourceId(entity.getIdType(myFhirContext));
656                String fhirId = entity.getFhirId();
657                assert fhirId != null;
658                myIdHelperService.addResolvedPidToFhirIdAfterCommit(
659                                jpaPid, theRequestPartitionId, getResourceName(), fhirId, null);
660                theTransactionDetails.addResolvedResourceId(jpaPid.getAssociatedResourceId(), jpaPid);
661                theTransactionDetails.addResolvedResource(jpaPid.getAssociatedResourceId(), theResource);
662
663                // Pre-cache the match URL, and create an entry in the HFJ_RES_SEARCH_URL table to
664                // protect against concurrent writes to the same conditional URL
665                if (theMatchUrl != null) {
666                        myResourceSearchUrlSvc.enforceMatchUrlResourceUniqueness(getResourceName(), theMatchUrl, updatedEntity);
667                        myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, getResourceName(), theMatchUrl, jpaPid);
668                }
669
670                // Update the version/last updated in the resource so that interceptors get
671                // the correct version
672                // TODO - the above updateEntity calls updateResourceMetadata
673                //              Maybe we don't need this call here?
674                myJpaStorageResourceParser.updateResourceMetadata(entity, theResource);
675
676                // Populate the PID in the resource so it is available to hooks
677                addPidToResource(entity, theResource);
678
679                // Notify JPA interceptors
680                if (!updatedEntity.isUnchangedInCurrentOperation()) {
681                        hookParams = new HookParams()
682                                        .add(IBaseResource.class, theResource)
683                                        .add(RequestDetails.class, theRequest)
684                                        .addIfMatchesType(ServletRequestDetails.class, theRequest)
685                                        .add(TransactionDetails.class, theTransactionDetails)
686                                        .add(
687                                                        InterceptorInvocationTimingEnum.class,
688                                                        theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
689                        doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, hookParams);
690                }
691
692                DaoMethodOutcome outcome = toMethodOutcome(theRequest, entity, theResource, theMatchUrl, theOperationType)
693                                .setCreated(true);
694                outcome.setResponseStatusCode(Constants.STATUS_HTTP_201_CREATED);
695
696                if (!thePerformIndexing) {
697                        outcome.setId(theResource.getIdElement());
698                }
699
700                populateOperationOutcomeForUpdate(w, outcome, theMatchUrl, theOperationType, theTransactionDetails);
701
702                return outcome;
703        }
704
705        /**
706         * Check for an id on the resource and if so,
707         * store it in ResourceTable.
708         *
709         * The fhirId property is either set here with the resource id
710         * OR by hibernate once the PK is generated for a server-assigned id.
711         *
712         * Used for both client-assigned id and for server-assigned UUIDs.
713         *
714         * @return true if this is a client-assigned id
715         *
716         * @see ca.uhn.fhir.jpa.model.entity.ResourceTable.FhirIdGenerator
717         */
718        private boolean storeNonPidResourceId(T theResource, ResourceTable entity) {
719                String resourceIdBeforeStorage = theResource.getIdElement().getIdPart();
720                boolean resourceHadIdBeforeStorage = isNotBlank(resourceIdBeforeStorage);
721                boolean resourceIdWasServerAssigned =
722                                theResource.getUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED) == Boolean.TRUE;
723
724                // We distinguish actual client-assigned ids from UUIDs which the server assigned.
725                boolean isClientAssigned = resourceHadIdBeforeStorage && !resourceIdWasServerAssigned;
726
727                // But both need to be set on the entity fhirId field.
728                if (resourceHadIdBeforeStorage) {
729                        entity.setFhirId(resourceIdBeforeStorage);
730                }
731
732                return isClientAssigned;
733        }
734
735        void validateResourceIdCreation(T theResource, RequestDetails theRequest) {
736                JpaStorageSettings.ClientIdStrategyEnum strategy = getStorageSettings().getResourceClientIdStrategy();
737
738                if (strategy == JpaStorageSettings.ClientIdStrategyEnum.NOT_ALLOWED) {
739                        if (!isSystemRequest(theRequest)) {
740                                throw new ResourceNotFoundException(Msg.code(959)
741                                                + getMessageSanitized(
742                                                                "failedToCreateWithClientAssignedIdNotAllowed",
743                                                                theResource.getIdElement().getIdPart()));
744                        }
745                }
746
747                if (strategy == JpaStorageSettings.ClientIdStrategyEnum.ALPHANUMERIC) {
748                        if (theResource.getIdElement().isIdPartValidLong()) {
749                                throw new InvalidRequestException(Msg.code(960)
750                                                + getMessageSanitized(
751                                                                "failedToCreateWithClientAssignedNumericId",
752                                                                theResource.getIdElement().getIdPart()));
753                        }
754                }
755        }
756
757        protected String getMessageSanitized(String theKey, String theIdPart) {
758                return getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, theKey, theIdPart);
759        }
760
761        private boolean isSystemRequest(RequestDetails theRequest) {
762                return theRequest instanceof SystemRequestDetails;
763        }
764
765        private IInstanceValidatorModule getInstanceValidator() {
766                return myInstanceValidator;
767        }
768
769        /**
770         * @deprecated Use {@link #delete(IIdType, RequestDetails)} instead
771         */
772        @Override
773        public DaoMethodOutcome delete(IIdType theId) {
774                return delete(theId, null);
775        }
776
777        @Override
778        public DaoMethodOutcome delete(IIdType theId, RequestDetails theRequestDetails) {
779                TransactionDetails transactionDetails = new TransactionDetails();
780
781                validateIdPresentForDelete(theId);
782                validateDeleteEnabled();
783
784                return myTransactionService.execute(theRequestDetails, transactionDetails, tx -> {
785                        DeleteConflictList deleteConflicts = new DeleteConflictList();
786                        if (isNotBlank(theId.getValue())) {
787                                deleteConflicts.setResourceIdMarkedForDeletion(theId);
788                        }
789
790                        StopWatch w = new StopWatch();
791
792                        DaoMethodOutcome retVal = delete(theId, deleteConflicts, theRequestDetails, transactionDetails);
793
794                        DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts);
795
796                        ourLog.debug("Processed delete on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
797                        return retVal;
798                });
799        }
800
801        @Override
802        public DaoMethodOutcome delete(
803                        IIdType theId,
804                        DeleteConflictList theDeleteConflicts,
805                        RequestDetails theRequestDetails,
806                        @Nonnull TransactionDetails theTransactionDetails) {
807                validateIdPresentForDelete(theId);
808                validateDeleteEnabled();
809
810                RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead(
811                                theRequestDetails, getResourceName(), theId);
812
813                final ResourceTable entity;
814                try {
815                        entity = readEntityLatestVersion(theRequestDetails, theId, requestPartitionId, theTransactionDetails);
816                } catch (ResourceNotFoundException ex) {
817                        // we don't want to throw 404s.
818                        // if not found, return an outcome anyway.
819                        // Because no object actually existed, we'll
820                        // just set the id and nothing else
821                        return createMethodOutcomeForResourceId(
822                                        theId.getValue(),
823                                        MESSAGE_KEY_DELETE_RESOURCE_NOT_EXISTING,
824                                        StorageResponseCodeEnum.SUCCESSFUL_DELETE_NOT_FOUND);
825                }
826
827                if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) {
828                        throw new ResourceVersionConflictException(
829                                        Msg.code(961) + "Trying to delete " + theId + " but this is not the current version");
830                }
831
832                JpaPid persistentId = entity.getId();
833                theTransactionDetails.addDeletedResourceId(persistentId);
834
835                // Don't delete again if it's already deleted
836                if (isDeleted(entity)) {
837                        DaoMethodOutcome outcome = createMethodOutcomeForResourceId(
838                                        entity.getIdDt().getValue(),
839                                        MESSAGE_KEY_DELETE_RESOURCE_ALREADY_DELETED,
840                                        StorageResponseCodeEnum.SUCCESSFUL_DELETE_ALREADY_DELETED);
841
842                        // used to exist, so we'll set the persistent id
843                        outcome.setPersistentId(persistentId);
844                        outcome.setEntity(entity);
845
846                        return outcome;
847                }
848
849                StopWatch w = new StopWatch();
850
851                T resourceToDelete =
852                                myJpaStorageResourceParser.toResource(theRequestDetails, myResourceType, entity, null, false);
853                theDeleteConflicts.setResourceIdMarkedForDeletion(theId);
854
855                // Notify IServerOperationInterceptors about pre-action call
856                HookParams hook = new HookParams()
857                                .add(IBaseResource.class, resourceToDelete)
858                                .add(RequestDetails.class, theRequestDetails)
859                                .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
860                                .add(TransactionDetails.class, theTransactionDetails);
861                doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hook);
862
863                myDeleteConflictService.validateOkToDelete(
864                                theDeleteConflicts, entity, false, theRequestDetails, theTransactionDetails);
865
866                preDelete(resourceToDelete, entity, theRequestDetails);
867
868                ResourceTable savedEntity = updateEntityForDelete(theRequestDetails, theTransactionDetails, entity);
869                resourceToDelete.setId(entity.getIdDt());
870
871                // Notify JPA interceptors
872                HookParams hookParams = new HookParams()
873                                .add(IBaseResource.class, resourceToDelete)
874                                .add(RequestDetails.class, theRequestDetails)
875                                .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
876                                .add(TransactionDetails.class, theTransactionDetails)
877                                .add(
878                                                InterceptorInvocationTimingEnum.class,
879                                                theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED));
880
881                doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
882
883                DaoMethodOutcome outcome = toMethodOutcome(
884                                                theRequestDetails, savedEntity, resourceToDelete, null, RestOperationTypeEnum.DELETE)
885                                .setCreated(true);
886
887                String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "successfulDeletes", 1);
888                msg += " "
889                                + getContext()
890                                                .getLocalizer()
891                                                .getMessageSanitized(BaseStorageDao.class, "successfulTimingSuffix", w.getMillis());
892                outcome.setOperationOutcome(createInfoOperationOutcome(msg, StorageResponseCodeEnum.SUCCESSFUL_DELETE));
893
894                myIdHelperService.addResolvedPidToFhirIdAfterCommit(
895                                entity.getPersistentId(),
896                                requestPartitionId,
897                                entity.getResourceType(),
898                                entity.getFhirId(),
899                                entity.getDeleted());
900
901                return outcome;
902        }
903
904        @Override
905        public DeleteMethodOutcome deleteByUrl(String theUrl, RequestDetails theRequest) {
906                validateDeleteEnabled();
907
908                TransactionDetails transactionDetails = new TransactionDetails();
909                ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(theUrl);
910
911                if (resourceSearch.isDeleteExpunge()) {
912                        return deleteExpunge(theUrl, theRequest);
913                }
914
915                return myTransactionService
916                                .withRequest(theRequest)
917                                .withTransactionDetails(transactionDetails)
918                                .execute(tx -> {
919                                        DeleteConflictList deleteConflicts = new DeleteConflictList();
920                                        DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequest, transactionDetails);
921                                        DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts);
922                                        return outcome;
923                                });
924        }
925
926        /**
927         * This method gets called by {@link #deleteByUrl(String, RequestDetails)} as well as by
928         * transaction processors
929         */
930        @Override
931        public DeleteMethodOutcome deleteByUrl(
932                        String theUrl,
933                        DeleteConflictList deleteConflicts,
934                        RequestDetails theRequestDetails,
935                        @Nonnull TransactionDetails theTransactionDetails) {
936                validateDeleteEnabled();
937
938                return myTransactionService
939                                .withRequest(theRequestDetails)
940                                .withTransactionDetails(theTransactionDetails)
941                                .execute(tx -> doDeleteByUrl(theUrl, deleteConflicts, theTransactionDetails, theRequestDetails));
942        }
943
944        @Nonnull
945        private DeleteMethodOutcome doDeleteByUrl(
946                        String theUrl,
947                        DeleteConflictList deleteConflicts,
948                        TransactionDetails theTransactionDetails,
949                        RequestDetails theRequestDetails) {
950                ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(theUrl);
951                SearchParameterMap paramMap = resourceSearch.getSearchParameterMap();
952                paramMap.setLoadSynchronous(true);
953
954                Set<JpaPid> resourceIds = myMatchResourceUrlService.search(paramMap, myResourceType, theRequestDetails, null);
955
956                if (resourceIds.size() > 1) {
957                        if (!getStorageSettings().isAllowMultipleDelete()) {
958                                throw new PreconditionFailedException(Msg.code(962)
959                                                + getContext()
960                                                                .getLocalizer()
961                                                                .getMessageSanitized(
962                                                                                BaseStorageDao.class,
963                                                                                "transactionOperationWithMultipleMatchFailure",
964                                                                                "DELETE",
965                                                                                myResourceName,
966                                                                                theUrl,
967                                                                                resourceIds.size()));
968                        }
969                        // TODO: LD: There is a still a bug on slow deletes:  https://github.com/hapifhir/hapi-fhir/issues/5675
970                        final long threshold = getStorageSettings().getRestDeleteByUrlResourceIdThreshold();
971                        if (resourceIds.size() > threshold) {
972                                throw new PreconditionFailedException(Msg.code(2496)
973                                                + getContext()
974                                                                .getLocalizer()
975                                                                .getMessageSanitized(
976                                                                                BaseStorageDao.class,
977                                                                                "deleteByUrlThresholdExceeded",
978                                                                                theUrl,
979                                                                                resourceIds.size(),
980                                                                                threshold));
981                        }
982                }
983
984                return deletePidList(theUrl, resourceIds, deleteConflicts, theRequestDetails, theTransactionDetails);
985        }
986
987        @Override
988        public <P extends IResourcePersistentId> void expunge(Collection<P> theResourceIds, RequestDetails theRequest) {
989                ExpungeOptions options = new ExpungeOptions();
990                options.setExpungeDeletedResources(true);
991                for (P pid : theResourceIds) {
992                        if (pid instanceof JpaPid) {
993                                ResourceTable entity = myEntityManager.find(ResourceTable.class, pid.getId());
994
995                                forceExpungeInExistingTransaction(entity.getIdDt().toVersionless(), options, theRequest);
996                        } else {
997                                ourLog.warn("Unable to process expunge on resource {}", pid);
998                                return;
999                        }
1000                }
1001        }
1002
1003        @Nonnull
1004        @Override
1005        public <P extends IResourcePersistentId> DeleteMethodOutcome deletePidList(
1006                        String theUrl,
1007                        Collection<P> theResourceIds,
1008                        DeleteConflictList theDeleteConflicts,
1009                        RequestDetails theRequestDetails,
1010                        TransactionDetails theTransactionDetails) {
1011                StopWatch w = new StopWatch();
1012                TransactionDetails transactionDetails =
1013                                theTransactionDetails != null ? theTransactionDetails : new TransactionDetails();
1014                List<ResourceTable> deletedResources = new ArrayList<>();
1015
1016                List<IResourcePersistentId<?>> resolvedIds =
1017                                theResourceIds.stream().map(t -> (IResourcePersistentId<?>) t).collect(Collectors.toList());
1018                mySystemDao.preFetchResources(resolvedIds, false);
1019
1020                for (P pid : theResourceIds) {
1021                        JpaPid jpaPid = (JpaPid) pid;
1022
1023                        // This shouldn't actually need to hit the DB because we pre-fetch above
1024                        ResourceTable entity = myEntityManager.find(ResourceTable.class, jpaPid);
1025                        deletedResources.add(entity);
1026
1027                        T resourceToDelete =
1028                                        myJpaStorageResourceParser.toResource(theRequestDetails, myResourceType, entity, null, false);
1029
1030                        transactionDetails.addDeletedResourceId(pid);
1031
1032                        // Notify IServerOperationInterceptors about pre-action call
1033                        HookParams hooks = new HookParams()
1034                                        .add(IBaseResource.class, resourceToDelete)
1035                                        .add(RequestDetails.class, theRequestDetails)
1036                                        .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
1037                                        .add(TransactionDetails.class, transactionDetails);
1038                        doCallHooks(transactionDetails, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks);
1039
1040                        myDeleteConflictService.validateOkToDelete(
1041                                        theDeleteConflicts, entity, false, theRequestDetails, transactionDetails);
1042
1043                        // Perform delete
1044
1045                        preDelete(resourceToDelete, entity, theRequestDetails);
1046
1047                        updateEntityForDelete(theRequestDetails, transactionDetails, entity);
1048                        resourceToDelete.setId(entity.getIdDt());
1049
1050                        // Notify JPA interceptors
1051                        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
1052                                @Override
1053                                public void beforeCommit(boolean readOnly) {
1054                                        HookParams hookParams = new HookParams()
1055                                                        .add(IBaseResource.class, resourceToDelete)
1056                                                        .add(RequestDetails.class, theRequestDetails)
1057                                                        .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
1058                                                        .add(TransactionDetails.class, transactionDetails)
1059                                                        .add(
1060                                                                        InterceptorInvocationTimingEnum.class,
1061                                                                        transactionDetails.getInvocationTiming(
1062                                                                                        Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED));
1063                                        doCallHooks(
1064                                                        transactionDetails,
1065                                                        theRequestDetails,
1066                                                        Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED,
1067                                                        hookParams);
1068                                }
1069                        });
1070                }
1071
1072                IBaseOperationOutcome oo;
1073                if (deletedResources.isEmpty()) {
1074                        String msg = getContext()
1075                                        .getLocalizer()
1076                                        .getMessageSanitized(BaseStorageDao.class, "unableToDeleteNotFound", theUrl);
1077                        oo = createWarnOperationOutcome(msg, "not-found", StorageResponseCodeEnum.SUCCESSFUL_DELETE_NOT_FOUND);
1078                } else {
1079                        String msg = getContext()
1080                                        .getLocalizer()
1081                                        .getMessageSanitized(BaseStorageDao.class, "successfulDeletes", deletedResources.size());
1082                        msg += " "
1083                                        + getContext()
1084                                                        .getLocalizer()
1085                                                        .getMessageSanitized(BaseStorageDao.class, "successfulTimingSuffix", w.getMillis());
1086                        oo = createInfoOperationOutcome(msg, StorageResponseCodeEnum.SUCCESSFUL_DELETE);
1087                }
1088
1089                ourLog.debug(
1090                                "Processed delete on {} (matched {} resource(s)) in {}ms",
1091                                theUrl,
1092                                deletedResources.size(),
1093                                w.getMillis());
1094
1095                DeleteMethodOutcome retVal = new DeleteMethodOutcome();
1096                retVal.setDeletedEntities(deletedResources);
1097                retVal.setOperationOutcome(oo);
1098                return retVal;
1099        }
1100
1101        protected ResourceTable updateEntityForDelete(
1102                        RequestDetails theRequest, TransactionDetails theTransactionDetails, ResourceTable theEntity) {
1103                myResourceSearchUrlSvc.deleteByResId(theEntity.getPersistentId());
1104                Date updateTime = new Date();
1105                return updateEntity(theRequest, null, theEntity, updateTime, true, true, theTransactionDetails, false, true);
1106        }
1107
1108        private void validateDeleteEnabled() {
1109                if (!getStorageSettings().isDeleteEnabled()) {
1110                        String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "deleteBlockedBecauseDisabled");
1111                        throw new PreconditionFailedException(Msg.code(966) + msg);
1112                }
1113        }
1114
1115        private void validateIdPresentForDelete(IIdType theId) {
1116                if (theId == null || !theId.hasIdPart()) {
1117                        throw new InvalidRequestException(Msg.code(967) + "Can not perform delete, no ID provided");
1118                }
1119        }
1120
1121        private <MT extends IBaseMetaType> void doMetaAdd(
1122                        MT theMetaAdd,
1123                        BaseHasResource<?> theEntity,
1124                        RequestDetails theRequestDetails,
1125                        TransactionDetails theTransactionDetails) {
1126                IBaseResource oldVersion = myJpaStorageResourceParser.toResource(theEntity, false);
1127
1128                List<TagDefinition> tags = toTagList(theMetaAdd);
1129                for (TagDefinition nextDef : tags) {
1130
1131                        boolean entityHasTag = false;
1132                        for (BaseTag next : theEntity.getTags()) {
1133                                if (Objects.equals(next.getTag().getTagType(), nextDef.getTagType())
1134                                                && Objects.equals(next.getTag().getSystem(), nextDef.getSystem())
1135                                                && Objects.equals(next.getTag().getCode(), nextDef.getCode())
1136                                                && Objects.equals(next.getTag().getVersion(), nextDef.getVersion())
1137                                                && Objects.equals(next.getTag().getUserSelected(), nextDef.getUserSelected())) {
1138                                        entityHasTag = true;
1139                                        break;
1140                                }
1141                        }
1142
1143                        if (!entityHasTag) {
1144                                theEntity.setHasTags(true);
1145
1146                                TagDefinition def = cacheTagDefinitionDao.getTagOrNull(
1147                                                theTransactionDetails,
1148                                                nextDef.getTagType(),
1149                                                nextDef.getSystem(),
1150                                                nextDef.getCode(),
1151                                                nextDef.getDisplay(),
1152                                                nextDef.getVersion(),
1153                                                nextDef.getUserSelected());
1154                                if (def != null) {
1155                                        BaseTag newEntity = theEntity.addTag(def);
1156                                        if (newEntity.getTagId() == null) {
1157                                                myEntityManager.persist(newEntity);
1158                                        }
1159                                }
1160                        }
1161                }
1162
1163                validateMetaCount(theEntity.getTags().size());
1164
1165                myEntityManager.merge(theEntity);
1166
1167                // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED
1168                IBaseResource newVersion = myJpaStorageResourceParser.toResource(theEntity, false);
1169                HookParams preStorageParams = new HookParams()
1170                                .add(IBaseResource.class, oldVersion)
1171                                .add(IBaseResource.class, newVersion)
1172                                .add(RequestDetails.class, theRequestDetails)
1173                                .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
1174                                .add(TransactionDetails.class, theTransactionDetails);
1175                myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams);
1176
1177                // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED
1178                HookParams preCommitParams = new HookParams()
1179                                .add(IBaseResource.class, oldVersion)
1180                                .add(IBaseResource.class, newVersion)
1181                                .add(RequestDetails.class, theRequestDetails)
1182                                .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
1183                                .add(TransactionDetails.class, theTransactionDetails)
1184                                .add(
1185                                                InterceptorInvocationTimingEnum.class,
1186                                                theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED));
1187                myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams);
1188        }
1189
1190        private <MT extends IBaseMetaType> void doMetaDelete(
1191                        MT theMetaDel,
1192                        BaseHasResource theEntity,
1193                        RequestDetails theRequestDetails,
1194                        TransactionDetails theTransactionDetails) {
1195
1196                // todo mb update hibernate search index if we are storing resources - it assumes inline tags.
1197                IBaseResource oldVersion = myJpaStorageResourceParser.toResource(theEntity, false);
1198
1199                List<TagDefinition> tags = toTagList(theMetaDel);
1200
1201                for (TagDefinition nextDef : tags) {
1202                        for (BaseTag next : new ArrayList<BaseTag>(theEntity.getTags())) {
1203                                if (Objects.equals(next.getTag().getTagType(), nextDef.getTagType())
1204                                                && Objects.equals(next.getTag().getSystem(), nextDef.getSystem())
1205                                                && Objects.equals(next.getTag().getCode(), nextDef.getCode())) {
1206                                        myEntityManager.remove(next);
1207                                        theEntity.getTags().remove(next);
1208                                }
1209                        }
1210                }
1211
1212                if (theEntity.getTags().isEmpty()) {
1213                        theEntity.setHasTags(false);
1214                }
1215
1216                theEntity = myEntityManager.merge(theEntity);
1217
1218                // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED
1219                IBaseResource newVersion = myJpaStorageResourceParser.toResource(theEntity, false);
1220                HookParams preStorageParams = new HookParams()
1221                                .add(IBaseResource.class, oldVersion)
1222                                .add(IBaseResource.class, newVersion)
1223                                .add(RequestDetails.class, theRequestDetails)
1224                                .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
1225                                .add(TransactionDetails.class, theTransactionDetails);
1226                myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams);
1227
1228                HookParams preCommitParams = new HookParams()
1229                                .add(IBaseResource.class, oldVersion)
1230                                .add(IBaseResource.class, newVersion)
1231                                .add(RequestDetails.class, theRequestDetails)
1232                                .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
1233                                .add(TransactionDetails.class, theTransactionDetails)
1234                                .add(
1235                                                InterceptorInvocationTimingEnum.class,
1236                                                theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED));
1237
1238                myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams);
1239        }
1240
1241        @Override
1242        public ExpungeOutcome expunge(IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest) {
1243                HapiTransactionService.noTransactionAllowed();
1244                validateExpungeEnabled();
1245                return forceExpungeInExistingTransaction(theId, theExpungeOptions, theRequest);
1246        }
1247
1248        @Override
1249        public ExpungeOutcome expunge(ExpungeOptions theExpungeOptions, RequestDetails theRequestDetails) {
1250                HapiTransactionService.noTransactionAllowed();
1251                ourLog.info("Beginning TYPE[{}] expunge operation", getResourceName());
1252                validateExpungeEnabled();
1253                return myExpungeService.expunge(getResourceName(), null, theExpungeOptions, theRequestDetails);
1254        }
1255
1256        private void validateExpungeEnabled() {
1257                if (!getStorageSettings().isExpungeEnabled()) {
1258                        throw new MethodNotAllowedException(Msg.code(968) + "$expunge is not enabled on this server");
1259                }
1260        }
1261
1262        @Override
1263        public Stream<IResourcePersistentId> fetchAllVersionsOfResources(
1264                        RequestDetails theRequestDetails, Collection<IResourcePersistentId> theIds) {
1265                HapiTransactionService.requireTransaction();
1266
1267                List<JpaPidFk> ids = theIds.stream().map(t -> ((JpaPid) t).toFk()).toList();
1268
1269                return myResourceHistoryTableDao
1270                                .findVersionPidsForResources(Pageable.unpaged(), ids)
1271                                .map(t -> (IResourcePersistentId) t);
1272        }
1273
1274        @Override
1275        public ExpungeOutcome forceExpungeInExistingTransaction(
1276                        IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest) {
1277                TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
1278
1279                BaseHasResource<?> entity = txTemplate.execute(t -> readEntity(theId, theRequest));
1280                Validate.notNull(entity, "Resource with ID %s not found in database", theId);
1281
1282                if (theId.hasVersionIdPart()) {
1283                        BaseHasResource<?> currentVersion;
1284                        currentVersion = txTemplate.execute(t -> readEntity(theId.toVersionless(), theRequest));
1285                        Validate.notNull(
1286                                        currentVersion,
1287                                        "Current version of resource with ID %s not found in database",
1288                                        theId.toVersionless());
1289
1290                        if (entity.getVersion() == currentVersion.getVersion()) {
1291                                throw new PreconditionFailedException(
1292                                                Msg.code(969) + "Can not perform version-specific expunge of resource "
1293                                                                + theId.toUnqualified().getValue() + " as this is the current version");
1294                        }
1295
1296                        return myExpungeService.expunge(getResourceName(), entity.getResourceId(), theExpungeOptions, theRequest);
1297                }
1298
1299                // Downstream code expects the version to be null in the JpaPid if we're not
1300                // doing a version specific expunge
1301                JpaPid resourceId = entity.getResourceId();
1302                resourceId = JpaPid.fromId(resourceId.getId(), resourceId.getPartitionId());
1303
1304                return myExpungeService.expunge(getResourceName(), resourceId, theExpungeOptions, theRequest);
1305        }
1306
1307        @Override
1308        @Nonnull
1309        public String getResourceName() {
1310                return myResourceName;
1311        }
1312
1313        @Override
1314        public Class<T> getResourceType() {
1315                return myResourceType;
1316        }
1317
1318        @SuppressWarnings("unchecked")
1319        public void setResourceType(Class<? extends IBaseResource> theTableType) {
1320                myResourceType = (Class<T>) theTableType;
1321        }
1322
1323        @Override
1324        public IBundleProvider history(Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequestDetails) {
1325                StopWatch w = new StopWatch();
1326                RequestPartitionId requestPartitionId =
1327                                myRequestPartitionHelperService.determineReadPartitionForRequestForHistory(
1328                                                theRequestDetails, myResourceName, null);
1329                IBundleProvider retVal = myTransactionService
1330                                .withRequest(theRequestDetails)
1331                                .withRequestPartitionId(requestPartitionId)
1332                                .execute(() -> myPersistedJpaBundleProviderFactory.history(
1333                                                theRequestDetails, myResourceName, null, theSince, theUntil, theOffset, requestPartitionId));
1334
1335                ourLog.debug("Processed history on {} in {}ms", myResourceName, w.getMillisAndRestart());
1336                return retVal;
1337        }
1338
1339        /**
1340         * @deprecated Use {@link #history(IIdType, HistorySearchDateRangeParam, RequestDetails)} instead
1341         */
1342        @Override
1343        public IBundleProvider history(
1344                        final IIdType theId, final Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequest) {
1345                StopWatch w = new StopWatch();
1346
1347                RequestPartitionId requestPartitionId =
1348                                myRequestPartitionHelperService.determineReadPartitionForRequestForHistory(
1349                                                theRequest, myResourceName, theId);
1350                IBundleProvider retVal = myTransactionService
1351                                .withRequest(theRequest)
1352                                .withRequestPartitionId(requestPartitionId)
1353                                .execute(() -> {
1354                                        IIdType id = theId.withResourceType(myResourceName).toUnqualifiedVersionless();
1355                                        BaseHasResource entity = readEntity(id, true, theRequest, requestPartitionId);
1356
1357                                        return myPersistedJpaBundleProviderFactory.history(
1358                                                        theRequest,
1359                                                        myResourceName,
1360                                                        entity.getResourceId(),
1361                                                        theSince,
1362                                                        theUntil,
1363                                                        theOffset,
1364                                                        requestPartitionId);
1365                                });
1366
1367                ourLog.debug("Processed history on {} in {}ms", theId, w.getMillisAndRestart());
1368                return retVal;
1369        }
1370
1371        @Override
1372        public IBundleProvider history(
1373                        final IIdType theId,
1374                        final HistorySearchDateRangeParam theHistorySearchDateRangeParam,
1375                        RequestDetails theRequest) {
1376                StopWatch w = new StopWatch();
1377                RequestPartitionId requestPartitionId =
1378                                myRequestPartitionHelperService.determineReadPartitionForRequestForHistory(
1379                                                theRequest, myResourceName, theId);
1380                IBundleProvider retVal = myTransactionService
1381                                .withRequest(theRequest)
1382                                .withRequestPartitionId(requestPartitionId)
1383                                .execute(() -> {
1384                                        IIdType id = theId.withResourceType(myResourceName).toUnqualifiedVersionless();
1385                                        BaseHasResource entity = readEntity(id, true, theRequest, requestPartitionId);
1386
1387                                        return myPersistedJpaBundleProviderFactory.history(
1388                                                        theRequest,
1389                                                        myResourceName,
1390                                                        entity.getResourceId(),
1391                                                        theHistorySearchDateRangeParam.getLowerBoundAsInstant(),
1392                                                        theHistorySearchDateRangeParam.getUpperBoundAsInstant(),
1393                                                        theHistorySearchDateRangeParam.getOffset(),
1394                                                        theHistorySearchDateRangeParam.getHistorySearchType(),
1395                                                        requestPartitionId);
1396                                });
1397
1398                ourLog.debug("Processed history on {} in {}ms", theId, w.getMillisAndRestart());
1399                return retVal;
1400        }
1401
1402        protected boolean isPagingProviderDatabaseBacked(RequestDetails theRequestDetails) {
1403                if (theRequestDetails == null || theRequestDetails.getServer() == null) {
1404                        return false;
1405                }
1406                IRestfulServerDefaults server = theRequestDetails.getServer();
1407                IPagingProvider pagingProvider = server.getPagingProvider();
1408                return pagingProvider != null;
1409        }
1410
1411        protected void requestReindexForRelatedResources(
1412                        Boolean theCurrentlyReindexing, List<String> theBase, RequestDetails theRequestDetails) {
1413                // Avoid endless loops
1414                if (Boolean.TRUE.equals(theCurrentlyReindexing) || shouldSkipReindex(theRequestDetails)) {
1415                        return;
1416                }
1417
1418                if (getStorageSettings().isMarkResourcesForReindexingUponSearchParameterChange()) {
1419
1420                        ReindexJobParameters params = new ReindexJobParameters();
1421
1422                        List<String> urls = List.of();
1423                        if (!isCommonSearchParam(theBase)) {
1424                                urls = theBase.stream().map(t -> t + "?").collect(Collectors.toList());
1425                        }
1426
1427                        myJobPartitionProvider.getPartitionedUrls(theRequestDetails, urls).forEach(params::addPartitionedUrl);
1428
1429                        JobInstanceStartRequest request = new JobInstanceStartRequest();
1430                        request.setJobDefinitionId(JOB_REINDEX);
1431                        request.setParameters(params);
1432                        myJobCoordinator.startInstance(theRequestDetails, request);
1433
1434                        ourLog.debug("Started reindex job with parameters {}", params);
1435                }
1436
1437                mySearchParamRegistry.requestRefresh();
1438        }
1439
1440        protected final boolean shouldSkipReindex(RequestDetails theRequestDetails) {
1441                if (theRequestDetails == null) {
1442                        return false;
1443                }
1444                Object shouldSkip = theRequestDetails.getUserData().getOrDefault(JpaConstants.SKIP_REINDEX_ON_UPDATE, false);
1445                return Boolean.parseBoolean(shouldSkip.toString());
1446        }
1447
1448        private boolean isCommonSearchParam(List<String> theBase) {
1449                // If the base contains the special resource "Resource", this is a common SP that applies to all resources
1450                return theBase.stream().map(String::toLowerCase).anyMatch(BASE_RESOURCE_NAME::equals);
1451        }
1452
1453        @Override
1454        @Transactional
1455        public <MT extends IBaseMetaType> MT metaAddOperation(
1456                        IIdType theResourceId, MT theMetaAdd, RequestDetails theRequest) {
1457
1458                RequestPartitionId requestPartitionId =
1459                                myRequestPartitionHelperService.determineReadPartitionForRequestForServerOperation(
1460                                                theRequest, JpaConstants.OPERATION_META_ADD);
1461
1462                myTransactionService
1463                                .withRequest(theRequest)
1464                                .withRequestPartitionId(requestPartitionId)
1465                                .execute(() -> doMetaAddOperation(theResourceId, theMetaAdd, theRequest, requestPartitionId));
1466
1467                @SuppressWarnings("unchecked")
1468                MT retVal = (MT) metaGetOperation(theMetaAdd.getClass(), theResourceId, theRequest);
1469                return retVal;
1470        }
1471
1472        protected <MT extends IBaseMetaType> void doMetaAddOperation(
1473                        IIdType theResourceId, MT theMetaAdd, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
1474                TransactionDetails transactionDetails = new TransactionDetails();
1475
1476                StopWatch w = new StopWatch();
1477                BaseHasResource entity = readEntity(theResourceId, true, theRequest, theRequestPartitionId);
1478
1479                if (isNull(entity)) {
1480                        throw new ResourceNotFoundException(Msg.code(1993) + theResourceId);
1481                }
1482
1483                ResourceTable latestVersion =
1484                                readEntityLatestVersion(theRequest, theResourceId, theRequestPartitionId, transactionDetails);
1485                if (latestVersion.getVersion() != entity.getVersion()) {
1486                        doMetaAdd(theMetaAdd, entity, theRequest, transactionDetails);
1487                } else {
1488                        doMetaAdd(theMetaAdd, latestVersion, theRequest, transactionDetails);
1489
1490                        // Also update history entry
1491                        ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(
1492                                        entity.getResourceId().toFk(), entity.getVersion());
1493                        doMetaAdd(theMetaAdd, history, theRequest, transactionDetails);
1494                }
1495
1496                ourLog.debug("Processed metaAddOperation on {} in {}ms", theResourceId, w.getMillisAndRestart());
1497        }
1498
1499        @Override
1500        @Transactional
1501        public <MT extends IBaseMetaType> MT metaDeleteOperation(
1502                        IIdType theResourceId, MT theMetaDel, RequestDetails theRequest) {
1503
1504                RequestPartitionId requestPartitionId =
1505                                myRequestPartitionHelperService.determineReadPartitionForRequestForServerOperation(
1506                                                theRequest, JpaConstants.OPERATION_META_DELETE);
1507
1508                myTransactionService
1509                                .withRequest(theRequest)
1510                                .withRequestPartitionId(requestPartitionId)
1511                                .execute(() -> doMetaDeleteOperation(theResourceId, theMetaDel, theRequest, requestPartitionId));
1512
1513                @SuppressWarnings("unchecked")
1514                MT retVal = (MT) metaGetOperation(theMetaDel.getClass(), theResourceId, theRequest);
1515                return retVal;
1516        }
1517
1518        @Transactional
1519        public <MT extends IBaseMetaType> void doMetaDeleteOperation(
1520                        IIdType theResourceId, MT theMetaDel, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
1521                TransactionDetails transactionDetails = new TransactionDetails();
1522                StopWatch w = new StopWatch();
1523
1524                BaseHasResource entity = readEntity(theResourceId, true, theRequest, theRequestPartitionId);
1525
1526                if (isNull(entity)) {
1527                        throw new ResourceNotFoundException(Msg.code(1994) + theResourceId);
1528                }
1529
1530                ResourceTable latestVersion =
1531                                readEntityLatestVersion(theRequest, theResourceId, theRequestPartitionId, transactionDetails);
1532                boolean nonVersionedTags =
1533                                myStorageSettings.getTagStorageMode() != JpaStorageSettings.TagStorageModeEnum.VERSIONED;
1534
1535                if (latestVersion.getVersion() != entity.getVersion() || nonVersionedTags) {
1536                        doMetaDelete(theMetaDel, entity, theRequest, transactionDetails);
1537                } else {
1538                        doMetaDelete(theMetaDel, latestVersion, theRequest, transactionDetails);
1539                        // Also update history entry
1540                        ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion(
1541                                        entity.getResourceId().toFk(), entity.getVersion());
1542                        doMetaDelete(theMetaDel, history, theRequest, transactionDetails);
1543                }
1544
1545                ourLog.debug("Processed metaDeleteOperation on {} in {}ms", theResourceId.getValue(), w.getMillisAndRestart());
1546        }
1547
1548        @Override
1549        public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, IIdType theId, RequestDetails theRequest) {
1550                return myTransactionService.withRequest(theRequest).execute(() -> {
1551                        Set<TagDefinition> tagDefs = new HashSet<>();
1552                        BaseHasResource<?> entity = readEntity(theId, theRequest);
1553                        for (BaseTag next : entity.getTags()) {
1554                                tagDefs.add(next.getTag());
1555                        }
1556                        MT retVal = toMetaDt(theType, tagDefs);
1557
1558                        retVal.setLastUpdated(entity.getUpdatedDate());
1559                        retVal.setVersionId(Long.toString(entity.getVersion()));
1560
1561                        return retVal;
1562                });
1563        }
1564
1565        @Override
1566        @Transactional
1567        public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails) {
1568                String sql =
1569                                "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t WHERE t.myResourceType = :res_type)";
1570                TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class);
1571                q.setParameter("res_type", myResourceName);
1572                List<TagDefinition> tagDefinitions = q.getResultList();
1573
1574                return toMetaDt(theType, tagDefinitions);
1575        }
1576
1577        private boolean isDeleted(BaseHasResource entityToUpdate) {
1578                return entityToUpdate.getDeleted() != null;
1579        }
1580
1581        @PostConstruct
1582        @Override
1583        public void start() {
1584                assert getStorageSettings() != null;
1585
1586                RuntimeResourceDefinition def = getContext().getResourceDefinition(myResourceType);
1587                myResourceName = def.getName();
1588
1589                if (mySearchDao != null && mySearchDao.isDisabled()) {
1590                        mySearchDao = null;
1591                }
1592
1593                ourLog.debug("Starting resource DAO for type: {}", getResourceName());
1594                myInstanceValidator = getApplicationContext().getBean(IInstanceValidatorModule.class);
1595                myTxTemplate = new TransactionTemplate(myPlatformTransactionManager);
1596
1597                myStorageInterceptorHooks = new StorageInterceptorHooksFacade(myInterceptorBroadcaster);
1598
1599                super.start();
1600        }
1601
1602        /**
1603         * Subclasses may override to provide behaviour. Invoked within a delete
1604         * transaction with the resource that is about to be deleted.
1605         */
1606        protected void preDelete(T theResourceToDelete, ResourceTable theEntityToDelete, RequestDetails theRequestDetails) {
1607                // nothing by default
1608        }
1609
1610        @Override
1611        @Transactional
1612        public T readByPid(IResourcePersistentId thePid) {
1613                return readByPid(thePid, false);
1614        }
1615
1616        @Override
1617        @Transactional
1618        public T readByPid(IResourcePersistentId thePid, boolean theDeletedOk) {
1619                StopWatch w = new StopWatch();
1620                JpaPid jpaPid = (JpaPid) thePid;
1621
1622                Optional<ResourceTable> entity = myResourceTableDao.findById(jpaPid);
1623                if (entity.isEmpty()) {
1624                        throw new ResourceNotFoundException(Msg.code(975) + "No resource found with PID " + jpaPid);
1625                }
1626                if (isDeleted(entity.get()) && !theDeletedOk) {
1627                        throw createResourceGoneException(entity.get());
1628                }
1629
1630                T retVal = myJpaStorageResourceParser.toResource(null, myResourceType, entity.get(), null, false);
1631
1632                ourLog.debug("Processed read on {} in {}ms", jpaPid, w.getMillis());
1633                return retVal;
1634        }
1635
1636        /**
1637         * @deprecated Use {@link #read(IIdType, RequestDetails)} instead
1638         */
1639        @Override
1640        public T read(IIdType theId) {
1641                return read(theId, null);
1642        }
1643
1644        @Override
1645        public T read(IIdType theId, RequestDetails theRequestDetails) {
1646                return read(theId, theRequestDetails, false);
1647        }
1648
1649        @Override
1650        public T read(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) {
1651                validateResourceTypeAndThrowInvalidRequestException(theId);
1652                TransactionDetails transactionDetails = new TransactionDetails();
1653
1654                RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead(
1655                                theRequest, myResourceName, theId);
1656
1657                return myTransactionService
1658                                .withRequest(theRequest)
1659                                .withTransactionDetails(transactionDetails)
1660                                .withRequestPartitionId(requestPartitionId)
1661                                .read(() -> doReadInTransaction(theId, theRequest, theDeletedOk, requestPartitionId));
1662        }
1663
1664        private T doReadInTransaction(
1665                        IIdType theId, RequestDetails theRequest, boolean theDeletedOk, RequestPartitionId theRequestPartitionId) {
1666                assert TransactionSynchronizationManager.isActualTransactionActive();
1667
1668                StopWatch w = new StopWatch();
1669                BaseHasResource<?> entity = readEntity(theId, true, theRequest, theRequestPartitionId);
1670                validateResourceType(entity);
1671
1672                T retVal = myJpaStorageResourceParser.toResource(theRequest, myResourceType, entity, null, false);
1673
1674                if (!theDeletedOk) {
1675                        if (isDeleted(entity)) {
1676                                throw createResourceGoneException(entity);
1677                        }
1678                }
1679                // If the resolved fhir model is null, we don't need to run pre-access over or pre-show over it.
1680                if (retVal != null) {
1681                        invokeStoragePreAccessResources(theId, theRequest, retVal);
1682                        retVal = invokeStoragePreShowResources(theRequest, retVal);
1683                }
1684
1685                ourLog.debug("Processed read on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
1686                return retVal;
1687        }
1688
1689        @Nullable
1690        private T invokeStoragePreShowResources(RequestDetails theRequest, T retVal) {
1691                retVal = invokeStoragePreShowResources(myInterceptorBroadcaster, theRequest, retVal);
1692                return retVal;
1693        }
1694
1695        private void invokeStoragePreAccessResources(IIdType theId, RequestDetails theRequest, T theResource) {
1696                invokeStoragePreAccessResources(myInterceptorBroadcaster, theRequest, theId, theResource);
1697        }
1698
1699        private Optional<T> invokeStoragePreAccessResources(RequestDetails theRequest, T theResource) {
1700                IInterceptorBroadcaster compositeBroadcaster =
1701                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest);
1702                if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) {
1703                        SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(theResource);
1704                        HookParams params = new HookParams()
1705                                        .add(IPreResourceAccessDetails.class, accessDetails)
1706                                        .add(RequestDetails.class, theRequest)
1707                                        .addIfMatchesType(ServletRequestDetails.class, theRequest);
1708                        compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params);
1709                        if (accessDetails.isDontReturnResourceAtIndex(0)) {
1710                                return Optional.empty();
1711                        }
1712                }
1713                return Optional.of(theResource);
1714        }
1715
1716        @Override
1717        public BaseHasResource readEntity(IIdType theId, RequestDetails theRequest) {
1718                RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead(
1719                                theRequest, myResourceName, theId);
1720                return myTransactionService
1721                                .withRequest(theRequest)
1722                                .withRequestPartitionId(requestPartitionId)
1723                                .execute(() -> readEntity(theId, true, theRequest, requestPartitionId));
1724        }
1725
1726        @Override
1727        public ReindexOutcome reindex(
1728                        IResourcePersistentId thePid,
1729                        ReindexParameters theReindexParameters,
1730                        RequestDetails theRequest,
1731                        TransactionDetails theTransactionDetails) {
1732                ReindexOutcome retVal = new ReindexOutcome();
1733
1734                JpaPid jpaPid = (JpaPid) thePid;
1735
1736                // Careful!  Reindex only reads ResourceTable, but we tell Hibernate to check version
1737                // to ensure Hibernate will catch concurrent updates (PUT/DELETE) elsewhere.
1738                // Otherwise, we may index stale data.  See #4584
1739                // We use the main entity as the lock object since all the index rows hang off it.
1740                ResourceTable entity;
1741                if (theReindexParameters.isOptimisticLock()) {
1742                        entity = myEntityManager.find(ResourceTable.class, jpaPid, LockModeType.OPTIMISTIC);
1743                } else {
1744                        entity = myEntityManager.find(ResourceTable.class, jpaPid);
1745                }
1746
1747                if (entity == null) {
1748                        retVal.addWarning("Unable to find entity with PID: " + jpaPid.getId());
1749                        return retVal;
1750                }
1751
1752                boolean reindexSearchParameters =
1753                                theReindexParameters.getReindexSearchParameters() != ReindexParameters.ReindexSearchParametersEnum.NONE;
1754                boolean correctCurrentVersion =
1755                                theReindexParameters.getCorrectCurrentVersion() != ReindexParameters.CorrectCurrentVersionModeEnum.NONE;
1756                boolean optimizeStorage =
1757                                theReindexParameters.getOptimizeStorage() != ReindexParameters.OptimizeStorageModeEnum.NONE;
1758
1759                if (correctCurrentVersion) {
1760                        CorrectCurrentVersionResponse outcome =
1761                                        reindexCorrectCurrentVersion(entity, theReindexParameters.getReindexSearchParameters());
1762                        if (outcome.forceReindexSps()) {
1763                                reindexSearchParameters = true;
1764                        }
1765                        if (outcome.forceReloadResourceEntity() && (reindexSearchParameters || optimizeStorage)) {
1766                                entity = myEntityManager.find(ResourceTable.class, jpaPid);
1767                                if (outcome.currentVersion() != null) {
1768                                        entity.setCurrentVersionEntity(outcome.currentVersion());
1769                                }
1770                        }
1771                }
1772
1773                if (reindexSearchParameters) {
1774                        reindexSearchParameters(entity, retVal, theTransactionDetails);
1775                }
1776
1777                if (optimizeStorage) {
1778                        reindexOptimizeStorage(entity, theReindexParameters.getOptimizeStorage());
1779                }
1780
1781                return retVal;
1782        }
1783
1784        /**
1785         * @return Returns an updated copy of the entity if it was modified (returns null otherwise)
1786         */
1787        private CorrectCurrentVersionResponse reindexCorrectCurrentVersion(
1788                        ResourceTable theEntity, ReindexParameters.ReindexSearchParametersEnum theReindexSearchParameters) {
1789                ResourceHistoryTable version = theEntity.getCurrentVersionEntity();
1790
1791                if (version == null) {
1792                        Optional<ResourceHistoryTable> currentVersionOpt = myResourceHistoryTableDao
1793                                        .findVersionsForResource(
1794                                                        SINGLE_RESULT, theEntity.getResourceId().toFk())
1795                                        .findFirst();
1796
1797                        if (currentVersionOpt.isPresent()) {
1798                                Long newVersion = currentVersionOpt.get().getVersion();
1799                                if (newVersion == theEntity.getVersion()) {
1800                                        // Already have the correct version
1801                                        return null;
1802                                }
1803
1804                                ourLog.info(
1805                                                "Correcting current version for {}/{} (PID {}) from version {} to version {}",
1806                                                theEntity.getResourceType(),
1807                                                theEntity.getFhirId(),
1808                                                theEntity.getResourceId(),
1809                                                theEntity.getVersion(),
1810                                                newVersion);
1811
1812                                /*
1813                                 * We are going to manually change (i.e. correct) the version number on the HFJ_RESOURCE table. The
1814                                 * version number is used as the optimistic locking key, so we need to detach the entity first, or
1815                                 * we'll get an optimistic lock failure later.
1816                                 */
1817                                myEntityManager.detach(theEntity);
1818
1819                                myResourceTableDao.updateVersionAndLastUpdated(theEntity.getId(), newVersion, new Date());
1820
1821                                return new CorrectCurrentVersionResponse(false, true, currentVersionOpt.get());
1822
1823                        } else {
1824                                ourLog.info(
1825                                                "No versions exist for {}/{} (PID {}), marking resource as deleted",
1826                                                theEntity.getResourceType(),
1827                                                theEntity.getFhirId(),
1828                                                theEntity.getResourceId());
1829                                theEntity.setDeleted(new Date());
1830                                return new CorrectCurrentVersionResponse(true, false, null);
1831                        }
1832                }
1833
1834                return new CorrectCurrentVersionResponse(false, false, null);
1835        }
1836
1837        @SuppressWarnings("unchecked")
1838        private void reindexSearchParameters(
1839                        ResourceTable entity, ReindexOutcome theReindexOutcome, TransactionDetails theTransactionDetails) {
1840                try {
1841                        T resource = (T) myJpaStorageResourceParser.toResource(entity, false);
1842                        reindexSearchParameters(resource, entity, theTransactionDetails);
1843                } catch (Exception e) {
1844                        ourLog.warn("Failure during reindex: {}", e.toString());
1845                        theReindexOutcome.addWarning("Failed to reindex resource " + entity.getIdDt() + ": " + e);
1846                        myResourceTableDao.updateIndexStatus(entity.getId(), EntityIndexStatusEnum.INDEXING_FAILED);
1847                }
1848        }
1849
1850        /**
1851         * @deprecated Use {@link #reindex(IResourcePersistentId, ReindexParameters, RequestDetails, TransactionDetails)}
1852         */
1853        @Deprecated
1854        @Override
1855        public void reindex(T theResource, IBasePersistedResource theEntity) {
1856                assert TransactionSynchronizationManager.isActualTransactionActive();
1857                ResourceTable entity = (ResourceTable) theEntity;
1858                TransactionDetails transactionDetails = new TransactionDetails(entity.getUpdatedDate());
1859
1860                reindexSearchParameters(theResource, theEntity, transactionDetails);
1861        }
1862
1863        private void reindexSearchParameters(
1864                        T theResource, IBasePersistedResource theEntity, TransactionDetails transactionDetails) {
1865                ourLog.debug("Indexing resource {} - PID {}", theEntity.getIdDt().getValue(), theEntity.getPersistentId());
1866                if (theResource != null) {
1867                        CURRENTLY_REINDEXING.put(theResource, Boolean.TRUE);
1868                }
1869
1870                SystemRequestDetails request = new SystemRequestDetails();
1871                request.getUserData().put(JpaConstants.SKIP_REINDEX_ON_UPDATE, Boolean.TRUE);
1872
1873                updateEntity(
1874                                request, theResource, theEntity, theEntity.getDeleted(), true, false, transactionDetails, true, false);
1875                if (theResource != null) {
1876                        CURRENTLY_REINDEXING.put(theResource, null);
1877                }
1878        }
1879
1880        private void reindexOptimizeStorage(
1881                        ResourceTable entity, ReindexParameters.OptimizeStorageModeEnum theOptimizeStorageMode) {
1882                ResourceHistoryTable historyEntity = entity.getCurrentVersionEntity();
1883                if (historyEntity != null) {
1884                        reindexOptimizeStorageHistoryEntity(entity, historyEntity);
1885                        if (theOptimizeStorageMode == ReindexParameters.OptimizeStorageModeEnum.ALL_VERSIONS) {
1886                                int pageSize = 100;
1887                                for (int page = 0; ((long) page * pageSize) < entity.getVersion(); page++) {
1888
1889                                        // We need to sort the pages, because we are updating the same data we are paging through.
1890                                        // If not sorted explicitly, a database like Postgres returns the same data multiple times on
1891                                        // different pages as the underlying data gets updated.
1892                                        PageRequest pageRequest = PageRequest.of(page, pageSize, Sort.by("myId"));
1893                                        Slice<ResourceHistoryTable> historyEntities =
1894                                                        myResourceHistoryTableDao.findAllVersionsExceptSpecificForResourcePid(
1895                                                                        pageRequest, entity.getId().toFk(), historyEntity.getVersion());
1896
1897                                        for (ResourceHistoryTable next : historyEntities) {
1898                                                reindexOptimizeStorageHistoryEntity(entity, next);
1899                                        }
1900                                }
1901                        }
1902                }
1903        }
1904
1905        /**
1906         * Note that the entity will be detached after being saved if it has changed
1907         * in order to avoid growing the number of resources in memory to be too big
1908         */
1909        private void reindexOptimizeStorageHistoryEntity(ResourceTable entity, ResourceHistoryTable historyEntity) {
1910                if (historyEntity.getEncoding() == ResourceEncodingEnum.JSONC
1911                                || historyEntity.getEncoding() == ResourceEncodingEnum.JSON) {
1912                        byte[] resourceBytes = historyEntity.getResource();
1913                        if (resourceBytes != null) {
1914                                String resourceText = decodeResource(resourceBytes, historyEntity.getEncoding());
1915                                myResourceHistoryCalculator.conditionallyAlterHistoryEntity(entity, historyEntity, resourceText);
1916                        }
1917                }
1918                if (myStorageSettings.isAccessMetaSourceInformationFromProvenanceTable()) {
1919                        if (isBlank(historyEntity.getSourceUri()) && isBlank(historyEntity.getRequestId())) {
1920                                IdAndPartitionId id = historyEntity.getId().asIdAndPartitionId();
1921                                Optional<ResourceHistoryProvenanceEntity> provenanceEntityOpt =
1922                                                myResourceHistoryProvenanceDao.findById(id);
1923                                if (provenanceEntityOpt.isPresent()) {
1924                                        ResourceHistoryProvenanceEntity provenanceEntity = provenanceEntityOpt.get();
1925                                        historyEntity.setSourceUri(provenanceEntity.getSourceUri());
1926                                        historyEntity.setRequestId(provenanceEntity.getRequestId());
1927                                        myResourceHistoryProvenanceDao.delete(provenanceEntity);
1928                                }
1929                        }
1930                }
1931        }
1932
1933        private BaseHasResource<?> readEntity(
1934                        IIdType theId,
1935                        boolean theCheckForForcedId,
1936                        RequestDetails theRequest,
1937                        RequestPartitionId requestPartitionId) {
1938                validateResourceTypeAndThrowInvalidRequestException(theId);
1939
1940                BaseHasResource entity;
1941                JpaPid pid = myIdHelperService
1942                                .resolveResourceIdentity(
1943                                                requestPartitionId,
1944                                                getResourceName(),
1945                                                theId.getIdPart(),
1946                                                ResolveIdentityMode.includeDeleted().cacheOk())
1947                                .getPersistentId();
1948                Set<Integer> readPartitions = null;
1949                if (requestPartitionId.isAllPartitions()) {
1950                        entity = myEntityManager.find(ResourceTable.class, pid);
1951                } else {
1952                        readPartitions = myRequestPartitionHelperService.toReadPartitions(requestPartitionId);
1953                        if (readPartitions.size() == 1) {
1954                                if (readPartitions.contains(null)) {
1955                                        entity = myResourceTableDao
1956                                                        .readByPartitionIdNull(pid.getId())
1957                                                        .orElse(null);
1958                                } else {
1959                                        entity = myResourceTableDao
1960                                                        .readByPartitionId(readPartitions.iterator().next(), pid.getId())
1961                                                        .orElse(null);
1962                                }
1963                        } else {
1964                                if (readPartitions.contains(null)) {
1965                                        List<Integer> readPartitionsWithoutNull =
1966                                                        readPartitions.stream().filter(Objects::nonNull).collect(Collectors.toList());
1967                                        entity = myResourceTableDao
1968                                                        .readByPartitionIdsOrNull(readPartitionsWithoutNull, pid.getId())
1969                                                        .orElse(null);
1970                                } else {
1971                                        entity = myResourceTableDao
1972                                                        .readByPartitionIds(readPartitions, pid.getId())
1973                                                        .orElse(null);
1974                                }
1975                        }
1976                }
1977
1978                // Verify that the resource is for the correct partition
1979                if (entity != null && readPartitions != null && entity.getPartitionId() != null) {
1980                        if (!readPartitions.contains(entity.getPartitionId().getPartitionId())) {
1981                                ourLog.debug(
1982                                                "Performing a read for PartitionId={} but entity has partition: {}",
1983                                                requestPartitionId,
1984                                                entity.getPartitionId());
1985                                entity = null;
1986                        }
1987                }
1988
1989                if (theId.hasVersionIdPart()) {
1990                        if (!theId.isVersionIdPartValidLong()) {
1991                                throw new ResourceNotFoundException(Msg.code(978)
1992                                                + getContext()
1993                                                                .getLocalizer()
1994                                                                .getMessageSanitized(
1995                                                                                BaseStorageDao.class,
1996                                                                                "invalidVersion",
1997                                                                                theId.getVersionIdPart(),
1998                                                                                theId.toUnqualifiedVersionless()));
1999                        }
2000                        if (entity.getVersion() != theId.getVersionIdPartAsLong()) {
2001                                entity = null;
2002                        }
2003                }
2004
2005                if (entity == null) {
2006                        if (theId.hasVersionIdPart()) {
2007                                TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery(
2008                                                "SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER",
2009                                                ResourceHistoryTable.class);
2010                                q.setParameter("RID", pid.getId());
2011                                q.setParameter("RTYP", myResourceName);
2012                                q.setParameter("RVER", theId.getVersionIdPartAsLong());
2013                                try {
2014                                        entity = q.getSingleResult();
2015                                } catch (NoResultException e) {
2016                                        throw new ResourceNotFoundException(Msg.code(979)
2017                                                        + getContext()
2018                                                                        .getLocalizer()
2019                                                                        .getMessageSanitized(
2020                                                                                        BaseStorageDao.class,
2021                                                                                        "invalidVersion",
2022                                                                                        theId.getVersionIdPart(),
2023                                                                                        theId.toUnqualifiedVersionless()));
2024                                }
2025                        }
2026                }
2027
2028                if (entity == null) {
2029                        throw new ResourceNotFoundException(Msg.code(1996) + "Resource " + theId + " is not known");
2030                }
2031
2032                validateResourceType(entity);
2033
2034                if (theCheckForForcedId) {
2035                        validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
2036                }
2037                return entity;
2038        }
2039
2040        @Override
2041        protected IBasePersistedResource readEntityLatestVersion(
2042                        IResourcePersistentId thePersistentId,
2043                        RequestDetails theRequestDetails,
2044                        TransactionDetails theTransactionDetails) {
2045                JpaPid jpaPid = (JpaPid) thePersistentId;
2046                return myEntityManager.find(ResourceTable.class, jpaPid);
2047        }
2048
2049        @Override
2050        @Nonnull
2051        protected ResourceTable readEntityLatestVersion(
2052                        IIdType theId, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) {
2053                RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead(
2054                                theRequestDetails, getResourceName(), theId);
2055
2056                return myTransactionService
2057                                .withRequest(theRequestDetails)
2058                                .withRequestPartitionId(requestPartitionId)
2059                                .execute(() ->
2060                                                readEntityLatestVersion(theRequestDetails, theId, requestPartitionId, theTransactionDetails));
2061        }
2062
2063        @Nonnull
2064        private ResourceTable readEntityLatestVersion(
2065                        RequestDetails theRequestDetails,
2066                        IIdType theId,
2067                        @Nonnull RequestPartitionId theRequestPartitionId,
2068                        TransactionDetails theTransactionDetails) {
2069                HapiTransactionService.requireTransaction();
2070
2071                IIdType id = theId;
2072                validateResourceTypeAndThrowInvalidRequestException(id);
2073                if (!id.hasResourceType()) {
2074                        id = id.withResourceType(getResourceName());
2075                }
2076
2077                JpaPid persistentId = null;
2078                if (theTransactionDetails != null) {
2079                        if (theTransactionDetails.isResolvedResourceIdEmpty(id.toUnqualifiedVersionless())) {
2080                                throw new ResourceNotFoundException(Msg.code(1997) + id);
2081                        }
2082                        if (theTransactionDetails.hasResolvedResourceIds()) {
2083                                persistentId = (JpaPid) theTransactionDetails.getResolvedResourceId(id);
2084                        }
2085                }
2086
2087                if (persistentId == null) {
2088                        persistentId = myIdHelperService.resolveResourceIdentityPid(
2089                                        theRequestPartitionId,
2090                                        id.getResourceType(),
2091                                        id.getIdPart(),
2092                                        ResolveIdentityMode.includeDeleted().cacheOk());
2093                }
2094
2095                ResourceTable entity = myEntityManager.find(ResourceTable.class, persistentId);
2096                if (entity == null) {
2097                        throw new ResourceNotFoundException(Msg.code(1998) + id);
2098                }
2099                validateGivenIdIsAppropriateToRetrieveResource(id, entity);
2100                return entity;
2101        }
2102
2103        @Transactional
2104        @Override
2105        public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm) {
2106                removeTag(theId, theTagType, theScheme, theTerm, null);
2107        }
2108
2109        @Transactional
2110        @Override
2111        public void removeTag(
2112                        IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, RequestDetails theRequest) {
2113                StopWatch w = new StopWatch();
2114                BaseHasResource<?> entity = readEntity(theId, theRequest);
2115                if (entity == null) {
2116                        throw new ResourceNotFoundException(Msg.code(1999) + theId);
2117                }
2118
2119                for (BaseTag next : new ArrayList<>(entity.getTags())) {
2120                        if (Objects.equals(next.getTag().getTagType(), theTagType)
2121                                        && Objects.equals(next.getTag().getSystem(), theScheme)
2122                                        && Objects.equals(next.getTag().getCode(), theTerm)) {
2123                                myEntityManager.remove(next);
2124                                entity.getTags().remove(next);
2125                        }
2126                }
2127
2128                if (entity.getTags().isEmpty()) {
2129                        entity.setHasTags(false);
2130                }
2131
2132                myEntityManager.merge(entity);
2133
2134                ourLog.debug(
2135                                "Processed remove tag {}/{} on {} in {}ms",
2136                                theScheme,
2137                                theTerm,
2138                                theId.getValue(),
2139                                w.getMillisAndRestart());
2140        }
2141
2142        /**
2143         * @deprecated Use {@link #search(SearchParameterMap, RequestDetails)} instead
2144         */
2145        @Override
2146        public IBundleProvider search(final SearchParameterMap theParams) {
2147                return search(theParams, null);
2148        }
2149
2150        @Override
2151        public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequest) {
2152                return search(theParams, theRequest, null);
2153        }
2154
2155        @Override
2156        public IBundleProvider search(
2157                        final SearchParameterMap theParams, RequestDetails theRequest, HttpServletResponse theServletResponse) {
2158
2159                if (theParams.getSearchContainedMode() == SearchContainedModeEnum.BOTH) {
2160                        throw new MethodNotAllowedException(Msg.code(983) + "Contained mode 'both' is not currently supported");
2161                }
2162                if (theParams.getSearchContainedMode() != SearchContainedModeEnum.FALSE
2163                                && !myStorageSettings.isIndexOnContainedResources()) {
2164                        throw new MethodNotAllowedException(
2165                                        Msg.code(984) + "Searching with _contained mode enabled is not enabled on this server");
2166                }
2167
2168                translateListSearchParams(theParams);
2169
2170                setOffsetAndCount(theParams, theRequest);
2171
2172                CacheControlDirective cacheControlDirective = new CacheControlDirective();
2173                if (theRequest != null) {
2174                        cacheControlDirective.parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL));
2175                }
2176
2177                IBundleProvider retVal = mySearchCoordinatorSvc.registerSearch(
2178                                this, theParams, getResourceName(), cacheControlDirective, theRequest);
2179
2180                if (retVal instanceof PersistedJpaBundleProvider provider) {
2181                        // Note: we calculate the partition -after- calling registerSearch, since that
2182                        // method invokes several interceptors that can affect the partition selection.
2183                        RequestPartitionId requestPartitionId =
2184                                        myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(
2185                                                        theRequest, getResourceName(), theParams);
2186
2187                        provider.setRequestPartitionId(requestPartitionId);
2188                        if (provider.getCacheStatus() == SearchCacheStatusEnum.HIT) {
2189                                if (theServletResponse != null && theRequest != null) {
2190                                        String value = "HIT from " + theRequest.getFhirServerBase();
2191                                        theServletResponse.addHeader(Constants.HEADER_X_CACHE, value);
2192                                }
2193                        }
2194                }
2195
2196                return retVal;
2197        }
2198
2199        private void translateListSearchParams(SearchParameterMap theParams) {
2200
2201                Set<Map.Entry<String, List<List<IQueryParameterType>>>> entryHashSet = new HashSet<>(theParams.entrySet());
2202
2203                // Translate _list=42 to _has=List:item:_id=42
2204                for (Map.Entry<String, List<List<IQueryParameterType>>> stringListEntry : entryHashSet) {
2205                        String key = stringListEntry.getKey();
2206                        if (Constants.PARAM_LIST.equals((key))) {
2207                                List<List<IQueryParameterType>> andOrValues = theParams.get(key);
2208                                theParams.remove(key);
2209                                List<List<IQueryParameterType>> hasParamValues = new ArrayList<>();
2210                                for (List<IQueryParameterType> orValues : andOrValues) {
2211                                        List<IQueryParameterType> orList = new ArrayList<>();
2212                                        for (IQueryParameterType value : orValues) {
2213                                                orList.add(new HasParam(
2214                                                                "List", ListResource.SP_ITEM, BaseResource.SP_RES_ID, value.getValueAsQueryToken()));
2215                                        }
2216                                        hasParamValues.add(orList);
2217                                }
2218                                theParams.put(Constants.PARAM_HAS, hasParamValues);
2219                        }
2220                }
2221        }
2222
2223        protected void setOffsetAndCount(SearchParameterMap theParams, RequestDetails theRequest) {
2224                if (theRequest != null) {
2225
2226                        if (theRequest.isSubRequest()) {
2227                                Integer max = getStorageSettings().getMaximumSearchResultCountInTransaction();
2228                                if (max != null) {
2229                                        Validate.inclusiveBetween(
2230                                                        1,
2231                                                        Integer.MAX_VALUE,
2232                                                        max,
2233                                                        "Maximum search result count in transaction must be a positive integer");
2234                                        theParams.setLoadSynchronousUpTo(getStorageSettings().getMaximumSearchResultCountInTransaction());
2235                                }
2236                        }
2237
2238                        final Integer offset = RestfulServerUtils.extractOffsetParameter(theRequest);
2239                        if (offset != null || !isPagingProviderDatabaseBacked(theRequest)) {
2240                                theParams.setLoadSynchronous(true);
2241                                if (offset != null) {
2242                                        Validate.inclusiveBetween(0, Integer.MAX_VALUE, offset, "Offset must be a positive integer");
2243                                }
2244                                theParams.setOffset(offset);
2245                        }
2246
2247                        Integer count = RestfulServerUtils.extractCountParameter(theRequest);
2248                        if (count != null) {
2249                                Integer maxPageSize = theRequest.getServer().getMaximumPageSize();
2250                                if (maxPageSize != null && count > maxPageSize) {
2251                                        ourLog.info(
2252                                                        "Reducing {} from {} to {} which is the maximum allowable page size.",
2253                                                        Constants.PARAM_COUNT,
2254                                                        count,
2255                                                        maxPageSize);
2256                                        count = maxPageSize;
2257                                }
2258                                theParams.setCount(count);
2259                        } else if (theRequest.getServer().getDefaultPageSize() != null) {
2260                                theParams.setCount(theRequest.getServer().getDefaultPageSize());
2261                        }
2262                }
2263        }
2264
2265        @Override
2266        public List<JpaPid> searchForIds(
2267                        SearchParameterMap theParams,
2268                        RequestDetails theRequest,
2269                        @Nullable IBaseResource theConditionalOperationTargetOrNull) {
2270                TransactionDetails transactionDetails = new TransactionDetails();
2271                RequestPartitionId requestPartitionId =
2272                                myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(
2273                                                theRequest, myResourceName, theParams, theConditionalOperationTargetOrNull);
2274
2275                myStorageInterceptorHooks.callStoragePresearchRegistered(
2276                                theRequest, theParams, new NonPersistedSearch(getResourceName()), requestPartitionId);
2277
2278                return myTransactionService
2279                                .withRequest(theRequest)
2280                                .withTransactionDetails(transactionDetails)
2281                                .withRequestPartitionId(requestPartitionId)
2282                                .searchList(() -> {
2283                                        if (isNull(theParams.getLoadSynchronousUpTo())) {
2284                                                theParams.setLoadSynchronousUpTo(myStorageSettings.getInternalSynchronousSearchSize());
2285                                        }
2286
2287                                        ISearchBuilder<JpaPid> builder =
2288                                                        mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType());
2289
2290                                        List<JpaPid> ids = new ArrayList<>();
2291
2292                                        String uuid = UUID.randomUUID().toString();
2293
2294                                        SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid);
2295                                        try (IResultIterator<JpaPid> iter =
2296                                                        builder.createQuery(theParams, searchRuntimeDetails, theRequest, requestPartitionId)) {
2297                                                while (iter.hasNext()) {
2298                                                        ids.add(iter.next());
2299                                                }
2300                                        } catch (IOException e) {
2301                                                ourLog.error("IO failure during database access", e);
2302                                        }
2303
2304                                        return ids;
2305                                });
2306        }
2307
2308        @Override
2309        public <PID extends IResourcePersistentId<?>> Stream<PID> searchForIdStream(
2310                        SearchParameterMap theParams,
2311                        RequestDetails theRequest,
2312                        @Nullable IBaseResource theConditionalOperationTargetOrNull) {
2313
2314                // the Stream is useless outside the bound connection time, so require our caller to have a session.
2315                HapiTransactionService.requireTransaction();
2316
2317                RequestPartitionId requestPartitionId =
2318                                myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(
2319                                                theRequest, myResourceName, theParams, theConditionalOperationTargetOrNull);
2320
2321                ISearchBuilder<JpaPid> builder = mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType());
2322
2323                String uuid = UUID.randomUUID().toString();
2324
2325                SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid);
2326                //noinspection unchecked
2327                return (Stream<PID>) myTransactionService
2328                                .withRequest(theRequest)
2329                                .search(() ->
2330                                                builder.createQueryStream(theParams, searchRuntimeDetails, theRequest, requestPartitionId));
2331        }
2332
2333        @Override
2334        public List<T> searchForResources(SearchParameterMap theParams, RequestDetails theRequest) {
2335                return searchForTransformedIds(theParams, theRequest, this::pidsToResource);
2336        }
2337
2338        @Override
2339        public List<IIdType> searchForResourceIds(SearchParameterMap theParams, RequestDetails theRequest) {
2340                return searchForTransformedIds(theParams, theRequest, this::pidsToIds);
2341        }
2342
2343        private <V> List<V> searchForTransformedIds(
2344                        SearchParameterMap theParams,
2345                        RequestDetails theRequest,
2346                        TriFunction<RequestDetails, Stream<JpaPid>, RequestPartitionId, Stream<V>> transform) {
2347                RequestPartitionId requestPartitionId =
2348                                myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(
2349                                                theRequest, myResourceName, theParams);
2350
2351                String uuid = UUID.randomUUID().toString();
2352
2353                SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid);
2354                return myTransactionService
2355                                .withRequest(theRequest)
2356                                .withPropagation(Propagation.REQUIRED)
2357                                .searchList(() -> {
2358                                        ISearchBuilder<JpaPid> builder =
2359                                                        mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType());
2360                                        Stream<JpaPid> pidStream =
2361                                                        builder.createQueryStream(theParams, searchRuntimeDetails, theRequest, requestPartitionId);
2362
2363                                        Stream<V> transformedStream = transform.apply(theRequest, pidStream, requestPartitionId);
2364
2365                                        return transformedStream.collect(Collectors.toList());
2366                                });
2367        }
2368
2369        /**
2370         * Fetch the resources in chunks and apply PreAccess/PreShow interceptors.
2371         */
2372        @Nonnull
2373        private Stream<T> pidsToResource(
2374                        RequestDetails theRequest, Stream<JpaPid> thePidStream, RequestPartitionId theRequestPartitionId) {
2375                ISearchBuilder<JpaPid> searchBuilder =
2376                                mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType());
2377                @SuppressWarnings("unchecked")
2378                Stream<T> resourceStream = (Stream<T>) new QueryChunker<>()
2379                                .chunk(thePidStream, SearchBuilder.getMaximumPageSize())
2380                                .flatMap(pidChunk -> searchBuilder.loadResourcesByPid(pidChunk, theRequest).stream());
2381                // apply interceptors
2382                return resourceStream
2383                                .flatMap(resource -> resource == null
2384                                                ? Stream.empty()
2385                                                : invokeStoragePreAccessResources(theRequest, resource).stream())
2386                                .flatMap(resource -> Optional.ofNullable(invokeStoragePreShowResources(theRequest, resource)).stream());
2387        }
2388
2389        /**
2390         * get the Ids from the ResourceTable entities in chunks.
2391         */
2392        @Nonnull
2393        private Stream<IIdType> pidsToIds(
2394                        RequestDetails theRequestDetails, Stream<JpaPid> thePidStream, RequestPartitionId theRequestPartitionId) {
2395
2396                Stream<JpaPid> pidStream = thePidStream;
2397
2398                if (!theRequestPartitionId.isAllPartitions()
2399                                && theRequestPartitionId.getPartitionIds().size() == 1) {
2400                        pidStream = pidStream.map(t -> t.setPartitionIdIfNotAlreadySet(
2401                                        theRequestPartitionId.getPartitionIds().get(0)));
2402                }
2403
2404                return new QueryChunker<>()
2405                                .chunk(pidStream, SearchBuilder.getMaximumPageSize())
2406                                .flatMap(ids -> myResourceTableDao.findAllById(ids).stream())
2407                                .map(ResourceTable::getIdDt);
2408        }
2409
2410        protected <MT extends IBaseMetaType> MT toMetaDt(Class<MT> theType, Collection<TagDefinition> tagDefinitions) {
2411                MT retVal = ReflectionUtil.newInstance(theType);
2412                for (TagDefinition next : tagDefinitions) {
2413                        switch (next.getTagType()) {
2414                                case PROFILE:
2415                                        retVal.addProfile(next.getCode());
2416                                        break;
2417                                case SECURITY_LABEL:
2418                                        retVal.addSecurity()
2419                                                        .setSystem(next.getSystem())
2420                                                        .setCode(next.getCode())
2421                                                        .setDisplay(next.getDisplay());
2422                                        break;
2423                                case TAG:
2424                                        retVal.addTag()
2425                                                        .setSystem(next.getSystem())
2426                                                        .setCode(next.getCode())
2427                                                        .setDisplay(next.getDisplay());
2428                                        break;
2429                        }
2430                }
2431                myMetaTagSorter.sort(retVal);
2432                return retVal;
2433        }
2434
2435        private ArrayList<TagDefinition> toTagList(IBaseMetaType theMeta) {
2436                ArrayList<TagDefinition> retVal = new ArrayList<>();
2437
2438                for (IBaseCoding next : theMeta.getTag()) {
2439                        retVal.add(new TagDefinition(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay()));
2440                }
2441                for (IBaseCoding next : theMeta.getSecurity()) {
2442                        retVal.add(
2443                                        new TagDefinition(TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay()));
2444                }
2445                for (IPrimitiveType<String> next : theMeta.getProfile()) {
2446                        retVal.add(new TagDefinition(TagTypeEnum.PROFILE, BaseHapiFhirDao.NS_JPA_PROFILE, next.getValue(), null));
2447                }
2448
2449                return retVal;
2450        }
2451
2452        /**
2453         * @deprecated Use {@link #update(T, RequestDetails)} instead
2454         */
2455        @Override
2456        public DaoMethodOutcome update(T theResource) {
2457                return update(theResource, null, null);
2458        }
2459
2460        @Override
2461        public DaoMethodOutcome update(T theResource, RequestDetails theRequestDetails) {
2462                return update(theResource, null, theRequestDetails);
2463        }
2464
2465        /**
2466         * @deprecated Use {@link #update(T, String, RequestDetails)} instead
2467         */
2468        @Override
2469        public DaoMethodOutcome update(T theResource, String theMatchUrl) {
2470                return update(theResource, theMatchUrl, null);
2471        }
2472
2473        @Override
2474        public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) {
2475                return update(theResource, theMatchUrl, true, theRequestDetails);
2476        }
2477
2478        @Override
2479        public DaoMethodOutcome update(
2480                        T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) {
2481                return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails, new TransactionDetails());
2482        }
2483
2484        @Override
2485        public DaoMethodOutcome update(
2486                        T theResource,
2487                        String theMatchUrl,
2488                        boolean thePerformIndexing,
2489                        boolean theForceUpdateVersion,
2490                        RequestDetails theRequest,
2491                        @Nonnull TransactionDetails theTransactionDetails) {
2492                if (theResource == null) {
2493                        String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "missingBody");
2494                        throw new InvalidRequestException(Msg.code(986) + msg);
2495                }
2496                if (!theResource.getIdElement().hasIdPart() && isBlank(theMatchUrl)) {
2497                        String type = myFhirContext.getResourceType(theResource);
2498                        String msg = myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "updateWithNoId", type);
2499                        throw new InvalidRequestException(Msg.code(987) + msg);
2500                }
2501
2502                /*
2503                 * Resource updates will modify/update the version of the resource with the new version. This is generally helpful
2504                 * but leads to issues if the transaction is rolled back and retried. So if we do a rollback, we reset the resource
2505                 * version to what it was.
2506                 */
2507                String id = theResource.getIdElement().getValue();
2508                Runnable onRollback = () -> {
2509                        ourLog.trace("Rolling back from {} to {}", theResource.getIdElement(), id);
2510                        theResource.getIdElement().setValue(id);
2511                };
2512                theTransactionDetails.addRollbackUndoAction(onRollback);
2513
2514                RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(
2515                                theRequest, theResource, getResourceName());
2516
2517                boolean rewriteHistory = theRequest != null && theRequest.isRewriteHistory();
2518                if (rewriteHistory && !myStorageSettings.isUpdateWithHistoryRewriteEnabled()) {
2519                        String msg =
2520                                        getContext().getLocalizer().getMessage(BaseStorageDao.class, "updateWithHistoryRewriteDisabled");
2521                        throw new InvalidRequestException(Msg.code(2781) + msg);
2522                }
2523
2524                Callable<DaoMethodOutcome> updateCallback;
2525                if (rewriteHistory) {
2526                        updateCallback = () -> doUpdateWithHistoryRewrite(
2527                                        theResource, theRequest, theTransactionDetails, requestPartitionId, RestOperationTypeEnum.UPDATE);
2528                } else {
2529                        updateCallback = () -> doUpdate(
2530                                        theResource,
2531                                        theMatchUrl,
2532                                        thePerformIndexing,
2533                                        theForceUpdateVersion,
2534                                        theRequest,
2535                                        theTransactionDetails,
2536                                        requestPartitionId);
2537                }
2538
2539                // Execute the update in a retryable transaction
2540                return myTransactionService
2541                                .withRequest(theRequest)
2542                                .withTransactionDetails(theTransactionDetails)
2543                                .withRequestPartitionId(requestPartitionId)
2544                                .execute(updateCallback);
2545        }
2546
2547        private DaoMethodOutcome doUpdate(
2548                        T theResource,
2549                        String theMatchUrl,
2550                        boolean thePerformIndexing,
2551                        boolean theForceUpdateVersion,
2552                        RequestDetails theRequest,
2553                        TransactionDetails theTransactionDetails,
2554                        RequestPartitionId theRequestPartitionId) {
2555                DaoMethodOutcome outcome = null;
2556                preProcessResourceForStorage(theResource);
2557                preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing);
2558
2559                ResourceTable entity = null;
2560
2561                IIdType resourceId = null;
2562                RestOperationTypeEnum update = RestOperationTypeEnum.UPDATE;
2563                if (isNotBlank(theMatchUrl)) {
2564                        // Validate that the supplied resource matches the conditional.
2565                        Set<JpaPid> match = myMatchResourceUrlService.processMatchUrl(
2566                                        theMatchUrl, myResourceType, theTransactionDetails, theRequest, theResource, theRequestPartitionId);
2567                        if (match.size() > 1) {
2568                                String msg = getContext()
2569                                                .getLocalizer()
2570                                                .getMessageSanitized(
2571                                                                BaseStorageDao.class,
2572                                                                "transactionOperationWithMultipleMatchFailure",
2573                                                                "UPDATE",
2574                                                                myResourceName,
2575                                                                theMatchUrl,
2576                                                                match.size());
2577                                throw new PreconditionFailedException(Msg.code(988) + msg);
2578                        } else if (match.size() == 1) {
2579                                JpaPid pid = match.iterator().next();
2580                                entity = myEntityManager.find(ResourceTable.class, pid);
2581                                resourceId = entity.getIdDt();
2582                                if (myFhirContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)
2583                                                && theResource.getIdElement().getIdPart() != null) {
2584                                        if (!Objects.equals(theResource.getIdElement().getIdPart(), resourceId.getIdPart())) {
2585                                                String msg = getContext()
2586                                                                .getLocalizer()
2587                                                                .getMessageSanitized(
2588                                                                                BaseStorageDao.class,
2589                                                                                "transactionOperationWithIdNotMatchFailure",
2590                                                                                "UPDATE",
2591                                                                                theMatchUrl);
2592                                                throw new InvalidRequestException(Msg.code(2279) + msg);
2593                                        }
2594                                }
2595                        } else {
2596                                // assign UUID if no id provided in the request (numeric id mode is handled in doCreateForPostOrPut)
2597                                if (!theResource.getIdElement().hasIdPart()
2598                                                && getStorageSettings().getResourceServerIdStrategy()
2599                                                                == JpaStorageSettings.IdStrategyEnum.UUID) {
2600                                        theResource.setId(UUID.randomUUID().toString());
2601                                        theResource.setUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED, Boolean.TRUE);
2602                                }
2603                                outcome = doCreateForPostOrPut(
2604                                                theRequest,
2605                                                theResource,
2606                                                theMatchUrl,
2607                                                false,
2608                                                thePerformIndexing,
2609                                                theRequestPartitionId,
2610                                                update,
2611                                                theTransactionDetails);
2612
2613                                // Pre-cache the match URL
2614                                if (outcome.getPersistentId() != null) {
2615                                        myMatchResourceUrlService.matchUrlResolved(
2616                                                        theTransactionDetails, getResourceName(), theMatchUrl, (JpaPid) outcome.getPersistentId());
2617                                }
2618                        }
2619                } else {
2620                        /*
2621                         * Note: resourceId will not be null or empty here, because we
2622                         * check it and reject requests in
2623                         * BaseOutcomeReturningMethodBindingWithResourceParam
2624                         */
2625                        resourceId = theResource.getIdElement();
2626                        assert resourceId != null;
2627                        assert resourceId.hasIdPart();
2628
2629                        if (!resourceId.hasResourceType()) {
2630                                resourceId = resourceId.withResourceType(getResourceName());
2631                        }
2632
2633                        boolean create = false;
2634
2635                        if (theRequest != null) {
2636                                String existenceCheck = theRequest.getHeader(JpaConstants.HEADER_UPSERT_EXISTENCE_CHECK);
2637                                if (JpaConstants.HEADER_UPSERT_EXISTENCE_CHECK_DISABLED.equals(existenceCheck)) {
2638                                        create = true;
2639                                }
2640                        }
2641
2642                        if (!create) {
2643                                if (theTransactionDetails != null && theTransactionDetails.hasNullResolvedResourceId(resourceId)) {
2644                                        create = true;
2645                                } else {
2646                                        try {
2647                                                entity = readEntityLatestVersion(
2648                                                                theRequest, resourceId, theRequestPartitionId, theTransactionDetails);
2649                                                if (myPartitionSettings.isPartitioningEnabled()) {
2650                                                        validatePartitionIdMatch(theResource.getIdElement(), theRequestPartitionId, entity);
2651                                                }
2652                                        } catch (ResourceNotFoundException e) {
2653                                                create = true;
2654                                        }
2655                                }
2656                        }
2657
2658                        if (create) {
2659                                outcome = doCreateForPostOrPut(
2660                                                theRequest,
2661                                                theResource,
2662                                                null,
2663                                                false,
2664                                                thePerformIndexing,
2665                                                theRequestPartitionId,
2666                                                update,
2667                                                theTransactionDetails);
2668                        }
2669                }
2670
2671                // Start
2672                if (outcome == null) {
2673                        UpdateParameters updateParameters = new UpdateParameters<>()
2674                                        .setRequestDetails(theRequest)
2675                                        .setResourceIdToUpdate(resourceId)
2676                                        .setMatchUrl(theMatchUrl)
2677                                        .setShouldPerformIndexing(thePerformIndexing)
2678                                        .setShouldForceUpdateVersion(theForceUpdateVersion)
2679                                        .setResource(theResource)
2680                                        .setEntity(entity)
2681                                        .setOperationType(update)
2682                                        .setTransactionDetails(theTransactionDetails)
2683                                        .setShouldForcePopulateOldResourceForProcessing(false);
2684
2685                        outcome = doUpdateForUpdateOrPatch(updateParameters);
2686                }
2687
2688                postUpdateTransaction(theTransactionDetails);
2689
2690                return outcome;
2691        }
2692
2693        @SuppressWarnings("rawtypes")
2694        protected void postUpdateTransaction(TransactionDetails theTransactionDetails) {
2695                // Transactions will delete these at the end of the entire transaction
2696                if (!theTransactionDetails.isFhirTransaction()) {
2697                        Set<IResourcePersistentId> resourceIds = theTransactionDetails.getUpdatedResourceIds();
2698                        if (resourceIds != null && !resourceIds.isEmpty()) {
2699                                @Nonnull
2700                                List<JpaPid> ids = resourceIds.stream().map(r -> (JpaPid) r).collect(Collectors.toList());
2701                                myResourceSearchUrlSvc.deleteByResIds(ids);
2702                        }
2703                }
2704        }
2705
2706        @Override
2707        protected DaoMethodOutcome doUpdateForUpdateOrPatch(UpdateParameters<T> theUpdateParameters) {
2708
2709                /*
2710                 * We stored a resource searchUrl at creation time to prevent resource duplication.
2711                 * We'll clear any currently existing urls from the db, otherwise we could hit
2712                 * duplicate index violations if we try to add another (after this create/update)
2713                 */
2714                ResourceTable entity = (ResourceTable) theUpdateParameters.getEntity();
2715
2716                /*
2717                 * If we read back the entity from hibernate, and it's already saying
2718                 * it's been updated in this transaction, then someone is probably
2719                 * doing multiple modifications of the same resource within the same
2720                 * database transaction. The only way for this to work cleanly is for
2721                 * us to flush hibernate now. That way we can increment the version
2722                 * a second time, create multiple history entries, etc.
2723                 */
2724                if (entity != null && entity.isVersionUpdatedInCurrentTransaction()) {
2725                        myEntityManager.flush();
2726                }
2727
2728                if (entity.isSearchUrlPresent()) {
2729                        JpaPid persistentId = entity.getResourceId();
2730                        theUpdateParameters.getTransactionDetails().addUpdatedResourceId(persistentId);
2731
2732                        entity.setSearchUrlPresent(false); // it will be removed at the end
2733                }
2734
2735                if (entity.isDeleted()) {
2736                        // We're un-deleting this entity so let's inform the memory cache service
2737                        myIdHelperService.addResolvedPidToFhirIdAfterCommit(
2738                                        entity.getPersistentId(),
2739                                        entity.getPartitionId() == null
2740                                                        ? RequestPartitionId.defaultPartition()
2741                                                        : entity.getPartitionId().toPartitionId(),
2742                                        entity.getResourceType(),
2743                                        entity.getFhirId(),
2744                                        null);
2745                }
2746
2747                boolean shouldForcePopulateOldResourceForProcessing = myInterceptorBroadcaster instanceof InterceptorService
2748                                && ((InterceptorService) myInterceptorBroadcaster)
2749                                                .hasRegisteredInterceptor(PatientCompartmentEnforcingInterceptor.class);
2750                theUpdateParameters.setShouldForcePopulateOldResourceForProcessing(shouldForcePopulateOldResourceForProcessing);
2751                return super.doUpdateForUpdateOrPatch(theUpdateParameters);
2752        }
2753
2754        /**
2755         * Method for updating the historical version of the resource when a history version id is included in the request.
2756         *
2757         * @param theResource           to be saved
2758         * @param theRequest            details of the request
2759         * @param theTransactionDetails details of the transaction
2760         * @param theRequestPartitionId the partition on which to perform the request
2761         * @param theRestOperationType  the rest operation type (update or patch)
2762         * @return the outcome of the operation
2763         */
2764        DaoMethodOutcome doUpdateWithHistoryRewrite(
2765                        T theResource,
2766                        RequestDetails theRequest,
2767                        TransactionDetails theTransactionDetails,
2768                        RequestPartitionId theRequestPartitionId,
2769                        RestOperationTypeEnum theRestOperationType) {
2770                StopWatch w = new StopWatch();
2771
2772                // No need for indexing as this will update a non-current version of the resource which will not be searchable
2773                preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, false);
2774
2775                BaseHasResource entity;
2776                BaseHasResource currentEntity;
2777
2778                IIdType resourceId;
2779
2780                resourceId = theResource.getIdElement();
2781                assert resourceId != null;
2782                assert resourceId.hasIdPart();
2783
2784                try {
2785                        currentEntity = readEntityLatestVersion(
2786                                        theRequest, resourceId.toVersionless(), theRequestPartitionId, theTransactionDetails);
2787
2788                        if (!resourceId.hasVersionIdPart()) {
2789                                throw new InvalidRequestException(
2790                                                Msg.code(2093) + "Invalid resource ID, ID must contain a history version");
2791                        }
2792                        entity = readEntity(resourceId, theRequest);
2793                        validateResourceType(entity);
2794                } catch (ResourceNotFoundException e) {
2795                        throw new ResourceNotFoundException(
2796                                        Msg.code(2087) + "Resource not found [" + resourceId + "] - Doesn't exist");
2797                }
2798
2799                if (resourceId.hasResourceType() && !resourceId.getResourceType().equals(getResourceName())) {
2800                        throw new UnprocessableEntityException(
2801                                        Msg.code(2088) + "Invalid resource ID[" + entity.getIdDt().toUnqualifiedVersionless() + "] of type["
2802                                                        + entity.getResourceType() + "] - Does not match expected [" + getResourceName() + "]");
2803                }
2804                assert resourceId.hasVersionIdPart();
2805
2806                boolean wasDeleted = isDeleted(entity);
2807                entity.setDeleted(null);
2808                boolean isUpdatingCurrent = resourceId.hasVersionIdPart()
2809                                && Long.parseLong(resourceId.getVersionIdPart()) == currentEntity.getVersion();
2810                IBasePersistedResource<?> savedEntity = updateHistoryEntity(
2811                                theRequest, theResource, currentEntity, entity, resourceId, theTransactionDetails, isUpdatingCurrent);
2812                DaoMethodOutcome outcome = toMethodOutcome(theRequest, savedEntity, theResource, null, theRestOperationType)
2813                                .setCreated(wasDeleted);
2814
2815                populateOperationOutcomeForUpdate(w, outcome, null, theRestOperationType, theTransactionDetails);
2816
2817                return outcome;
2818        }
2819
2820        @Override
2821        public MethodOutcome validate(
2822                        T theResource,
2823                        IIdType theId,
2824                        String theRawResource,
2825                        EncodingEnum theEncoding,
2826                        ValidationModeEnum theMode,
2827                        String theProfile,
2828                        RequestDetails theRequest) {
2829                TransactionDetails transactionDetails = new TransactionDetails();
2830
2831                if (theMode == ValidationModeEnum.DELETE) {
2832                        if (theId == null || !theId.hasIdPart()) {
2833                                throw new InvalidRequestException(
2834                                                Msg.code(991) + "No ID supplied. ID is required when validating with mode=DELETE");
2835                        }
2836
2837                        RequestPartitionId requestPartitionId =
2838                                        myRequestPartitionHelperService.determineReadPartitionForRequestForRead(
2839                                                        theRequest, getResourceName(), theId);
2840
2841                        return myTransactionService
2842                                        .withRequest(theRequest)
2843                                        .withRequestPartitionId(requestPartitionId)
2844                                        .execute(() -> {
2845                                                final ResourceTable entity =
2846                                                                readEntityLatestVersion(theRequest, theId, requestPartitionId, transactionDetails);
2847
2848                                                // Validate that there are no resources pointing to the candidate that
2849                                                // would prevent deletion
2850                                                DeleteConflictList deleteConflicts = new DeleteConflictList();
2851                                                if (getStorageSettings().isEnforceReferentialIntegrityOnDelete()) {
2852                                                        myDeleteConflictService.validateOkToDelete(
2853                                                                        deleteConflicts, entity, true, theRequest, new TransactionDetails());
2854                                                }
2855                                                DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts);
2856
2857                                                IBaseOperationOutcome oo = createInfoOperationOutcome("Ok to delete");
2858                                                return new MethodOutcome(new IdDt(theId.getValue()), oo);
2859                                        });
2860                }
2861
2862                FhirValidator validator = getContext().newValidator();
2863                validator.setInterceptorBroadcaster(
2864                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest));
2865                validator.registerValidatorModule(getInstanceValidator());
2866                validator.registerValidatorModule(new IdChecker(theMode));
2867
2868                IBaseResource resourceToValidateById = null;
2869                if (theId != null && theId.hasResourceType() && theId.hasIdPart()) {
2870                        Class<? extends IBaseResource> type =
2871                                        getContext().getResourceDefinition(theId.getResourceType()).getImplementingClass();
2872                        IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDaoOrNull(type);
2873                        resourceToValidateById = dao.read(theId, theRequest);
2874                }
2875
2876                ValidationResult result;
2877                ValidationOptions options = new ValidationOptions().addProfileIfNotBlank(theProfile);
2878
2879                if (theResource == null) {
2880                        if (resourceToValidateById != null) {
2881                                result = validator.validateWithResult(resourceToValidateById, options);
2882                        } else {
2883                                String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "cantValidateWithNoResource");
2884                                throw new InvalidRequestException(Msg.code(992) + msg);
2885                        }
2886                } else if (isNotBlank(theRawResource)) {
2887                        result = validator.validateWithResult(theRawResource, options);
2888                } else {
2889                        result = validator.validateWithResult(theResource, options);
2890                }
2891
2892                MethodOutcome retVal = new MethodOutcome();
2893                retVal.setOperationOutcome(result.toOperationOutcome());
2894                // Note an earlier version of this code returned PreconditionFailedException when the validation
2895                // failed, but we since realized the spec requires we return 200 regardless of the validation result.
2896                return retVal;
2897        }
2898
2899        /**
2900         * Get the resource definition from the criteria which specifies the resource type
2901         */
2902        @Override
2903        public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria) {
2904                String resourceName;
2905                if (criteria == null || criteria.trim().isEmpty()) {
2906                        throw new IllegalArgumentException(Msg.code(994) + "Criteria cannot be empty");
2907                }
2908                if (criteria.contains("?")) {
2909                        resourceName = criteria.substring(0, criteria.indexOf("?"));
2910                } else {
2911                        resourceName = criteria;
2912                }
2913
2914                return getContext().getResourceDefinition(resourceName);
2915        }
2916
2917        private void validateGivenIdIsAppropriateToRetrieveResource(IIdType theId, BaseHasResource entity) {
2918                if (!entity.getIdDt().getIdPart().equals(theId.getIdPart())) {
2919                        // This means that the resource with the given numeric ID exists, but it has a "forced ID", meaning
2920                        // that
2921                        // as far as the outside world is concerned, the given ID doesn't exist (it's just an internal
2922                        // pointer
2923                        // to the
2924                        // forced ID)
2925                        throw new ResourceNotFoundException(Msg.code(2000) + theId);
2926                }
2927        }
2928
2929        private void validateResourceType(IBasePersistedResource<?> entity) {
2930                validateResourceType(entity, myResourceName);
2931        }
2932
2933        private void validateResourceTypeAndThrowInvalidRequestException(IIdType theId) {
2934                if (theId.hasResourceType() && !theId.getResourceType().equals(myResourceName)) {
2935                        // Note- Throw a HAPI FHIR exception here so that hibernate doesn't try to translate it into a database
2936                        // exception
2937                        throw new InvalidRequestException(Msg.code(996) + "Incorrect resource type (" + theId.getResourceType()
2938                                        + ") for this DAO, wanted: " + myResourceName);
2939                }
2940        }
2941
2942        @VisibleForTesting
2943        public void setIdHelperSvcForUnitTest(IIdHelperService theIdHelperService) {
2944                myIdHelperService = theIdHelperService;
2945        }
2946
2947        @SuppressWarnings("BooleanMethodIsAlwaysInverted")
2948        public static <T extends IBaseResource> boolean isResourceIdServerAssigned(IBaseResource theResource) {
2949                return Boolean.TRUE.equals(theResource.getUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED));
2950        }
2951
2952        private static class IdChecker implements IValidatorModule {
2953
2954                private final ValidationModeEnum myMode;
2955
2956                IdChecker(ValidationModeEnum theMode) {
2957                        myMode = theMode;
2958                }
2959
2960                @Override
2961                public void validateResource(IValidationContext<IBaseResource> theCtx) {
2962                        IBaseResource resource = theCtx.getResource();
2963                        if (resource instanceof Parameters) {
2964                                List<ParametersParameterComponent> params = ((Parameters) resource).getParameter();
2965                                params = params.stream()
2966                                                .filter(param -> param.getName().contains("resource"))
2967                                                .collect(Collectors.toList());
2968                                resource = params.get(0).getResource();
2969                        }
2970                        boolean hasId = resource.getIdElement().hasIdPart();
2971                        if (myMode == ValidationModeEnum.CREATE) {
2972                                if (hasId) {
2973                                        throw new UnprocessableEntityException(
2974                                                        Msg.code(997) + "Resource has an ID - ID must not be populated for a FHIR create");
2975                                }
2976                        } else if (myMode == ValidationModeEnum.UPDATE) {
2977                                if (!hasId) {
2978                                        throw new UnprocessableEntityException(
2979                                                        Msg.code(998) + "Resource has no ID - ID must be populated for a FHIR update");
2980                                }
2981                        }
2982                }
2983        }
2984
2985        /**
2986         * Validates that the partition ID of the request resource matches the partition ID of the entity.
2987         * <p>
2988         * If the request is for all partitions, this method does nothing. Otherwise, it checks that the resource type
2989         * and FHIR ID of the entity match those of the provided resource ID type, and that the partition IDs are equal.
2990         * If there is a mismatch, an {@link InvalidRequestException} is thrown.
2991         *
2992         * @param theResourceIdType     The resource ID type being processed
2993         * @param theRequestPartitionId The partition ID from the request
2994         * @param theEntity             The entity being updated
2995         * @throws InvalidRequestException if the partition IDs do not match for the same resource type and FHIR ID
2996         */
2997        @VisibleForTesting
2998        protected void validatePartitionIdMatch(
2999                        @Nonnull IIdType theResourceIdType,
3000                        @Nonnull RequestPartitionId theRequestPartitionId,
3001                        @Nonnull ResourceTable theEntity) {
3002                if (theRequestPartitionId.isAllPartitions()) {
3003                        return;
3004                }
3005
3006                boolean sameResType = Objects.equals(theEntity.getResourceType(), theResourceIdType.getResourceType());
3007                boolean sameFhirId = Objects.equals(theResourceIdType.getIdPart(), theEntity.getFhirId());
3008                boolean differentPartition =
3009                                !theRequestPartitionId.isPartition(theEntity.getPartitionId().getPartitionId());
3010
3011                if (sameResType && sameFhirId && differentPartition) {
3012                        String msg = getContext()
3013                                        .getLocalizer()
3014                                        .getMessageSanitized(
3015                                                        BaseHapiFhirDao.class,
3016                                                        "resourceTypeAndFhirIdConflictAcrossPartitions",
3017                                                        theEntity.getResourceType(),
3018                                                        theResourceIdType.getIdPart(),
3019                                                        theRequestPartitionId.getFirstPartitionNameOrNull());
3020                        throw new InvalidRequestException(Msg.code(2733) + msg);
3021                }
3022        }
3023
3024        /**
3025         * Method return type for {@link #reindexCorrectCurrentVersion(ResourceTable, ReindexParameters.ReindexSearchParametersEnum)}
3026         */
3027        private record CorrectCurrentVersionResponse(
3028                        boolean forceReindexSps, boolean forceReloadResourceEntity, ResourceHistoryTable currentVersion) {}
3029}