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