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