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