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