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