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