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