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