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