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