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