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