001package ca.uhn.fhir.jpa.dao;
002
003/*
004 * #%L
005 * HAPI FHIR JPA Server
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.batch2.api.IJobCoordinator;
024import ca.uhn.fhir.batch2.jobs.parameters.UrlPartitioner;
025import ca.uhn.fhir.batch2.jobs.reindex.ReindexAppCtx;
026import ca.uhn.fhir.batch2.jobs.reindex.ReindexJobParameters;
027import ca.uhn.fhir.batch2.model.JobInstanceStartRequest;
028import ca.uhn.fhir.context.FhirVersionEnum;
029import ca.uhn.fhir.context.RuntimeResourceDefinition;
030import ca.uhn.fhir.i18n.Msg;
031import ca.uhn.fhir.interceptor.api.HookParams;
032import ca.uhn.fhir.interceptor.api.Pointcut;
033import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails;
034import ca.uhn.fhir.interceptor.model.RequestPartitionId;
035import ca.uhn.fhir.jpa.api.config.DaoConfig;
036import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
037import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
038import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
039import ca.uhn.fhir.jpa.api.model.DeleteConflictList;
040import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome;
041import ca.uhn.fhir.jpa.api.model.ExpungeOptions;
042import ca.uhn.fhir.jpa.api.model.ExpungeOutcome;
043import ca.uhn.fhir.jpa.api.model.LazyDaoMethodOutcome;
044import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
045import ca.uhn.fhir.jpa.dao.index.IdHelperService;
046import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
047import ca.uhn.fhir.jpa.delete.DeleteConflictUtil;
048import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
049import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
050import ca.uhn.fhir.jpa.model.entity.BaseTag;
051import ca.uhn.fhir.jpa.model.entity.ForcedId;
052import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
053import ca.uhn.fhir.jpa.model.entity.ResourceTable;
054import ca.uhn.fhir.jpa.model.entity.TagDefinition;
055import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
056import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
057import ca.uhn.fhir.jpa.model.util.JpaConstants;
058import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
059import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
060import ca.uhn.fhir.jpa.patch.FhirPatch;
061import ca.uhn.fhir.jpa.patch.JsonPatchUtils;
062import ca.uhn.fhir.jpa.patch.XmlPatchUtils;
063import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider;
064import ca.uhn.fhir.jpa.search.cache.SearchCacheStatusEnum;
065import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
066import ca.uhn.fhir.jpa.searchparam.ResourceSearch;
067import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
068import ca.uhn.fhir.jpa.util.MemoryCacheService;
069import ca.uhn.fhir.model.api.IQueryParameterType;
070import ca.uhn.fhir.model.dstu2.resource.ListResource;
071import ca.uhn.fhir.model.primitive.IdDt;
072import ca.uhn.fhir.parser.DataFormatException;
073import ca.uhn.fhir.rest.api.CacheControlDirective;
074import ca.uhn.fhir.rest.api.Constants;
075import ca.uhn.fhir.rest.api.EncodingEnum;
076import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum;
077import ca.uhn.fhir.rest.api.MethodOutcome;
078import ca.uhn.fhir.rest.api.PatchTypeEnum;
079import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
080import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
081import ca.uhn.fhir.rest.api.ValidationModeEnum;
082import ca.uhn.fhir.rest.api.server.IBundleProvider;
083import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
084import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
085import ca.uhn.fhir.rest.api.server.RequestDetails;
086import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails;
087import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
088import ca.uhn.fhir.rest.api.server.storage.IDeleteExpungeJobSubmitter;
089import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
090import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
091import ca.uhn.fhir.rest.param.HasParam;
092import ca.uhn.fhir.rest.server.IPagingProvider;
093import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
094import ca.uhn.fhir.rest.server.RestfulServerUtils;
095import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
096import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
097import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
098import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
099import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
100import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException;
101import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
102import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
103import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
104import ca.uhn.fhir.util.ObjectUtil;
105import ca.uhn.fhir.util.OperationOutcomeUtil;
106import ca.uhn.fhir.util.ReflectionUtil;
107import ca.uhn.fhir.util.StopWatch;
108import ca.uhn.fhir.validation.FhirValidator;
109import ca.uhn.fhir.validation.IInstanceValidatorModule;
110import ca.uhn.fhir.validation.IValidationContext;
111import ca.uhn.fhir.validation.IValidatorModule;
112import ca.uhn.fhir.validation.ValidationOptions;
113import ca.uhn.fhir.validation.ValidationResult;
114import com.google.common.annotations.VisibleForTesting;
115import org.apache.commons.lang3.Validate;
116import org.hl7.fhir.instance.model.api.IBaseCoding;
117import org.hl7.fhir.instance.model.api.IBaseMetaType;
118import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
119import org.hl7.fhir.instance.model.api.IBaseParameters;
120import org.hl7.fhir.instance.model.api.IBaseResource;
121import org.hl7.fhir.instance.model.api.IIdType;
122import org.hl7.fhir.instance.model.api.IPrimitiveType;
123import org.springframework.beans.factory.annotation.Autowired;
124import org.springframework.beans.factory.annotation.Required;
125import org.springframework.transaction.PlatformTransactionManager;
126import org.springframework.transaction.annotation.Propagation;
127import org.springframework.transaction.annotation.Transactional;
128import org.springframework.transaction.support.TransactionSynchronization;
129import org.springframework.transaction.support.TransactionSynchronizationManager;
130import org.springframework.transaction.support.TransactionTemplate;
131
132import javax.annotation.Nonnull;
133import javax.annotation.Nullable;
134import javax.annotation.PostConstruct;
135import javax.persistence.NoResultException;
136import javax.persistence.TypedQuery;
137import javax.servlet.http.HttpServletResponse;
138import java.io.IOException;
139import java.util.ArrayList;
140import java.util.Collection;
141import java.util.Collections;
142import java.util.Date;
143import java.util.HashSet;
144import java.util.Iterator;
145import java.util.List;
146import java.util.Optional;
147import java.util.Set;
148import java.util.UUID;
149import java.util.function.Supplier;
150import java.util.stream.Collectors;
151
152import static org.apache.commons.lang3.StringUtils.isBlank;
153import static org.apache.commons.lang3.StringUtils.isNotBlank;
154
155public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends BaseHapiFhirDao<T> implements IFhirResourceDao<T> {
156
157        public static final String BASE_RESOURCE_NAME = "resource";
158        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirResourceDao.class);
159        @Autowired
160        protected PlatformTransactionManager myPlatformTransactionManager;
161        @Autowired(required = false)
162        protected IFulltextSearchSvc mySearchDao;
163        @Autowired
164        protected HapiTransactionService myTransactionService;
165        @Autowired
166        private MatchResourceUrlService myMatchResourceUrlService;
167        @Autowired
168        private SearchBuilderFactory mySearchBuilderFactory;
169        @Autowired
170        private DaoRegistry myDaoRegistry;
171        @Autowired
172        private IRequestPartitionHelperSvc myRequestPartitionHelperService;
173        @Autowired
174        private MatchUrlService myMatchUrlService;
175        @Autowired
176        private IDeleteExpungeJobSubmitter myDeleteExpungeJobSubmitter;
177        @Autowired
178        private IJobCoordinator myJobCoordinator;
179
180        private IInstanceValidatorModule myInstanceValidator;
181        private String myResourceName;
182        private Class<T> myResourceType;
183
184        @Autowired
185        private MemoryCacheService myMemoryCacheService;
186        private TransactionTemplate myTxTemplate;
187
188        @Autowired
189        private UrlPartitioner myUrlPartitioner;
190
191        @Override
192        public DaoMethodOutcome create(final T theResource) {
193                return create(theResource, null, true, new TransactionDetails(), null);
194        }
195
196        @Override
197        public DaoMethodOutcome create(final T theResource, RequestDetails theRequestDetails) {
198                return create(theResource, null, true, new TransactionDetails(), theRequestDetails);
199        }
200
201        @Override
202        public DaoMethodOutcome create(final T theResource, String theIfNoneExist) {
203                return create(theResource, theIfNoneExist, null);
204        }
205
206        @Override
207        public DaoMethodOutcome create(final T theResource, String theIfNoneExist, RequestDetails theRequestDetails) {
208                return create(theResource, theIfNoneExist, true, new TransactionDetails(), theRequestDetails);
209        }
210
211        @VisibleForTesting
212        public void setTransactionService(HapiTransactionService theTransactionService) {
213                myTransactionService = theTransactionService;
214        }
215
216        @Override
217        public DaoMethodOutcome create(T theResource, String theIfNoneExist, boolean thePerformIndexing, @Nonnull TransactionDetails theTransactionDetails, RequestDetails theRequestDetails) {
218                return myTransactionService.execute(theRequestDetails, theTransactionDetails, tx -> doCreateForPost(theResource, theIfNoneExist, thePerformIndexing, theTransactionDetails, theRequestDetails));
219        }
220
221        @VisibleForTesting
222        public void setRequestPartitionHelperService(IRequestPartitionHelperSvc theRequestPartitionHelperService) {
223                myRequestPartitionHelperService = theRequestPartitionHelperService;
224        }
225
226        /**
227         * Called for FHIR create (POST) operations
228         */
229        protected DaoMethodOutcome doCreateForPost(T theResource, String theIfNoneExist, boolean thePerformIndexing, TransactionDetails theTransactionDetails, RequestDetails theRequestDetails) {
230                if (theResource == null) {
231                        String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "missingBody");
232                        throw new InvalidRequestException(Msg.code(956) + msg);
233                }
234
235                if (isNotBlank(theResource.getIdElement().getIdPart())) {
236                        if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
237                                String message = getMessageSanitized("failedToCreateWithClientAssignedId", theResource.getIdElement().getIdPart());
238                                throw new InvalidRequestException(Msg.code(957) + message, createErrorOperationOutcome(message, "processing"));
239                        } else {
240                                // As of DSTU3, ID and version in the body should be ignored for a create/update
241                                theResource.setId("");
242                        }
243                }
244
245                if (getConfig().getResourceServerIdStrategy() == DaoConfig.IdStrategyEnum.UUID) {
246                        theResource.setId(UUID.randomUUID().toString());
247                        theResource.setUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED, Boolean.TRUE);
248                }
249
250                RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequestDetails, theResource, getResourceName());
251                return doCreateForPostOrPut(theResource, theIfNoneExist, thePerformIndexing, theTransactionDetails, theRequestDetails, requestPartitionId);
252        }
253
254        /**
255         * Called both for FHIR create (POST) operations (via {@link #doCreateForPost(IBaseResource, String, boolean, TransactionDetails, RequestDetails)}
256         * 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)}.
257         */
258        private DaoMethodOutcome doCreateForPostOrPut(T theResource, String theIfNoneExist, boolean thePerformIndexing, TransactionDetails theTransactionDetails, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) {
259                StopWatch w = new StopWatch();
260
261                preProcessResourceForStorage(theResource);
262                preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing);
263
264                ResourceTable entity = new ResourceTable();
265                entity.setResourceType(toResourceName(theResource));
266                entity.setPartitionId(myRequestPartitionHelperService.toStoragePartition(theRequestPartitionId));
267                entity.setCreatedByMatchUrl(theIfNoneExist);
268                entity.setVersion(1);
269
270                if (isNotBlank(theIfNoneExist)) {
271                        Set<ResourcePersistentId> match = myMatchResourceUrlService.processMatchUrl(theIfNoneExist, myResourceType, theTransactionDetails, theRequest);
272                        if (match.size() > 1) {
273                                String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "transactionOperationWithMultipleMatchFailure", "CREATE", theIfNoneExist, match.size());
274                                throw new PreconditionFailedException(Msg.code(958) + msg);
275                        } else if (match.size() == 1) {
276                                ResourcePersistentId pid = match.iterator().next();
277
278                                Supplier<LazyDaoMethodOutcome.EntityAndResource> entitySupplier = () -> {
279                                        return myTxTemplate.execute(tx -> {
280                                                ResourceTable foundEntity = myEntityManager.find(ResourceTable.class, pid.getId());
281                                                IBaseResource resource = toResource(foundEntity, false);
282                                                theResource.setId(resource.getIdElement().getValue());
283                                                return new LazyDaoMethodOutcome.EntityAndResource(foundEntity, resource);
284                                        });
285                                };
286
287                                Supplier<IIdType> idSupplier = () -> {
288                                        return myTxTemplate.execute(tx -> {
289                                                IIdType retVal = myIdHelperService.translatePidIdToForcedId(myFhirContext, myResourceName, pid);
290                                                if (!retVal.hasVersionIdPart()) {
291                                                        IIdType idWithVersion = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid.getIdAsLong());
292                                                        if (idWithVersion == null) {
293                                                                Long version = myResourceTableDao.findCurrentVersionByPid(pid.getIdAsLong());
294                                                                if (version != null) {
295                                                                        retVal = myFhirContext.getVersion().newIdType().setParts(retVal.getBaseUrl(), retVal.getResourceType(), retVal.getIdPart(), Long.toString(version));
296                                                                        myMemoryCacheService.putAfterCommit(MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid.getIdAsLong(), retVal);
297                                                                }
298                                                        } else {
299                                                                retVal = idWithVersion;
300                                                        }
301                                                }
302                                                return retVal;
303                                        });
304                                };
305
306                                return toMethodOutcomeLazy(theRequest, pid, entitySupplier, idSupplier).setCreated(false).setNop(true);
307                        }
308                }
309
310                String resourceIdBeforeStorage = theResource.getIdElement().getIdPart();
311                boolean resourceHadIdBeforeStorage = isNotBlank(resourceIdBeforeStorage);
312                boolean resourceIdWasServerAssigned = theResource.getUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED) == Boolean.TRUE;
313
314                HookParams hookParams;
315
316                // Notify interceptor for accepting/rejecting client assigned ids
317                if (!resourceIdWasServerAssigned && resourceHadIdBeforeStorage) {
318                        hookParams = new HookParams()
319                                .add(IBaseResource.class, theResource)
320                                .add(RequestDetails.class, theRequest);
321                        doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_CLIENT_ASSIGNED_ID, hookParams);
322                }
323
324                // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED
325                hookParams = new HookParams()
326                        .add(IBaseResource.class, theResource)
327                        .add(RequestDetails.class, theRequest)
328                        .addIfMatchesType(ServletRequestDetails.class, theRequest)
329                        .add(RequestPartitionId.class, theRequestPartitionId)
330                        .add(TransactionDetails.class, theTransactionDetails);
331                doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, hookParams);
332
333                if (resourceHadIdBeforeStorage && !resourceIdWasServerAssigned) {
334                        validateResourceIdCreation(theResource, theRequest);
335                }
336
337                // Perform actual DB update
338                // this call will also update the metadata
339                ResourceTable updatedEntity = updateEntity(theRequest, theResource, entity, null, thePerformIndexing, false, theTransactionDetails, false, thePerformIndexing);
340
341                // Store the resource forced ID if necessary
342                ResourcePersistentId persistentId = new ResourcePersistentId(updatedEntity.getResourceId());
343                if (resourceHadIdBeforeStorage) {
344                        if (resourceIdWasServerAssigned) {
345                                boolean createForPureNumericIds = true;
346                                createForcedIdIfNeeded(entity, resourceIdBeforeStorage, createForPureNumericIds);
347                        } else {
348                                boolean createForPureNumericIds = getConfig().getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ALPHANUMERIC;
349                                createForcedIdIfNeeded(entity, resourceIdBeforeStorage, createForPureNumericIds);
350                        }
351                } else {
352                        switch (getConfig().getResourceClientIdStrategy()) {
353                                case NOT_ALLOWED:
354                                case ALPHANUMERIC:
355                                        break;
356                                case ANY:
357                                        boolean createForPureNumericIds = true;
358                                        createForcedIdIfNeeded(updatedEntity, theResource.getIdElement().getIdPart(), createForPureNumericIds);
359                                        // for client ID mode ANY, we will always have a forced ID. If we ever
360                                        // stop populating the transient forced ID be warned that we use it
361                                        // (and expect it to be set correctly) farther below.
362                                        assert updatedEntity.getTransientForcedId() != null;
363                                        break;
364                        }
365                }
366
367                // Populate the resource with its actual final stored ID from the entity
368                theResource.setId(entity.getIdDt());
369
370                // Pre-cache the resource ID
371                persistentId.setAssociatedResourceId(entity.getIdType(myFhirContext));
372                myIdHelperService.addResolvedPidToForcedId(persistentId, theRequestPartitionId, getResourceName(), entity.getTransientForcedId(), null);
373                theTransactionDetails.addResolvedResourceId(persistentId.getAssociatedResourceId(), persistentId);
374
375                // Pre-cache the match URL
376                if (theIfNoneExist != null) {
377                        myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, getResourceName(), theIfNoneExist, persistentId);
378                }
379
380                // Update the version/last updated in the resource so that interceptors get
381                // the correct version
382                // TODO - the above updateEntity calls updateResourceMetadata
383                //              Maybe we don't need this call here?
384                updateResourceMetadata(entity, theResource);
385
386                // Populate the PID in the resource so it is available to hooks
387                addPidToResource(entity, theResource);
388
389                // Notify JPA interceptors
390                if (!updatedEntity.isUnchangedInCurrentOperation()) {
391                        hookParams = new HookParams()
392                                .add(IBaseResource.class, theResource)
393                                .add(RequestDetails.class, theRequest)
394                                .addIfMatchesType(ServletRequestDetails.class, theRequest)
395                                .add(TransactionDetails.class, theTransactionDetails)
396                                .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
397                        doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, hookParams);
398                }
399
400                DaoMethodOutcome outcome = toMethodOutcome(theRequest, entity, theResource).setCreated(true);
401                if (!thePerformIndexing) {
402                        outcome.setId(theResource.getIdElement());
403                }
404
405                String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "successfulCreate", outcome.getId(), w.getMillisAndRestart());
406                outcome.setOperationOutcome(createInfoOperationOutcome(msg));
407
408                ourLog.debug(msg);
409                return outcome;
410        }
411
412        private void createForcedIdIfNeeded(ResourceTable theEntity, String theResourceId, boolean theCreateForPureNumericIds) {
413                if (isNotBlank(theResourceId) && theEntity.getForcedId() == null) {
414                        if (theCreateForPureNumericIds || !IdHelperService.isValidPid(theResourceId)) {
415                                ForcedId forcedId = new ForcedId();
416                                forcedId.setResourceType(theEntity.getResourceType());
417                                forcedId.setForcedId(theResourceId);
418                                forcedId.setResource(theEntity);
419                                forcedId.setPartitionId(theEntity.getPartitionId());
420
421                                /*
422                                 * As of Hibernate 5.6.2, assigning the forced ID to the
423                                 * resource table causes an extra update to happen, even
424                                 * though the ResourceTable entity isn't actually changed
425                                 * (there is a @OneToOne reference on ResourceTable to the
426                                 * ForcedId table, but the actual column is on the ForcedId
427                                 * table so it doesn't actually make sense to update the table
428                                 * when this is set). But to work around that we avoid
429                                 * actually assigning ResourceTable#myForcedId here.
430                                 *
431                                 * It's conceivable they may fix this in the future, or
432                                 * they may not.
433                                 *
434                                 * If you want to try assigning the forced it to the resource
435                                 * entity (by calling ResourceTable#setForcedId) try running
436                                 * the tests FhirResourceDaoR4QueryCountTest to verify that
437                                 * nothing has broken as a result.
438                                 * JA 20220121
439                                 */
440                                theEntity.setTransientForcedId(forcedId.getForcedId());
441                                myForcedIdDao.save(forcedId);
442                        }
443                }
444        }
445
446        void validateResourceIdCreation(T theResource, RequestDetails theRequest) {
447                DaoConfig.ClientIdStrategyEnum strategy = getConfig().getResourceClientIdStrategy();
448
449                if (strategy == DaoConfig.ClientIdStrategyEnum.NOT_ALLOWED) {
450                        if (!isSystemRequest(theRequest)) {
451                                throw new ResourceNotFoundException(Msg.code(959) + getMessageSanitized("failedToCreateWithClientAssignedIdNotAllowed", theResource.getIdElement().getIdPart()));
452                        }
453                }
454
455                if (strategy == DaoConfig.ClientIdStrategyEnum.ALPHANUMERIC) {
456                        if (theResource.getIdElement().isIdPartValidLong()) {
457                                throw new InvalidRequestException(Msg.code(960) + getMessageSanitized("failedToCreateWithClientAssignedNumericId", theResource.getIdElement().getIdPart()));
458                        }
459                }
460        }
461
462        protected String getMessageSanitized(String theKey, String theIdPart) {
463                return getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, theKey, theIdPart);
464        }
465
466        private boolean isSystemRequest(RequestDetails theRequest) {
467                return theRequest instanceof SystemRequestDetails;
468        }
469
470        private IInstanceValidatorModule getInstanceValidator() {
471                return myInstanceValidator;
472        }
473
474        @Override
475        public DaoMethodOutcome delete(IIdType theId) {
476                return delete(theId, null);
477        }
478
479        @Override
480        public DaoMethodOutcome delete(IIdType theId, RequestDetails theRequestDetails) {
481                TransactionDetails transactionDetails = new TransactionDetails();
482
483                validateIdPresentForDelete(theId);
484                validateDeleteEnabled();
485
486                return myTransactionService.execute(theRequestDetails, transactionDetails, tx -> {
487                        DeleteConflictList deleteConflicts = new DeleteConflictList();
488                        if (isNotBlank(theId.getValue())) {
489                                deleteConflicts.setResourceIdMarkedForDeletion(theId);
490                        }
491
492                        StopWatch w = new StopWatch();
493
494                        DaoMethodOutcome retVal = delete(theId, deleteConflicts, theRequestDetails, transactionDetails);
495
496                        DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts);
497
498                        ourLog.debug("Processed delete on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
499                        return retVal;
500                });
501        }
502
503        /**
504         * Creates a base method outcome for a delete request for the provided ID.
505         * <p>
506         * Additional information may be set on the outcome.
507         *
508         * @param theId - the id of the object being deleted. Eg: Patient/123
509         */
510        private DaoMethodOutcome createMethodOutcomeForDelete(String theId, String theKey) {
511                DaoMethodOutcome outcome = new DaoMethodOutcome();
512
513                IIdType id = getContext().getVersion().newIdType();
514                id.setValue(theId);
515                outcome.setId(id);
516
517                IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext());
518                String message = getContext().getLocalizer().getMessage(BaseStorageDao.class, theKey, id);
519                String severity = "information";
520                String code = "informational";
521                OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
522                outcome.setOperationOutcome(oo);
523
524                return outcome;
525        }
526
527        @Override
528        public DaoMethodOutcome delete(IIdType theId,
529                                                                                         DeleteConflictList theDeleteConflicts,
530                                                                                         RequestDetails theRequestDetails,
531                                                                                         @Nonnull TransactionDetails theTransactionDetails) {
532                validateIdPresentForDelete(theId);
533                validateDeleteEnabled();
534
535                final ResourceTable entity;
536                try {
537                        entity = readEntityLatestVersion(theId, theRequestDetails, theTransactionDetails);
538                } catch (ResourceNotFoundException ex) {
539                        // we don't want to throw 404s.
540                        // if not found, return an outcome anyways.
541                        // Because no object actually existed, we'll
542                        // just set the id and nothing else
543                        DaoMethodOutcome outcome = createMethodOutcomeForDelete(theId.getValue(), "deleteResourceNotExisting");
544                        return outcome;
545                }
546
547                if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) {
548                        throw new ResourceVersionConflictException(Msg.code(961) + "Trying to delete " + theId + " but this is not the current version");
549                }
550
551                // Don't delete again if it's already deleted
552                if (isDeleted(entity)) {
553                        DaoMethodOutcome outcome = createMethodOutcomeForDelete(entity.getIdDt().getValue(), "deleteResourceAlreadyDeleted");
554
555                        // used to exist, so we'll set the persistent id
556                        outcome.setPersistentId(new ResourcePersistentId(entity.getResourceId()));
557                        outcome.setEntity(entity);
558
559                        return outcome;
560                }
561
562                StopWatch w = new StopWatch();
563
564                T resourceToDelete = toResource(myResourceType, entity, null, false);
565                theDeleteConflicts.setResourceIdMarkedForDeletion(theId);
566
567                // Notify IServerOperationInterceptors about pre-action call
568                HookParams hook = new HookParams()
569                        .add(IBaseResource.class, resourceToDelete)
570                        .add(RequestDetails.class, theRequestDetails)
571                        .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
572                        .add(TransactionDetails.class, theTransactionDetails);
573                doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hook);
574
575                myDeleteConflictService.validateOkToDelete(theDeleteConflicts, entity, false, theRequestDetails, theTransactionDetails);
576
577                preDelete(resourceToDelete, entity, theRequestDetails);
578
579                ResourceTable savedEntity = updateEntityForDelete(theRequestDetails, theTransactionDetails, entity);
580                resourceToDelete.setId(entity.getIdDt());
581
582                // Notify JPA interceptors
583                HookParams hookParams = new HookParams()
584                        .add(IBaseResource.class, resourceToDelete)
585                        .add(RequestDetails.class, theRequestDetails)
586                        .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
587                        .add(TransactionDetails.class, theTransactionDetails)
588                        .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED));
589
590
591                doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
592
593                DaoMethodOutcome outcome = toMethodOutcome(theRequestDetails, savedEntity, resourceToDelete).setCreated(true);
594
595                IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext());
596                String message = getContext().getLocalizer().getMessage(BaseStorageDao.class, "successfulDeletes", 1, w.getMillis());
597                String severity = "information";
598                String code = "informational";
599                OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
600                outcome.setOperationOutcome(oo);
601
602                return outcome;
603        }
604
605        @Override
606        public DeleteMethodOutcome deleteByUrl(String theUrl, RequestDetails theRequest) {
607                validateDeleteEnabled();
608
609                TransactionDetails transactionDetails = new TransactionDetails();
610                ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(theUrl);
611
612                if (resourceSearch.isDeleteExpunge()) {
613                        return deleteExpunge(theUrl, theRequest);
614                }
615
616                return myTransactionService.execute(theRequest, transactionDetails, tx -> {
617                        DeleteConflictList deleteConflicts = new DeleteConflictList();
618                        DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequest);
619                        DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts);
620                        return outcome;
621                });
622        }
623
624        /**
625         * This method gets called by {@link #deleteByUrl(String, RequestDetails)} as well as by
626         * transaction processors
627         */
628        @Override
629        public DeleteMethodOutcome deleteByUrl(String theUrl, DeleteConflictList deleteConflicts, RequestDetails theRequestDetails) {
630                validateDeleteEnabled();
631                TransactionDetails transactionDetails = new TransactionDetails();
632
633                return myTransactionService.execute(theRequestDetails, transactionDetails, tx -> doDeleteByUrl(theUrl, deleteConflicts, theRequestDetails));
634        }
635
636        @Nonnull
637        private DeleteMethodOutcome doDeleteByUrl(String theUrl, DeleteConflictList deleteConflicts, RequestDetails theRequest) {
638                ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(theUrl);
639                SearchParameterMap paramMap = resourceSearch.getSearchParameterMap();
640                paramMap.setLoadSynchronous(true);
641
642                Set<ResourcePersistentId> resourceIds = myMatchResourceUrlService.search(paramMap, myResourceType, theRequest, null);
643
644                if (resourceIds.size() > 1) {
645                        if (!getConfig().isAllowMultipleDelete()) {
646                                throw new PreconditionFailedException(Msg.code(962) + getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "transactionOperationWithMultipleMatchFailure", "DELETE", theUrl, resourceIds.size()));
647                        }
648                }
649
650                return deletePidList(theUrl, resourceIds, deleteConflicts, theRequest);
651        }
652
653        private DeleteMethodOutcome deleteExpunge(String theUrl, RequestDetails theRequest) {
654                if (!getConfig().canDeleteExpunge()) {
655                        throw new MethodNotAllowedException(Msg.code(963) + "_expunge is not enabled on this server: " + getConfig().cannotDeleteExpungeReason());
656                }
657
658                if (theUrl.contains(Constants.PARAMETER_CASCADE_DELETE) || (theRequest.getHeader(Constants.HEADER_CASCADE) != null && theRequest.getHeader(Constants.HEADER_CASCADE).equals(Constants.CASCADE_DELETE))) {
659                        throw new InvalidRequestException(Msg.code(964) + "_expunge cannot be used with _cascade");
660                }
661
662                List<String> urlsToDeleteExpunge = Collections.singletonList(theUrl);
663                try {
664                        String jobId = myDeleteExpungeJobSubmitter.submitJob(getConfig().getExpungeBatchSize(), urlsToDeleteExpunge, theRequest);
665                        return new DeleteMethodOutcome(createInfoOperationOutcome("Delete job submitted with id " + jobId));
666                } catch (InvalidRequestException e) {
667                        throw new InvalidRequestException(Msg.code(965) + "Invalid Delete Expunge Request: " + e.getMessage(), e);
668                }
669        }
670
671        @Nonnull
672        @Override
673        public DeleteMethodOutcome deletePidList(String theUrl, Collection<ResourcePersistentId> theResourceIds, DeleteConflictList theDeleteConflicts, RequestDetails theRequest) {
674                StopWatch w = new StopWatch();
675                TransactionDetails transactionDetails = new TransactionDetails();
676                List<ResourceTable> deletedResources = new ArrayList<>();
677                for (ResourcePersistentId pid : theResourceIds) {
678                        ResourceTable entity = myEntityManager.find(ResourceTable.class, pid.getIdAsLong());
679                        deletedResources.add(entity);
680
681                        T resourceToDelete = toResource(myResourceType, entity, null, false);
682
683                        // Notify IServerOperationInterceptors about pre-action call
684                        HookParams hooks = new HookParams()
685                                .add(IBaseResource.class, resourceToDelete)
686                                .add(RequestDetails.class, theRequest)
687                                .addIfMatchesType(ServletRequestDetails.class, theRequest)
688                                .add(TransactionDetails.class, transactionDetails);
689                        doCallHooks(transactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks);
690
691                        myDeleteConflictService.validateOkToDelete(theDeleteConflicts, entity, false, theRequest, transactionDetails);
692
693                        // Perform delete
694
695                        updateEntityForDelete(theRequest, transactionDetails, entity);
696                        resourceToDelete.setId(entity.getIdDt());
697
698                        // Notify JPA interceptors
699                        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
700                                @Override
701                                public void beforeCommit(boolean readOnly) {
702                                        HookParams hookParams = new HookParams()
703                                                .add(IBaseResource.class, resourceToDelete)
704                                                .add(RequestDetails.class, theRequest)
705                                                .addIfMatchesType(ServletRequestDetails.class, theRequest)
706                                                .add(TransactionDetails.class, transactionDetails)
707                                                .add(InterceptorInvocationTimingEnum.class, transactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED));
708                                        doCallHooks(transactionDetails, theRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams);
709                                }
710                        });
711                }
712
713                IBaseOperationOutcome oo;
714                if (deletedResources.isEmpty()) {
715                        oo = OperationOutcomeUtil.newInstance(getContext());
716                        String message = getMessageSanitized("unableToDeleteNotFound", theUrl);
717                        String severity = "warning";
718                        String code = "not-found";
719                        OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
720                } else {
721                        oo = OperationOutcomeUtil.newInstance(getContext());
722                        String message = getContext().getLocalizer().getMessage(BaseStorageDao.class, "successfulDeletes", deletedResources.size(), w.getMillis());
723                        String severity = "information";
724                        String code = "informational";
725                        OperationOutcomeUtil.addIssue(getContext(), oo, severity, message, null, code);
726                }
727
728                ourLog.debug("Processed delete on {} (matched {} resource(s)) in {}ms", theUrl, deletedResources.size(), w.getMillis());
729
730                DeleteMethodOutcome retVal = new DeleteMethodOutcome();
731                retVal.setDeletedEntities(deletedResources);
732                retVal.setOperationOutcome(oo);
733                return retVal;
734        }
735
736        private void validateDeleteEnabled() {
737                if (!getConfig().isDeleteEnabled()) {
738                        String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "deleteBlockedBecauseDisabled");
739                        throw new PreconditionFailedException(Msg.code(966) + msg);
740                }
741        }
742
743        private void validateIdPresentForDelete(IIdType theId) {
744                if (theId == null || !theId.hasIdPart()) {
745                        throw new InvalidRequestException(Msg.code(967) + "Can not perform delete, no ID provided");
746                }
747        }
748
749        @PostConstruct
750        public void detectSearchDaoDisabled() {
751                if (mySearchDao != null && mySearchDao.isDisabled()) {
752                        mySearchDao = null;
753                }
754        }
755
756        private <MT extends IBaseMetaType> void doMetaAdd(MT theMetaAdd, BaseHasResource theEntity, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) {
757                IBaseResource oldVersion = toResource(theEntity, false);
758
759                List<TagDefinition> tags = toTagList(theMetaAdd);
760                for (TagDefinition nextDef : tags) {
761
762                        boolean hasTag = false;
763                        for (BaseTag next : new ArrayList<>(theEntity.getTags())) {
764                                if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) &&
765                                        ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) &&
766                                        ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) {
767                                        hasTag = true;
768                                        break;
769                                }
770                        }
771
772                        if (!hasTag) {
773                                theEntity.setHasTags(true);
774
775                                TagDefinition def = getTagOrNull(theTransactionDetails, nextDef.getTagType(), nextDef.getSystem(), nextDef.getCode(), nextDef.getDisplay());
776                                if (def != null) {
777                                        BaseTag newEntity = theEntity.addTag(def);
778                                        if (newEntity.getTagId() == null) {
779                                                myEntityManager.persist(newEntity);
780                                        }
781                                }
782                        }
783                }
784
785                validateMetaCount(theEntity.getTags().size());
786
787                myEntityManager.merge(theEntity);
788
789                // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED
790                IBaseResource newVersion = toResource(theEntity, false);
791                HookParams preStorageParams = new HookParams()
792                        .add(IBaseResource.class, oldVersion)
793                        .add(IBaseResource.class, newVersion)
794                        .add(RequestDetails.class, theRequestDetails)
795                        .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
796                        .add(TransactionDetails.class, theTransactionDetails);
797                myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams);
798
799                // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED
800                HookParams preCommitParams = new HookParams()
801                        .add(IBaseResource.class, oldVersion)
802                        .add(IBaseResource.class, newVersion)
803                        .add(RequestDetails.class, theRequestDetails)
804                        .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
805                        .add(TransactionDetails.class, theTransactionDetails)
806                        .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED));
807                myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams);
808
809        }
810
811        private <MT extends IBaseMetaType> void doMetaDelete(MT theMetaDel, BaseHasResource theEntity, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) {
812
813                // todo mb update hibernate search index if we are storing resources - it assumes inline tags.
814                IBaseResource oldVersion = toResource(theEntity, false);
815
816                List<TagDefinition> tags = toTagList(theMetaDel);
817
818                for (TagDefinition nextDef : tags) {
819                        for (BaseTag next : new ArrayList<BaseTag>(theEntity.getTags())) {
820                                if (ObjectUtil.equals(next.getTag().getTagType(), nextDef.getTagType()) &&
821                                        ObjectUtil.equals(next.getTag().getSystem(), nextDef.getSystem()) &&
822                                        ObjectUtil.equals(next.getTag().getCode(), nextDef.getCode())) {
823                                        myEntityManager.remove(next);
824                                        theEntity.getTags().remove(next);
825                                }
826                        }
827                }
828
829                if (theEntity.getTags().isEmpty()) {
830                        theEntity.setHasTags(false);
831                }
832
833                theEntity = myEntityManager.merge(theEntity);
834
835                // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED
836                IBaseResource newVersion = toResource(theEntity, false);
837                HookParams preStorageParams = new HookParams()
838                        .add(IBaseResource.class, oldVersion)
839                        .add(IBaseResource.class, newVersion)
840                        .add(RequestDetails.class, theRequestDetails)
841                        .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
842                        .add(TransactionDetails.class, theTransactionDetails);
843                myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams);
844
845                HookParams preCommitParams = new HookParams()
846                        .add(IBaseResource.class, oldVersion)
847                        .add(IBaseResource.class, newVersion)
848                        .add(RequestDetails.class, theRequestDetails)
849                        .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
850                        .add(TransactionDetails.class, theTransactionDetails)
851                        .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED));
852
853                myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams);
854
855        }
856
857        private void validateExpungeEnabled() {
858                if (!getConfig().isExpungeEnabled()) {
859                        throw new MethodNotAllowedException(Msg.code(968) + "$expunge is not enabled on this server");
860                }
861        }
862
863        @Override
864        @Transactional(propagation = Propagation.NEVER)
865        public ExpungeOutcome expunge(IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest) {
866                validateExpungeEnabled();
867                return forceExpungeInExistingTransaction(theId, theExpungeOptions, theRequest);
868        }
869
870        @Override
871        public ExpungeOutcome forceExpungeInExistingTransaction(IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest) {
872                TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
873
874                BaseHasResource entity = txTemplate.execute(t -> readEntity(theId, theRequest));
875                Validate.notNull(entity, "Resource with ID %s not found in database", theId);
876
877                if (theId.hasVersionIdPart()) {
878                        BaseHasResource currentVersion;
879                        currentVersion = txTemplate.execute(t -> readEntity(theId.toVersionless(), theRequest));
880                        Validate.notNull(currentVersion, "Current version of resource with ID %s not found in database", theId.toVersionless());
881
882                        if (entity.getVersion() == currentVersion.getVersion()) {
883                                throw new PreconditionFailedException(Msg.code(969) + "Can not perform version-specific expunge of resource " + theId.toUnqualified().getValue() + " as this is the current version");
884                        }
885
886                        return myExpungeService.expunge(getResourceName(), new ResourcePersistentId(entity.getResourceId(), entity.getVersion()), theExpungeOptions, theRequest);
887                }
888
889                return myExpungeService.expunge(getResourceName(), new ResourcePersistentId(entity.getResourceId()), theExpungeOptions, theRequest);
890        }
891
892        @Override
893        @Transactional(propagation = Propagation.NEVER)
894        public ExpungeOutcome expunge(ExpungeOptions theExpungeOptions, RequestDetails theRequestDetails) {
895                ourLog.info("Beginning TYPE[{}] expunge operation", getResourceName());
896
897                return myExpungeService.expunge(getResourceName(), null, theExpungeOptions, theRequestDetails);
898        }
899
900        @Override
901        public String getResourceName() {
902                return myResourceName;
903        }
904
905        @Override
906        public Class<T> getResourceType() {
907                return myResourceType;
908        }
909
910        @SuppressWarnings("unchecked")
911        @Required
912        public void setResourceType(Class<? extends IBaseResource> theTableType) {
913                myResourceType = (Class<T>) theTableType;
914        }
915
916        @Override
917        @Transactional
918        public IBundleProvider history(Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequestDetails) {
919                StopWatch w = new StopWatch();
920                IBundleProvider retVal = super.history(theRequestDetails, myResourceName, null, theSince, theUntil, theOffset);
921                ourLog.debug("Processed history on {} in {}ms", myResourceName, w.getMillisAndRestart());
922                return retVal;
923        }
924
925        @Override
926        @Transactional
927        public IBundleProvider history(final IIdType theId, final Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequest) {
928                StopWatch w = new StopWatch();
929
930                IIdType id = theId.withResourceType(myResourceName).toUnqualifiedVersionless();
931                BaseHasResource entity = readEntity(id, theRequest);
932
933                IBundleProvider retVal = super.history(theRequest, myResourceName, entity.getId(), theSince, theUntil, theOffset);
934
935                ourLog.debug("Processed history on {} in {}ms", id, w.getMillisAndRestart());
936                return retVal;
937        }
938
939        protected boolean isPagingProviderDatabaseBacked(RequestDetails theRequestDetails) {
940                if (theRequestDetails == null || theRequestDetails.getServer() == null) {
941                        return false;
942                }
943                IRestfulServerDefaults server = theRequestDetails.getServer();
944                IPagingProvider pagingProvider = server.getPagingProvider();
945                return pagingProvider != null;
946        }
947
948        protected void requestReindexForRelatedResources(Boolean theCurrentlyReindexing, List<String> theBase, RequestDetails theRequestDetails) {
949                // Avoid endless loops
950                if (Boolean.TRUE.equals(theCurrentlyReindexing) || shouldSkipReindex(theRequestDetails)) {
951                        return;
952                }
953
954                if (getConfig().isMarkResourcesForReindexingUponSearchParameterChange()) {
955
956                        ReindexJobParameters params = new ReindexJobParameters();
957
958                        if (!isCommonSearchParam(theBase)) {
959                                addAllResourcesTypesToReindex(theBase, theRequestDetails, params);
960                        }
961
962                        ReadPartitionIdRequestDetails details = new ReadPartitionIdRequestDetails(null, RestOperationTypeEnum.EXTENDED_OPERATION_SERVER, null, null, null);
963                        RequestPartitionId requestPartition = myRequestPartitionHelperService.determineReadPartitionForRequest(theRequestDetails, null, details);
964                        params.setRequestPartitionId(requestPartition);
965
966                        JobInstanceStartRequest request = new JobInstanceStartRequest();
967                        request.setJobDefinitionId(ReindexAppCtx.JOB_REINDEX);
968                        request.setParameters(params);
969                        myJobCoordinator.startInstance(request);
970
971                        ourLog.debug("Started reindex job with parameters {}", params);
972
973                }
974
975                mySearchParamRegistry.requestRefresh();
976        }
977
978        private boolean shouldSkipReindex(RequestDetails theRequestDetails) {
979                if (theRequestDetails == null) {
980                        return false;
981                }
982                Object shouldSkip = theRequestDetails.getUserData().getOrDefault(JpaConstants.SKIP_REINDEX_ON_UPDATE, false);
983                return Boolean.parseBoolean(shouldSkip.toString());
984        }
985
986        private void addAllResourcesTypesToReindex(List<String> theBase, RequestDetails theRequestDetails, ReindexJobParameters params) {
987                theBase
988                        .stream()
989                        .map(t -> t + "?")
990                        .map(url -> myUrlPartitioner.partitionUrl(url, theRequestDetails))
991                        .forEach(params::addPartitionedUrl);
992        }
993
994        private boolean isCommonSearchParam(List<String> theBase) {
995                // If the base contains the special resource "Resource", this is a common SP that applies to all resources
996                return theBase.stream()
997                        .map(String::toLowerCase)
998                        .anyMatch(BASE_RESOURCE_NAME::equals);
999        }
1000
1001        @Override
1002        @Transactional
1003        public <MT extends IBaseMetaType> MT metaAddOperation(IIdType theResourceId, MT theMetaAdd, RequestDetails theRequest) {
1004                TransactionDetails transactionDetails = new TransactionDetails();
1005
1006                StopWatch w = new StopWatch();
1007                BaseHasResource entity = readEntity(theResourceId, theRequest);
1008                if (entity == null) {
1009                        throw new ResourceNotFoundException(Msg.code(1993) + theResourceId);
1010                }
1011
1012                ResourceTable latestVersion = readEntityLatestVersion(theResourceId, theRequest, transactionDetails);
1013                if (latestVersion.getVersion() != entity.getVersion()) {
1014                        doMetaAdd(theMetaAdd, entity, theRequest, transactionDetails);
1015                } else {
1016                        doMetaAdd(theMetaAdd, latestVersion, theRequest, transactionDetails);
1017
1018                        // Also update history entry
1019                        ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(entity.getId(), entity.getVersion());
1020                        doMetaAdd(theMetaAdd, history, theRequest, transactionDetails);
1021                }
1022
1023                ourLog.debug("Processed metaAddOperation on {} in {}ms", theResourceId, w.getMillisAndRestart());
1024
1025                @SuppressWarnings("unchecked")
1026                MT retVal = (MT) metaGetOperation(theMetaAdd.getClass(), theResourceId, theRequest);
1027                return retVal;
1028        }
1029
1030        @Override
1031        @Transactional
1032        public <MT extends IBaseMetaType> MT metaDeleteOperation(IIdType theResourceId, MT theMetaDel, RequestDetails theRequest) {
1033                TransactionDetails transactionDetails = new TransactionDetails();
1034
1035                StopWatch w = new StopWatch();
1036                BaseHasResource entity = readEntity(theResourceId, theRequest);
1037                if (entity == null) {
1038                        throw new ResourceNotFoundException(Msg.code(1994) + theResourceId);
1039                }
1040
1041                ResourceTable latestVersion = readEntityLatestVersion(theResourceId, theRequest, transactionDetails);
1042                if (latestVersion.getVersion() != entity.getVersion()) {
1043                        doMetaDelete(theMetaDel, entity, theRequest, transactionDetails);
1044                } else {
1045                        doMetaDelete(theMetaDel, latestVersion, theRequest, transactionDetails);
1046
1047                        // Also update history entry
1048                        ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(entity.getId(), entity.getVersion());
1049                        doMetaDelete(theMetaDel, history, theRequest, transactionDetails);
1050                }
1051
1052                ourLog.debug("Processed metaDeleteOperation on {} in {}ms", theResourceId.getValue(), w.getMillisAndRestart());
1053
1054                @SuppressWarnings("unchecked")
1055                MT retVal = (MT) metaGetOperation(theMetaDel.getClass(), theResourceId, theRequest);
1056                return retVal;
1057        }
1058
1059        @Override
1060        @Transactional
1061        public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, IIdType theId, RequestDetails theRequest) {
1062                Set<TagDefinition> tagDefs = new HashSet<>();
1063                BaseHasResource entity = readEntity(theId, theRequest);
1064                for (BaseTag next : entity.getTags()) {
1065                        tagDefs.add(next.getTag());
1066                }
1067                MT retVal = toMetaDt(theType, tagDefs);
1068
1069                retVal.setLastUpdated(entity.getUpdatedDate());
1070                retVal.setVersionId(Long.toString(entity.getVersion()));
1071
1072                return retVal;
1073        }
1074
1075        @Override
1076        @Transactional
1077        public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails) {
1078                String sql = "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t WHERE t.myResourceType = :res_type)";
1079                TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class);
1080                q.setParameter("res_type", myResourceName);
1081                List<TagDefinition> tagDefinitions = q.getResultList();
1082
1083                return toMetaDt(theType, tagDefinitions);
1084        }
1085
1086        @Override
1087        public DaoMethodOutcome patch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, IBaseParameters theFhirPatchBody, RequestDetails theRequest) {
1088                TransactionDetails transactionDetails = new TransactionDetails();
1089                return myTransactionService.execute(theRequest, transactionDetails, tx -> doPatch(theId, theConditionalUrl, thePatchType, thePatchBody, theFhirPatchBody, theRequest, transactionDetails));
1090        }
1091
1092        private DaoMethodOutcome doPatch(IIdType theId, String theConditionalUrl, PatchTypeEnum thePatchType, String thePatchBody, IBaseParameters theFhirPatchBody, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
1093                ResourceTable entityToUpdate;
1094                if (isNotBlank(theConditionalUrl)) {
1095
1096                        Set<ResourcePersistentId> match = myMatchResourceUrlService.processMatchUrl(theConditionalUrl, myResourceType, theTransactionDetails, theRequest);
1097                        if (match.size() > 1) {
1098                                String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "transactionOperationWithMultipleMatchFailure", "PATCH", theConditionalUrl, match.size());
1099                                throw new PreconditionFailedException(Msg.code(972) + msg);
1100                        } else if (match.size() == 1) {
1101                                ResourcePersistentId pid = match.iterator().next();
1102                                entityToUpdate = myEntityManager.find(ResourceTable.class, pid.getId());
1103                        } else {
1104                                String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidMatchUrlNoMatches", theConditionalUrl);
1105                                throw new ResourceNotFoundException(Msg.code(973) + msg);
1106                        }
1107
1108                } else {
1109                        entityToUpdate = readEntityLatestVersion(theId, theRequest, theTransactionDetails);
1110                        if (theId.hasVersionIdPart()) {
1111                                if (theId.getVersionIdPartAsLong() != entityToUpdate.getVersion()) {
1112                                        throw new ResourceVersionConflictException(Msg.code(974) + "Version " + theId.getVersionIdPart() + " is not the most recent version of this resource, unable to apply patch");
1113                                }
1114                        }
1115                }
1116
1117                validateResourceType(entityToUpdate);
1118
1119                if (isDeleted(entityToUpdate)) {
1120                        throw createResourceGoneException(entityToUpdate);
1121                }
1122
1123                IBaseResource resourceToUpdate = toResource(entityToUpdate, false);
1124                IBaseResource destination;
1125                switch (thePatchType) {
1126                        case JSON_PATCH:
1127                                destination = JsonPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody);
1128                                break;
1129                        case XML_PATCH:
1130                                destination = XmlPatchUtils.apply(getContext(), resourceToUpdate, thePatchBody);
1131                                break;
1132                        case FHIR_PATCH_XML:
1133                        case FHIR_PATCH_JSON:
1134                        default:
1135                                IBaseParameters fhirPatchJson = theFhirPatchBody;
1136                                new FhirPatch(getContext()).apply(resourceToUpdate, fhirPatchJson);
1137                                destination = resourceToUpdate;
1138                                break;
1139                }
1140
1141                @SuppressWarnings("unchecked")
1142                T destinationCasted = (T) destination;
1143                return update(destinationCasted, null, true, theRequest);
1144        }
1145
1146        private boolean isDeleted(BaseHasResource entityToUpdate) {
1147                return entityToUpdate.getDeleted() != null;
1148        }
1149
1150        @PostConstruct
1151        @Override
1152        public void start() {
1153                assert getConfig() != null;
1154
1155                ourLog.debug("Starting resource DAO for type: {}", getResourceName());
1156                myInstanceValidator = getApplicationContext().getBean(IInstanceValidatorModule.class);
1157                myTxTemplate = new TransactionTemplate(myPlatformTransactionManager);
1158                super.start();
1159        }
1160
1161        @PostConstruct
1162        public void postConstruct() {
1163                RuntimeResourceDefinition def = getContext().getResourceDefinition(myResourceType);
1164                myResourceName = def.getName();
1165        }
1166
1167        /**
1168         * Subclasses may override to provide behaviour. Invoked within a delete
1169         * transaction with the resource that is about to be deleted.
1170         */
1171        protected void preDelete(T theResourceToDelete, ResourceTable theEntityToDelete, RequestDetails theRequestDetails) {
1172                // nothing by default
1173        }
1174
1175        @Override
1176        @Transactional
1177        public T readByPid(ResourcePersistentId thePid) {
1178                return readByPid(thePid, false);
1179        }
1180
1181        @Override
1182        @Transactional
1183        public T readByPid(ResourcePersistentId thePid, boolean theDeletedOk) {
1184                StopWatch w = new StopWatch();
1185
1186                Optional<ResourceTable> entity = myResourceTableDao.findById(thePid.getIdAsLong());
1187                if (!entity.isPresent()) {
1188                        throw new ResourceNotFoundException(Msg.code(975) + "No resource found with PID " + thePid);
1189                }
1190                if (isDeleted(entity.get()) && !theDeletedOk) {
1191                        throw createResourceGoneException(entity.get());
1192                }
1193
1194                T retVal = toResource(myResourceType, entity.get(), null, false);
1195
1196                ourLog.debug("Processed read on {} in {}ms", thePid, w.getMillis());
1197                return retVal;
1198        }
1199
1200        @Override
1201        public T read(IIdType theId) {
1202                return read(theId, null);
1203        }
1204
1205        @Override
1206        public T read(IIdType theId, RequestDetails theRequestDetails) {
1207                return read(theId, theRequestDetails, false);
1208        }
1209
1210        @Override
1211        public T read(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) {
1212                validateResourceTypeAndThrowInvalidRequestException(theId);
1213                TransactionDetails transactionDetails = new TransactionDetails();
1214
1215                return myTransactionService.execute(theRequest, transactionDetails, tx -> doRead(theId, theRequest, theDeletedOk));
1216        }
1217
1218        public T doRead(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) {
1219                assert TransactionSynchronizationManager.isActualTransactionActive();
1220
1221                StopWatch w = new StopWatch();
1222                BaseHasResource entity = readEntity(theId, theRequest);
1223                validateResourceType(entity);
1224
1225                T retVal = toResource(myResourceType, entity, null, false);
1226
1227                if (theDeletedOk == false) {
1228                        if (isDeleted(entity)) {
1229                                throw createResourceGoneException(entity);
1230                        }
1231                }
1232
1233                // Interceptor broadcast: STORAGE_PREACCESS_RESOURCES
1234                {
1235                        SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(retVal);
1236                        HookParams params = new HookParams()
1237                                .add(IPreResourceAccessDetails.class, accessDetails)
1238                                .add(RequestDetails.class, theRequest)
1239                                .addIfMatchesType(ServletRequestDetails.class, theRequest);
1240                        CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
1241                        if (accessDetails.isDontReturnResourceAtIndex(0)) {
1242                                throw new ResourceNotFoundException(Msg.code(1995) + "Resource " + theId + " is not known");
1243                        }
1244                }
1245
1246                // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES
1247                {
1248                        SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(retVal);
1249                        HookParams params = new HookParams()
1250                                .add(IPreResourceShowDetails.class, showDetails)
1251                                .add(RequestDetails.class, theRequest)
1252                                .addIfMatchesType(ServletRequestDetails.class, theRequest);
1253                        CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PRESHOW_RESOURCES, params);
1254                        //noinspection unchecked
1255                        retVal = (T) showDetails.getResource(0);
1256                }
1257
1258                ourLog.debug("Processed read on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
1259                return retVal;
1260        }
1261
1262        @Override
1263        @Transactional
1264        public BaseHasResource readEntity(IIdType theId, RequestDetails theRequest) {
1265                return readEntity(theId, true, theRequest);
1266        }
1267
1268        @Override
1269        @Transactional
1270        public String getCurrentVersionId(IIdType theReferenceElement) {
1271                return Long.toString(readEntity(theReferenceElement.toVersionless(), null).getVersion());
1272        }
1273
1274        @SuppressWarnings("unchecked")
1275        @Override
1276        public void reindex(ResourcePersistentId theResourcePersistentId, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
1277                Optional<ResourceTable> entityOpt = myResourceTableDao.findById(theResourcePersistentId.getIdAsLong());
1278                if (!entityOpt.isPresent()) {
1279                        ourLog.warn("Unable to find entity with PID: {}", theResourcePersistentId.getId());
1280                        return;
1281                }
1282
1283                ResourceTable entity = entityOpt.get();
1284                try {
1285                        T resource = (T) toResource(entity, false);
1286                        reindex(resource, entity);
1287                } catch (BaseServerResponseException | DataFormatException e) {
1288                        myResourceTableDao.updateIndexStatus(entity.getId(), INDEX_STATUS_INDEXING_FAILED);
1289                        throw e;
1290                }
1291        }
1292
1293        @Override
1294        @Transactional
1295        public BaseHasResource readEntity(IIdType theId, boolean theCheckForForcedId, RequestDetails theRequest) {
1296                validateResourceTypeAndThrowInvalidRequestException(theId);
1297
1298                RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead(theRequest, getResourceName(), theId);
1299
1300                BaseHasResource entity;
1301                ResourcePersistentId pid = myIdHelperService.resolveResourcePersistentIds(requestPartitionId, getResourceName(), theId.getIdPart());
1302                Set<Integer> readPartitions = null;
1303                if (requestPartitionId.isAllPartitions()) {
1304                        entity = myEntityManager.find(ResourceTable.class, pid.getIdAsLong());
1305                } else {
1306                        readPartitions = myRequestPartitionHelperService.toReadPartitions(requestPartitionId);
1307                        if (readPartitions.size() == 1) {
1308                                if (readPartitions.contains(null)) {
1309                                        entity = myResourceTableDao.readByPartitionIdNull(pid.getIdAsLong()).orElse(null);
1310                                } else {
1311                                        entity = myResourceTableDao.readByPartitionId(readPartitions.iterator().next(), pid.getIdAsLong()).orElse(null);
1312                                }
1313                        } else {
1314                                if (readPartitions.contains(null)) {
1315                                        List<Integer> readPartitionsWithoutNull = readPartitions.stream().filter(t -> t != null).collect(Collectors.toList());
1316                                        entity = myResourceTableDao.readByPartitionIdsOrNull(readPartitionsWithoutNull, pid.getIdAsLong()).orElse(null);
1317                                } else {
1318                                        entity = myResourceTableDao.readByPartitionIds(readPartitions, pid.getIdAsLong()).orElse(null);
1319                                }
1320                        }
1321                }
1322
1323                // Verify that the resource is for the correct partition
1324                if (entity != null && readPartitions != null && entity.getPartitionId() != null) {
1325                        if (!readPartitions.contains(entity.getPartitionId().getPartitionId())) {
1326                                ourLog.debug("Performing a read for PartitionId={} but entity has partition: {}", requestPartitionId, entity.getPartitionId());
1327                                entity = null;
1328                        }
1329                }
1330
1331                if (entity == null) {
1332                        throw new ResourceNotFoundException(Msg.code(1996) + "Resource " + theId + " is not known");
1333                }
1334
1335                if (theId.hasVersionIdPart()) {
1336                        if (theId.isVersionIdPartValidLong() == false) {
1337                                throw new ResourceNotFoundException(Msg.code(978) + getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidVersion", theId.getVersionIdPart(), theId.toUnqualifiedVersionless()));
1338                        }
1339                        if (entity.getVersion() != theId.getVersionIdPartAsLong()) {
1340                                entity = null;
1341                        }
1342                }
1343
1344                if (entity == null) {
1345                        if (theId.hasVersionIdPart()) {
1346                                TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery("SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class);
1347                                q.setParameter("RID", pid.getId());
1348                                q.setParameter("RTYP", myResourceName);
1349                                q.setParameter("RVER", theId.getVersionIdPartAsLong());
1350                                try {
1351                                        entity = q.getSingleResult();
1352                                } catch (NoResultException e) {
1353                                        throw new ResourceNotFoundException(Msg.code(979) + getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "invalidVersion", theId.getVersionIdPart(), theId.toUnqualifiedVersionless()));
1354                                }
1355                        }
1356                }
1357
1358                Validate.notNull(entity);
1359                validateResourceType(entity);
1360
1361                if (theCheckForForcedId) {
1362                        validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
1363                }
1364                return entity;
1365        }
1366
1367        @Nonnull
1368        protected ResourceTable readEntityLatestVersion(IIdType theId, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) {
1369                RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead(theRequestDetails, getResourceName(), theId);
1370                return readEntityLatestVersion(theId, requestPartitionId, theTransactionDetails);
1371        }
1372
1373        @Nonnull
1374        private ResourceTable readEntityLatestVersion(IIdType theId, @Nonnull RequestPartitionId theRequestPartitionId, TransactionDetails theTransactionDetails) {
1375                validateResourceTypeAndThrowInvalidRequestException(theId);
1376
1377                ResourcePersistentId persistentId = null;
1378                if (theTransactionDetails != null) {
1379                        if (theTransactionDetails.isResolvedResourceIdEmpty(theId.toUnqualifiedVersionless())) {
1380                                throw new ResourceNotFoundException(Msg.code(1997) + theId);
1381                        }
1382                        if (theTransactionDetails.hasResolvedResourceIds()) {
1383                                persistentId = theTransactionDetails.getResolvedResourceId(theId);
1384                        }
1385                }
1386
1387                if (persistentId == null) {
1388                        persistentId = myIdHelperService.resolveResourcePersistentIds(theRequestPartitionId, getResourceName(), theId.getIdPart());
1389                }
1390
1391                ResourceTable entity = myEntityManager.find(ResourceTable.class, persistentId.getId());
1392                if (entity == null) {
1393                        throw new ResourceNotFoundException(Msg.code(1998) + theId);
1394                }
1395                validateGivenIdIsAppropriateToRetrieveResource(theId, entity);
1396                entity.setTransientForcedId(theId.getIdPart());
1397                return entity;
1398        }
1399
1400        @Override
1401        public void reindex(T theResource, ResourceTable theEntity) {
1402                assert TransactionSynchronizationManager.isActualTransactionActive();
1403
1404                ourLog.debug("Indexing resource {} - PID {}", theEntity.getIdDt().getValue(), theEntity.getId());
1405                if (theResource != null) {
1406                        CURRENTLY_REINDEXING.put(theResource, Boolean.TRUE);
1407                }
1408
1409                TransactionDetails transactionDetails = new TransactionDetails(theEntity.getUpdatedDate());
1410                ResourceTable resourceTable = updateEntity(null, theResource, theEntity, theEntity.getDeleted(), true, false, transactionDetails, true, false);
1411                if (theResource != null) {
1412                        CURRENTLY_REINDEXING.put(theResource, null);
1413                }
1414        }
1415
1416
1417        @Transactional
1418        @Override
1419        public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm) {
1420                removeTag(theId, theTagType, theScheme, theTerm, null);
1421        }
1422
1423        @Transactional
1424        @Override
1425        public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, RequestDetails theRequest) {
1426                StopWatch w = new StopWatch();
1427                BaseHasResource entity = readEntity(theId, theRequest);
1428                if (entity == null) {
1429                        throw new ResourceNotFoundException(Msg.code(1999) + theId);
1430                }
1431
1432                for (BaseTag next : new ArrayList<>(entity.getTags())) {
1433                        if (ObjectUtil.equals(next.getTag().getTagType(), theTagType) &&
1434                                ObjectUtil.equals(next.getTag().getSystem(), theScheme) &&
1435                                ObjectUtil.equals(next.getTag().getCode(), theTerm)) {
1436                                myEntityManager.remove(next);
1437                                entity.getTags().remove(next);
1438                        }
1439                }
1440
1441                if (entity.getTags().isEmpty()) {
1442                        entity.setHasTags(false);
1443                }
1444
1445                myEntityManager.merge(entity);
1446
1447                ourLog.debug("Processed remove tag {}/{} on {} in {}ms", theScheme, theTerm, theId.getValue(), w.getMillisAndRestart());
1448        }
1449
1450        @Transactional(propagation = Propagation.SUPPORTS)
1451        @Override
1452        public IBundleProvider search(final SearchParameterMap theParams) {
1453                return search(theParams, null);
1454        }
1455
1456        @Transactional(propagation = Propagation.SUPPORTS)
1457        @Override
1458        public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequest) {
1459                return search(theParams, theRequest, null);
1460        }
1461
1462        @Transactional(propagation = Propagation.SUPPORTS)
1463        @Override
1464        public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequest, HttpServletResponse theServletResponse) {
1465
1466                if (theParams.getSearchContainedMode() == SearchContainedModeEnum.BOTH) {
1467                        throw new MethodNotAllowedException(Msg.code(983) + "Contained mode 'both' is not currently supported");
1468                }
1469                if (theParams.getSearchContainedMode() != SearchContainedModeEnum.FALSE && !myModelConfig.isIndexOnContainedResources()) {
1470                        throw new MethodNotAllowedException(Msg.code(984) + "Searching with _contained mode enabled is not enabled on this server");
1471                }
1472
1473                translateListSearchParams(theParams);
1474
1475                notifySearchInterceptors(theParams, theRequest);
1476
1477                CacheControlDirective cacheControlDirective = new CacheControlDirective();
1478                if (theRequest != null) {
1479                        cacheControlDirective.parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL));
1480                }
1481
1482                RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), theParams, null);
1483                IBundleProvider retVal = mySearchCoordinatorSvc.registerSearch(this, theParams, getResourceName(), cacheControlDirective, theRequest, requestPartitionId);
1484
1485                if (retVal instanceof PersistedJpaBundleProvider) {
1486                        PersistedJpaBundleProvider provider = (PersistedJpaBundleProvider) retVal;
1487                        if (provider.getCacheStatus() == SearchCacheStatusEnum.HIT) {
1488                                if (theServletResponse != null && theRequest != null) {
1489                                        String value = "HIT from " + theRequest.getFhirServerBase();
1490                                        theServletResponse.addHeader(Constants.HEADER_X_CACHE, value);
1491                                }
1492                        }
1493                }
1494
1495                return retVal;
1496        }
1497
1498        private void translateListSearchParams(SearchParameterMap theParams) {
1499                Iterator<String> keyIterator = theParams.keySet().iterator();
1500
1501                // Translate _list=42 to _has=List:item:_id=42
1502                while (keyIterator.hasNext()) {
1503                        String key = keyIterator.next();
1504                        if (Constants.PARAM_LIST.equals((key))) {
1505                                List<List<IQueryParameterType>> andOrValues = theParams.get(key);
1506                                theParams.remove(key);
1507                                List<List<IQueryParameterType>> hasParamValues = new ArrayList<>();
1508                                for (List<IQueryParameterType> orValues : andOrValues) {
1509                                        List<IQueryParameterType> orList = new ArrayList<>();
1510                                        for (IQueryParameterType value : orValues) {
1511                                                orList.add(new HasParam("List", ListResource.SP_ITEM, ListResource.SP_RES_ID, value.getValueAsQueryToken(null)));
1512                                        }
1513                                        hasParamValues.add(orList);
1514                                }
1515                                theParams.put(Constants.PARAM_HAS, hasParamValues);
1516                        }
1517                }
1518        }
1519
1520        private void notifySearchInterceptors(SearchParameterMap theParams, RequestDetails theRequest) {
1521                if (theRequest != null) {
1522
1523                        if (theRequest.isSubRequest()) {
1524                                Integer max = getConfig().getMaximumSearchResultCountInTransaction();
1525                                if (max != null) {
1526                                        Validate.inclusiveBetween(1, Integer.MAX_VALUE, max, "Maximum search result count in transaction ust be a positive integer");
1527                                        theParams.setLoadSynchronousUpTo(getConfig().getMaximumSearchResultCountInTransaction());
1528                                }
1529                        }
1530
1531                        final Integer offset = RestfulServerUtils.extractOffsetParameter(theRequest);
1532                        if (offset != null || !isPagingProviderDatabaseBacked(theRequest)) {
1533                                theParams.setLoadSynchronous(true);
1534                                if (offset != null) {
1535                                        Validate.inclusiveBetween(0, Integer.MAX_VALUE, offset, "Offset must be a positive integer");
1536                                }
1537                                theParams.setOffset(offset);
1538                        }
1539
1540                        Integer count = RestfulServerUtils.extractCountParameter(theRequest);
1541                        if (count != null) {
1542                                Integer maxPageSize = theRequest.getServer().getMaximumPageSize();
1543                                if (maxPageSize != null && count > maxPageSize) {
1544                                        ourLog.info("Reducing {} from {} to {} which is the maximum allowable page size.", Constants.PARAM_COUNT, count, maxPageSize);
1545                                        count = maxPageSize;
1546                                }
1547                                theParams.setCount(count);
1548                        } else if (theRequest.getServer().getDefaultPageSize() != null) {
1549                                theParams.setCount(theRequest.getServer().getDefaultPageSize());
1550                        }
1551                }
1552        }
1553
1554        @Override
1555        public List<ResourcePersistentId> searchForIds(SearchParameterMap theParams, RequestDetails theRequest, @Nullable IBaseResource theConditionalOperationTargetOrNull) {
1556                TransactionDetails transactionDetails = new TransactionDetails();
1557
1558                return myTransactionService.execute(theRequest, transactionDetails, tx -> {
1559
1560                        if (theParams.getLoadSynchronousUpTo() != null) {
1561                                theParams.setLoadSynchronousUpTo(Math.min(getConfig().getInternalSynchronousSearchSize(), theParams.getLoadSynchronousUpTo()));
1562                        } else {
1563                                theParams.setLoadSynchronousUpTo(getConfig().getInternalSynchronousSearchSize());
1564                        }
1565
1566                        ISearchBuilder builder = mySearchBuilderFactory.newSearchBuilder(this, getResourceName(), getResourceType());
1567
1568                        List<ResourcePersistentId> ids = new ArrayList<>();
1569
1570                        String uuid = UUID.randomUUID().toString();
1571                        RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType(theRequest, getResourceName(), theParams, theConditionalOperationTargetOrNull);
1572
1573                        SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid);
1574                        try (IResultIterator iter = builder.createQuery(theParams, searchRuntimeDetails, theRequest, requestPartitionId)) {
1575                                while (iter.hasNext()) {
1576                                        ids.add(iter.next());
1577                                }
1578                        } catch (IOException e) {
1579                                ourLog.error("IO failure during database access", e);
1580                        }
1581
1582                        return ids;
1583                });
1584        }
1585
1586        protected <MT extends IBaseMetaType> MT toMetaDt(Class<MT> theType, Collection<TagDefinition> tagDefinitions) {
1587                MT retVal = ReflectionUtil.newInstance(theType);
1588                for (TagDefinition next : tagDefinitions) {
1589                        switch (next.getTagType()) {
1590                                case PROFILE:
1591                                        retVal.addProfile(next.getCode());
1592                                        break;
1593                                case SECURITY_LABEL:
1594                                        retVal.addSecurity().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
1595                                        break;
1596                                case TAG:
1597                                        retVal.addTag().setSystem(next.getSystem()).setCode(next.getCode()).setDisplay(next.getDisplay());
1598                                        break;
1599                        }
1600                }
1601                return retVal;
1602        }
1603
1604        private ArrayList<TagDefinition> toTagList(IBaseMetaType theMeta) {
1605                ArrayList<TagDefinition> retVal = new ArrayList<>();
1606
1607                for (IBaseCoding next : theMeta.getTag()) {
1608                        retVal.add(new TagDefinition(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay()));
1609                }
1610                for (IBaseCoding next : theMeta.getSecurity()) {
1611                        retVal.add(new TagDefinition(TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay()));
1612                }
1613                for (IPrimitiveType<String> next : theMeta.getProfile()) {
1614                        retVal.add(new TagDefinition(TagTypeEnum.PROFILE, BaseHapiFhirDao.NS_JPA_PROFILE, next.getValue(), null));
1615                }
1616
1617                return retVal;
1618        }
1619
1620        @Override
1621        public DaoMethodOutcome update(T theResource) {
1622                return update(theResource, null, null);
1623        }
1624
1625        @Override
1626        public DaoMethodOutcome update(T theResource, RequestDetails theRequestDetails) {
1627                return update(theResource, null, theRequestDetails);
1628        }
1629
1630        @Override
1631        public DaoMethodOutcome update(T theResource, String theMatchUrl) {
1632                return update(theResource, theMatchUrl, null);
1633        }
1634
1635        @Override
1636        public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) {
1637                return update(theResource, theMatchUrl, true, theRequestDetails);
1638        }
1639
1640        @Override
1641        public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) {
1642                return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails, new TransactionDetails());
1643        }
1644
1645        @Override
1646        public DaoMethodOutcome update(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequest, @Nonnull TransactionDetails theTransactionDetails) {
1647                if (theResource == null) {
1648                        String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "missingBody");
1649                        throw new InvalidRequestException(Msg.code(986) + msg);
1650                }
1651                if (!theResource.getIdElement().hasIdPart() && isBlank(theMatchUrl)) {
1652                        String type = myFhirContext.getResourceType(theResource);
1653                        String msg = myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "updateWithNoId", type);
1654                        throw new InvalidRequestException(Msg.code(987) + msg);
1655                }
1656
1657                /*
1658                 * Resource updates will modify/update the version of the resource with the new version. This is generally helpful,
1659                 * but leads to issues if the transaction is rolled back and retried. So if we do a rollback, we reset the resource
1660                 * version to what it was.
1661                 */
1662                String id = theResource.getIdElement().getValue();
1663                Runnable onRollback = () -> theResource.getIdElement().setValue(id);
1664
1665                // Execute the update in a retryable transaction
1666                if (myDaoConfig.isUpdateWithHistoryRewriteEnabled() && theRequest.isRewriteHistory()) {
1667                        return myTransactionService.execute(theRequest, theTransactionDetails, tx -> doUpdateWithHistoryRewrite(theResource, theRequest, theTransactionDetails), onRollback);
1668                } else {
1669                        return myTransactionService.execute(theRequest, theTransactionDetails, tx -> doUpdate(theResource, theMatchUrl, thePerformIndexing, theForceUpdateVersion, theRequest, theTransactionDetails), onRollback);
1670                }
1671        }
1672
1673        private DaoMethodOutcome doUpdate(T theResource, String theMatchUrl, boolean thePerformIndexing, boolean theForceUpdateVersion, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
1674                StopWatch w = new StopWatch();
1675
1676                T resource = theResource;
1677
1678                preProcessResourceForStorage(resource);
1679                preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing);
1680
1681                ResourceTable entity = null;
1682
1683                IIdType resourceId;
1684                if (isNotBlank(theMatchUrl)) {
1685                        Set<ResourcePersistentId> match = myMatchResourceUrlService.processMatchUrl(theMatchUrl, myResourceType, theTransactionDetails, theRequest, theResource);
1686                        if (match.size() > 1) {
1687                                String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "transactionOperationWithMultipleMatchFailure", "UPDATE", theMatchUrl, match.size());
1688                                throw new PreconditionFailedException(Msg.code(988) + msg);
1689                        } else if (match.size() == 1) {
1690                                ResourcePersistentId pid = match.iterator().next();
1691                                entity = myEntityManager.find(ResourceTable.class, pid.getId());
1692                                resourceId = entity.getIdDt();
1693                        } else {
1694                                DaoMethodOutcome outcome = create(resource, null, thePerformIndexing, theTransactionDetails, theRequest);
1695
1696                                // Pre-cache the match URL
1697                                if (outcome.getPersistentId() != null) {
1698                                        myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, getResourceName(), theMatchUrl, outcome.getPersistentId());
1699                                }
1700
1701                                return outcome;
1702                        }
1703                } else {
1704                        /*
1705                         * Note: resourceId will not be null or empty here, because we
1706                         * check it and reject requests in
1707                         * BaseOutcomeReturningMethodBindingWithResourceParam
1708                         */
1709                        resourceId = theResource.getIdElement();
1710                        assert resourceId != null;
1711                        assert resourceId.hasIdPart();
1712
1713                        RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest(theRequest, theResource, getResourceName());
1714
1715                        boolean create = false;
1716
1717                        if (theRequest != null) {
1718                                String existenceCheck = theRequest.getHeader(JpaConstants.HEADER_UPSERT_EXISTENCE_CHECK);
1719                                if (JpaConstants.HEADER_UPSERT_EXISTENCE_CHECK_DISABLED.equals(existenceCheck)) {
1720                                        create = true;
1721                                }
1722                        }
1723
1724                        if (!create) {
1725                                try {
1726                                        entity = readEntityLatestVersion(resourceId, requestPartitionId, theTransactionDetails);
1727                                } catch (ResourceNotFoundException e) {
1728                                        create = true;
1729                                }
1730                        }
1731
1732                        if (create) {
1733                                return doCreateForPostOrPut(resource, null, thePerformIndexing, theTransactionDetails, theRequest, requestPartitionId);
1734                        }
1735                }
1736
1737                if (resourceId.hasVersionIdPart() && Long.parseLong(resourceId.getVersionIdPart()) != entity.getVersion()) {
1738                        throw new ResourceVersionConflictException(Msg.code(989) + "Trying to update " + resourceId + " but this is not the current version");
1739                }
1740
1741                if (resourceId.hasResourceType() && !resourceId.getResourceType().equals(getResourceName())) {
1742                        throw new UnprocessableEntityException(Msg.code(990) + "Invalid resource ID[" + entity.getIdDt().toUnqualifiedVersionless() + "] of type[" + entity.getResourceType() + "] - Does not match expected [" + getResourceName() + "]");
1743                }
1744
1745                IBaseResource oldResource;
1746                if (getConfig().isMassIngestionMode()) {
1747                        oldResource = null;
1748                } else {
1749                        oldResource = toResource(entity, false);
1750                }
1751
1752                /*
1753                 * Mark the entity as not deleted - This is also done in the actual updateInternal()
1754                 * method later on so it usually doesn't matter whether we do it here, but in the
1755                 * case of a transaction with multiple PUTs we don't get there until later so
1756                 * having this here means that a transaction can have a reference in one
1757                 * resource to another resource in the same transaction that is being
1758                 * un-deleted by the transaction. Wacky use case, sure. But it's real.
1759                 *
1760                 * See SystemProviderR4Test#testTransactionReSavesPreviouslyDeletedResources
1761                 * for a test that needs this.
1762                 */
1763                boolean wasDeleted = isDeleted(entity);
1764                entity.setDeleted(null);
1765
1766                /*
1767                 * If we aren't indexing, that means we're doing this inside a transaction.
1768                 * The transaction will do the actual storage to the database a bit later on,
1769                 * after placeholder IDs have been replaced, by calling {@link #updateInternal}
1770                 * directly. So we just bail now.
1771                 */
1772                if (!thePerformIndexing) {
1773                        resource.setId(entity.getIdDt().getValue());
1774                        DaoMethodOutcome outcome = toMethodOutcome(theRequest, entity, resource).setCreated(wasDeleted);
1775                        outcome.setPreviousResource(oldResource);
1776                        if (!outcome.isNop()) {
1777                                // Technically this may not end up being right since we might not increment if the
1778                                // contents turn out to be the same
1779                                outcome.setId(outcome.getId().withVersion(Long.toString(outcome.getId().getVersionIdPartAsLong() + 1)));
1780                        }
1781                        return outcome;
1782                }
1783
1784                /*
1785                 * Otherwise, we're not in a transaction
1786                 */
1787                ResourceTable savedEntity = updateInternal(theRequest, resource, thePerformIndexing, theForceUpdateVersion, entity, resourceId, oldResource, theTransactionDetails);
1788                DaoMethodOutcome outcome = toMethodOutcome(theRequest, savedEntity, resource).setCreated(wasDeleted);
1789
1790                if (!thePerformIndexing) {
1791                        IIdType id = getContext().getVersion().newIdType();
1792                        id.setValue(entity.getIdDt().getValue());
1793                        outcome.setId(id);
1794                }
1795
1796                String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "successfulUpdate", outcome.getId(), w.getMillisAndRestart());
1797                outcome.setOperationOutcome(createInfoOperationOutcome(msg));
1798
1799                ourLog.debug(msg);
1800                return outcome;
1801        }
1802
1803        /**
1804         * Method for updating the historical version of the resource when a history version id is included in the request.
1805         *
1806         * @param theResource           to be saved
1807         * @param theRequest            details of the request
1808         * @param theTransactionDetails details of the transaction
1809         * @return the outcome of the operation
1810         */
1811        private DaoMethodOutcome doUpdateWithHistoryRewrite(T theResource, RequestDetails theRequest, TransactionDetails theTransactionDetails) {
1812                StopWatch w = new StopWatch();
1813
1814                // No need for indexing as this will update a non-current version of the resource which will not be searchable
1815                preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, false);
1816
1817                BaseHasResource entity = null;
1818                BaseHasResource currentEntity = null;
1819
1820                IIdType resourceId;
1821
1822                resourceId = theResource.getIdElement();
1823                assert resourceId != null;
1824                assert resourceId.hasIdPart();
1825
1826                try {
1827                        currentEntity = readEntityLatestVersion(resourceId.toVersionless(), theRequest, theTransactionDetails);
1828
1829                        if (!resourceId.hasVersionIdPart()) {
1830                                throw new InvalidRequestException(Msg.code(2093) + "Invalid resource ID, ID must contain a history version");
1831                        }
1832                        entity = readEntity(resourceId, theRequest);
1833                        validateResourceType(entity);
1834                } catch (ResourceNotFoundException e) {
1835                        throw new ResourceNotFoundException(Msg.code(2087) + "Resource not found [" + resourceId + "] - Doesn't exist");
1836                }
1837
1838                if (resourceId.hasResourceType() && !resourceId.getResourceType().equals(getResourceName())) {
1839                        throw new UnprocessableEntityException(Msg.code(2088) + "Invalid resource ID[" + entity.getIdDt().toUnqualifiedVersionless() + "] of type[" + entity.getResourceType() + "] - Does not match expected [" + getResourceName() + "]");
1840                }
1841                assert resourceId.hasVersionIdPart();
1842
1843                boolean wasDeleted = isDeleted(entity);
1844                entity.setDeleted(null);
1845                boolean isUpdatingCurrent = resourceId.hasVersionIdPart() && Long.parseLong(resourceId.getVersionIdPart()) == currentEntity.getVersion();
1846                IBasePersistedResource savedEntity = updateHistoryEntity(theRequest, theResource, currentEntity, entity, resourceId, theTransactionDetails, isUpdatingCurrent);
1847                DaoMethodOutcome outcome = toMethodOutcome(theRequest, savedEntity, theResource).setCreated(wasDeleted);
1848
1849                String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "successfulUpdate", outcome.getId(), w.getMillisAndRestart());
1850                outcome.setOperationOutcome(createInfoOperationOutcome(msg));
1851
1852                ourLog.debug(msg);
1853                return outcome;
1854        }
1855
1856        @Override
1857        @Transactional(propagation = Propagation.SUPPORTS)
1858        public MethodOutcome validate(T theResource, IIdType theId, String theRawResource, EncodingEnum theEncoding, ValidationModeEnum theMode, String theProfile, RequestDetails theRequest) {
1859                TransactionDetails transactionDetails = new TransactionDetails();
1860
1861                if (theMode == ValidationModeEnum.DELETE) {
1862                        if (theId == null || theId.hasIdPart() == false) {
1863                                throw new InvalidRequestException(Msg.code(991) + "No ID supplied. ID is required when validating with mode=DELETE");
1864                        }
1865                        final ResourceTable entity = readEntityLatestVersion(theId, theRequest, transactionDetails);
1866
1867                        // Validate that there are no resources pointing to the candidate that
1868                        // would prevent deletion
1869                        DeleteConflictList deleteConflicts = new DeleteConflictList();
1870                        if (getConfig().isEnforceReferentialIntegrityOnDelete()) {
1871                                myDeleteConflictService.validateOkToDelete(deleteConflicts, entity, true, theRequest, new TransactionDetails());
1872                        }
1873                        DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts);
1874
1875                        IBaseOperationOutcome oo = createInfoOperationOutcome("Ok to delete");
1876                        return new MethodOutcome(new IdDt(theId.getValue()), oo);
1877                }
1878
1879                FhirValidator validator = getContext().newValidator();
1880                validator.setInterceptorBroadcaster(CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest));
1881                validator.registerValidatorModule(getInstanceValidator());
1882                validator.registerValidatorModule(new IdChecker(theMode));
1883
1884                IBaseResource resourceToValidateById = null;
1885                if (theId != null && theId.hasResourceType() && theId.hasIdPart()) {
1886                        Class<? extends IBaseResource> type = getContext().getResourceDefinition(theId.getResourceType()).getImplementingClass();
1887                        IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDaoOrNull(type);
1888                        resourceToValidateById = dao.read(theId, theRequest);
1889                }
1890
1891
1892                ValidationResult result;
1893                ValidationOptions options = new ValidationOptions()
1894                        .addProfileIfNotBlank(theProfile);
1895
1896                if (theResource == null) {
1897                        if (resourceToValidateById != null) {
1898                                result = validator.validateWithResult(resourceToValidateById, options);
1899                        } else {
1900                                String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "cantValidateWithNoResource");
1901                                throw new InvalidRequestException(Msg.code(992) + msg);
1902                        }
1903                } else if (isNotBlank(theRawResource)) {
1904                        result = validator.validateWithResult(theRawResource, options);
1905                } else {
1906                        result = validator.validateWithResult(theResource, options);
1907                }
1908
1909                if (result.isSuccessful()) {
1910                        MethodOutcome retVal = new MethodOutcome();
1911                        retVal.setOperationOutcome(result.toOperationOutcome());
1912                        return retVal;
1913                } else {
1914                        throw new PreconditionFailedException(Msg.code(993) + "Validation failed", result.toOperationOutcome());
1915                }
1916
1917        }
1918
1919        /**
1920         * Get the resource definition from the criteria which specifies the resource type
1921         */
1922        @Override
1923        public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria) {
1924                String resourceName;
1925                if (criteria == null || criteria.trim().isEmpty()) {
1926                        throw new IllegalArgumentException(Msg.code(994) + "Criteria cannot be empty");
1927                }
1928                if (criteria.contains("?")) {
1929                        resourceName = criteria.substring(0, criteria.indexOf("?"));
1930                } else {
1931                        resourceName = criteria;
1932                }
1933
1934                return getContext().getResourceDefinition(resourceName);
1935        }
1936
1937        private void validateGivenIdIsAppropriateToRetrieveResource(IIdType theId, BaseHasResource entity) {
1938                if (entity.getForcedId() != null) {
1939                        if (getConfig().getResourceClientIdStrategy() != DaoConfig.ClientIdStrategyEnum.ANY) {
1940                                if (theId.isIdPartValidLong()) {
1941                                        // This means that the resource with the given numeric ID exists, but it has a "forced ID", meaning that
1942                                        // as far as the outside world is concerned, the given ID doesn't exist (it's just an internal pointer
1943                                        // to the
1944                                        // forced ID)
1945                                        throw new ResourceNotFoundException(Msg.code(2000) + theId);
1946                                }
1947                        }
1948                }
1949        }
1950
1951        private void validateResourceType(BaseHasResource entity) {
1952                validateResourceType(entity, myResourceName);
1953        }
1954
1955        private void validateResourceTypeAndThrowInvalidRequestException(IIdType theId) {
1956                if (theId.hasResourceType() && !theId.getResourceType().equals(myResourceName)) {
1957                        // Note- Throw a HAPI FHIR exception here so that hibernate doesn't try to translate it into a database exception
1958                        throw new InvalidRequestException(Msg.code(996) + "Incorrect resource type (" + theId.getResourceType() + ") for this DAO, wanted: " + myResourceName);
1959                }
1960        }
1961
1962        @VisibleForTesting
1963        public void setIdHelperSvcForUnitTest(IIdHelperService theIdHelperService) {
1964                myIdHelperService = theIdHelperService;
1965        }
1966
1967        private static class IdChecker implements IValidatorModule {
1968
1969                private final ValidationModeEnum myMode;
1970
1971                IdChecker(ValidationModeEnum theMode) {
1972                        myMode = theMode;
1973                }
1974
1975                @Override
1976                public void validateResource(IValidationContext<IBaseResource> theCtx) {
1977                        boolean hasId = theCtx.getResource().getIdElement().hasIdPart();
1978                        if (myMode == ValidationModeEnum.CREATE) {
1979                                if (hasId) {
1980                                        throw new UnprocessableEntityException(Msg.code(997) + "Resource has an ID - ID must not be populated for a FHIR create");
1981                                }
1982                        } else if (myMode == ValidationModeEnum.UPDATE) {
1983                                if (hasId == false) {
1984                                        throw new UnprocessableEntityException(Msg.code(998) + "Resource has no ID - ID must be populated for a FHIR update");
1985                                }
1986                        }
1987
1988                }
1989
1990        }
1991
1992}