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