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