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