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