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