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