001/*-
002 * #%L
003 * HAPI FHIR Storage api
004 * %%
005 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.dao;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.RuntimeSearchParam;
024import ca.uhn.fhir.i18n.Msg;
025import ca.uhn.fhir.interceptor.api.HookParams;
026import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
027import ca.uhn.fhir.interceptor.api.Pointcut;
028import ca.uhn.fhir.interceptor.model.RequestPartitionId;
029import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
030import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
031import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
032import ca.uhn.fhir.jpa.api.model.LazyDaoMethodOutcome;
033import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
034import ca.uhn.fhir.jpa.cache.ResourcePersistentIdMap;
035import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
036import ca.uhn.fhir.jpa.model.entity.ResourceTable;
037import ca.uhn.fhir.jpa.model.entity.StorageSettings;
038import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
039import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
040import ca.uhn.fhir.model.api.IQueryParameterAnd;
041import ca.uhn.fhir.model.api.StorageResponseCodeEnum;
042import ca.uhn.fhir.rest.api.QualifiedParamList;
043import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
044import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
045import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails;
046import ca.uhn.fhir.rest.api.server.RequestDetails;
047import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails;
048import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
049import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
050import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
051import ca.uhn.fhir.rest.param.QualifierDetails;
052import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
053import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
054import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
055import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
056import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
057import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
058import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
059import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
060import ca.uhn.fhir.util.BundleUtil;
061import ca.uhn.fhir.util.FhirTerser;
062import ca.uhn.fhir.util.HapiExtensions;
063import ca.uhn.fhir.util.IMetaTagSorter;
064import ca.uhn.fhir.util.MetaUtil;
065import ca.uhn.fhir.util.OperationOutcomeUtil;
066import ca.uhn.fhir.util.ResourceReferenceInfo;
067import ca.uhn.fhir.util.StopWatch;
068import ca.uhn.fhir.util.UrlUtil;
069import com.google.common.annotations.VisibleForTesting;
070import jakarta.annotation.Nonnull;
071import jakarta.annotation.Nullable;
072import org.hl7.fhir.instance.model.api.IBaseBundle;
073import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
074import org.hl7.fhir.instance.model.api.IBaseReference;
075import org.hl7.fhir.instance.model.api.IBaseResource;
076import org.hl7.fhir.instance.model.api.IIdType;
077import org.hl7.fhir.r4.model.InstantType;
078import org.slf4j.Logger;
079import org.slf4j.LoggerFactory;
080import org.springframework.beans.factory.annotation.Autowired;
081import org.springframework.transaction.annotation.Propagation;
082import org.springframework.transaction.annotation.Transactional;
083
084import java.util.Collection;
085import java.util.Collections;
086import java.util.IdentityHashMap;
087import java.util.List;
088import java.util.Map;
089import java.util.Set;
090import java.util.function.Supplier;
091import java.util.stream.Collectors;
092import java.util.stream.Stream;
093
094import static org.apache.commons.lang3.StringUtils.defaultString;
095import static org.apache.commons.lang3.StringUtils.isNotBlank;
096
097public abstract class BaseStorageDao {
098        private static final Logger ourLog = LoggerFactory.getLogger(BaseStorageDao.class);
099
100        public static final String OO_SEVERITY_ERROR = "error";
101        public static final String OO_SEVERITY_INFO = "information";
102        public static final String OO_SEVERITY_WARN = "warning";
103        private static final String PROCESSING_SUB_REQUEST = "BaseStorageDao.processingSubRequest";
104
105        protected static final String MESSAGE_KEY_DELETE_RESOURCE_NOT_EXISTING = "deleteResourceNotExisting";
106        protected static final String MESSAGE_KEY_DELETE_RESOURCE_ALREADY_DELETED = "deleteResourceAlreadyDeleted";
107        public static final String OO_ISSUE_CODE_INFORMATIONAL = "informational";
108
109        @Autowired
110        protected ISearchParamRegistry mySearchParamRegistry;
111
112        @Autowired
113        protected FhirContext myFhirContext;
114
115        @Autowired
116        protected DaoRegistry myDaoRegistry;
117
118        @Autowired
119        protected IResourceVersionSvc myResourceVersionSvc;
120
121        @Autowired
122        protected JpaStorageSettings myStorageSettings;
123
124        @Autowired
125        protected IMetaTagSorter myMetaTagSorter;
126
127        @VisibleForTesting
128        public void setSearchParamRegistry(ISearchParamRegistry theSearchParamRegistry) {
129                mySearchParamRegistry = theSearchParamRegistry;
130        }
131
132        @VisibleForTesting
133        public void setMyMetaTagSorter(IMetaTagSorter theMetaTagSorter) {
134                myMetaTagSorter = theMetaTagSorter;
135        }
136
137        /**
138         * May be overridden by subclasses to validate resources prior to storage
139         *
140         * @param theResource The resource that is about to be stored
141         * @deprecated Use {@link #preProcessResourceForStorage(IBaseResource, RequestDetails, TransactionDetails, boolean)} instead
142         */
143        protected void preProcessResourceForStorage(IBaseResource theResource) {
144                // nothing
145        }
146
147        /**
148         * May be overridden by subclasses to validate resources prior to storage
149         *
150         * @param theResource The resource that is about to be stored
151         * @since 5.3.0
152         */
153        protected void preProcessResourceForStorage(
154                        IBaseResource theResource,
155                        RequestDetails theRequestDetails,
156                        TransactionDetails theTransactionDetails,
157                        boolean thePerformIndexing) {
158
159                verifyResourceTypeIsAppropriateForDao(theResource);
160
161                verifyResourceIdIsValid(theResource);
162
163                verifyBundleTypeIsAppropriateForStorage(theResource);
164
165                if (!getStorageSettings().getTreatBaseUrlsAsLocal().isEmpty()) {
166                        replaceAbsoluteReferencesWithRelative(theResource, myFhirContext.newTerser());
167                }
168
169                performAutoVersioning(theResource, thePerformIndexing);
170
171                myMetaTagSorter.sort(theResource.getMeta());
172        }
173
174        /**
175         * Sanity check - Is this resource the right type for this DAO?
176         */
177        private void verifyResourceTypeIsAppropriateForDao(IBaseResource theResource) {
178                String type = getContext().getResourceType(theResource);
179                if (getResourceName() != null && !getResourceName().equals(type)) {
180                        throw new InvalidRequestException(Msg.code(520)
181                                        + getContext()
182                                                        .getLocalizer()
183                                                        .getMessageSanitized(
184                                                                        BaseStorageDao.class, "incorrectResourceType", type, getResourceName()));
185                }
186        }
187
188        /**
189         * Verify that the resource ID is actually valid according to FHIR's rules
190         */
191        private void verifyResourceIdIsValid(IBaseResource theResource) {
192                if (theResource.getIdElement().hasResourceType()) {
193                        String expectedType = getContext().getResourceType(theResource);
194                        if (!expectedType.equals(theResource.getIdElement().getResourceType())) {
195                                throw new InvalidRequestException(Msg.code(2616)
196                                                + getContext()
197                                                                .getLocalizer()
198                                                                .getMessageSanitized(
199                                                                                BaseStorageDao.class,
200                                                                                "failedToCreateWithInvalidIdWrongResourceType",
201                                                                                theResource.getIdElement().toUnqualifiedVersionless()));
202                        }
203                }
204
205                if (theResource.getIdElement().hasIdPart()) {
206                        if (!theResource.getIdElement().isIdPartValid()) {
207                                throw new InvalidRequestException(Msg.code(521)
208                                                + getContext()
209                                                                .getLocalizer()
210                                                                .getMessageSanitized(
211                                                                                BaseStorageDao.class,
212                                                                                "failedToCreateWithInvalidId",
213                                                                                theResource.getIdElement().getIdPart()));
214                        }
215                }
216        }
217
218        /**
219         * Verify that we're not storing a Bundle with a disallowed bundle type
220         */
221        private void verifyBundleTypeIsAppropriateForStorage(IBaseResource theResource) {
222                if (theResource instanceof IBaseBundle) {
223                        Set<String> allowedBundleTypes = getStorageSettings().getBundleTypesAllowedForStorage();
224                        String bundleType = BundleUtil.getBundleType(getContext(), (IBaseBundle) theResource);
225                        bundleType = defaultString(bundleType);
226                        if (!allowedBundleTypes.contains(bundleType)) {
227                                String message = myFhirContext
228                                                .getLocalizer()
229                                                .getMessage(
230                                                                BaseStorageDao.class,
231                                                                "invalidBundleTypeForStorage",
232                                                                (isNotBlank(bundleType) ? bundleType : "(missing)"));
233                                throw new UnprocessableEntityException(Msg.code(522) + message);
234                        }
235                }
236        }
237
238        /**
239         * Replace absolute references with relative ones if configured to do so
240         */
241        private void replaceAbsoluteReferencesWithRelative(IBaseResource theResource, FhirTerser theTerser) {
242                List<ResourceReferenceInfo> refs = theTerser.getAllResourceReferences(theResource);
243                for (ResourceReferenceInfo nextRef : refs) {
244                        IIdType refId = nextRef.getResourceReference().getReferenceElement();
245                        if (refId != null && refId.hasBaseUrl()) {
246                                if (getStorageSettings().getTreatBaseUrlsAsLocal().contains(refId.getBaseUrl())) {
247                                        IIdType newRefId = refId.toUnqualified();
248                                        nextRef.getResourceReference().setReference(newRefId.getValue());
249                                }
250                        }
251                }
252        }
253
254        /**
255         * Handle {@link JpaStorageSettings#getAutoVersionReferenceAtPaths() auto-populate-versions}
256         * <p>
257         * We only do this if thePerformIndexing is true because if it's false, that means
258         * we're in a FHIR transaction during the first phase of write operation processing,
259         * meaning that the versions of other resources may not have need updatd yet. For example
260         * we're about to store an Observation with a reference to a Patient, and that Patient
261         * is also being updated in the same transaction, during the first "no index" phase,
262         * the Patient will not yet have its version number incremented, so it would be wrong
263         * to use that value. During the second phase it is correct.
264         * <p>
265         * Also note that {@link BaseTransactionProcessor} also has code to do auto-versioning
266         * and it is the one that takes care of the placeholder IDs. Look for the other caller of
267         * {@link #extractReferencesToAutoVersion(FhirContext, StorageSettings, IBaseResource)}
268         * to find this.
269         */
270        private void performAutoVersioning(IBaseResource theResource, boolean thePerformIndexing) {
271                if (thePerformIndexing) {
272                        Set<IBaseReference> referencesToVersion =
273                                        extractReferencesToAutoVersion(myFhirContext, myStorageSettings, theResource);
274                        for (IBaseReference nextReference : referencesToVersion) {
275                                IIdType referenceElement = nextReference.getReferenceElement();
276                                if (!referenceElement.hasBaseUrl()) {
277
278                                        ResourcePersistentIdMap resourceVersionMap = myResourceVersionSvc.getLatestVersionIdsForResourceIds(
279                                                        RequestPartitionId.allPartitions(), Collections.singletonList(referenceElement));
280
281                                        // 3 cases:
282                                        // 1) there exists a resource in the db with some version (use this version)
283                                        // 2) no resource exists, but we will create one (eventually). The version is 1
284                                        // 3) no resource exists, and none will be made -> throw
285                                        Long version;
286                                        if (resourceVersionMap.containsKey(referenceElement)) {
287                                                // the resource exists... latest id
288                                                // will be the value in the IResourcePersistentId
289                                                version = resourceVersionMap
290                                                                .getResourcePersistentId(referenceElement)
291                                                                .getVersion();
292                                        } else if (myStorageSettings.isAutoCreatePlaceholderReferenceTargets()) {
293                                                // if idToPID doesn't contain object
294                                                // but autcreateplaceholders is on
295                                                // then the version will be 1 (the first version)
296                                                version = 1L;
297                                        } else {
298                                                // resource not found
299                                                // and no autocreateplaceholders set...
300                                                // we throw
301                                                throw new ResourceNotFoundException(Msg.code(523) + referenceElement);
302                                        }
303                                        String newTargetReference =
304                                                        referenceElement.withVersion(version.toString()).getValue();
305                                        nextReference.setReference(newTargetReference);
306                                }
307                        }
308                }
309        }
310
311        protected DaoMethodOutcome toMethodOutcome(
312                        RequestDetails theRequest,
313                        @Nonnull final IBasePersistedResource theEntity,
314                        @Nonnull IBaseResource theResource,
315                        @Nullable String theMatchUrl,
316                        @Nonnull RestOperationTypeEnum theOperationType) {
317                DaoMethodOutcome outcome = new DaoMethodOutcome();
318
319                IResourcePersistentId persistentId = theEntity.getPersistentId();
320                persistentId.setAssociatedResourceId(theResource.getIdElement());
321
322                outcome.setPersistentId(persistentId);
323                outcome.setMatchUrl(theMatchUrl);
324                outcome.setOperationType(theOperationType);
325
326                if (theEntity instanceof ResourceTable) {
327                        if (((ResourceTable) theEntity).isUnchangedInCurrentOperation()) {
328                                outcome.setNop(true);
329                        }
330                }
331
332                IIdType id = null;
333                if (theResource.getIdElement().getValue() != null) {
334                        id = theResource.getIdElement();
335                }
336                if (id == null) {
337                        id = theEntity.getIdDt();
338                        if (getContext().getVersion().getVersion().isRi()) {
339                                id = getContext().getVersion().newIdType().setValue(id.getValue());
340                        }
341                }
342
343                outcome.setId(id);
344                if (theEntity.getDeleted() == null) {
345                        outcome.setResource(theResource);
346                }
347                outcome.setEntity(theEntity);
348
349                // Interceptor broadcast: STORAGE_PREACCESS_RESOURCES
350                if (outcome.getResource() != null) {
351                        SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(outcome.getResource());
352                        IInterceptorBroadcaster compositeBroadcaster =
353                                        CompositeInterceptorBroadcaster.newCompositeBroadcaster(getInterceptorBroadcaster(), theRequest);
354                        if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) {
355                                HookParams params = new HookParams()
356                                                .add(IPreResourceAccessDetails.class, accessDetails)
357                                                .add(RequestDetails.class, theRequest)
358                                                .addIfMatchesType(ServletRequestDetails.class, theRequest);
359                                compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params);
360                                if (accessDetails.isDontReturnResourceAtIndex(0)) {
361                                        outcome.setResource(null);
362                                }
363                        }
364                }
365
366                // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES
367                // Note that this will only fire if someone actually goes to use the
368                // resource in a response (it's their responsibility to call
369                // outcome.fireResourceViewCallback())
370                outcome.registerResourceViewCallback(() -> {
371                        if (outcome.getResource() != null) {
372                                IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster(
373                                                getInterceptorBroadcaster(), theRequest);
374                                if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) {
375                                        SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(outcome.getResource());
376                                        HookParams params = new HookParams()
377                                                        .add(IPreResourceShowDetails.class, showDetails)
378                                                        .add(RequestDetails.class, theRequest)
379                                                        .addIfMatchesType(ServletRequestDetails.class, theRequest);
380                                        compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params);
381                                        outcome.setResource(showDetails.getResource(0));
382                                }
383                        }
384                });
385
386                return outcome;
387        }
388
389        protected DaoMethodOutcome toMethodOutcomeLazy(
390                        RequestDetails theRequest,
391                        IResourcePersistentId theResourcePersistentId,
392                        @Nonnull final Supplier<LazyDaoMethodOutcome.EntityAndResource> theEntity,
393                        Supplier<IIdType> theIdSupplier) {
394                LazyDaoMethodOutcome outcome = new LazyDaoMethodOutcome(theResourcePersistentId);
395
396                outcome.setEntitySupplier(theEntity);
397                outcome.setIdSupplier(theIdSupplier);
398                outcome.setEntitySupplierUseCallback(() -> {
399                        // Interceptor broadcast: STORAGE_PREACCESS_RESOURCES
400                        if (outcome.getResource() != null) {
401                                IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster(
402                                                getInterceptorBroadcaster(), theRequest);
403                                if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) {
404                                        SimplePreResourceAccessDetails accessDetails =
405                                                        new SimplePreResourceAccessDetails(outcome.getResource());
406                                        HookParams params = new HookParams()
407                                                        .add(IPreResourceAccessDetails.class, accessDetails)
408                                                        .add(RequestDetails.class, theRequest)
409                                                        .addIfMatchesType(ServletRequestDetails.class, theRequest);
410                                        compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params);
411                                        if (accessDetails.isDontReturnResourceAtIndex(0)) {
412                                                outcome.setResource(null);
413                                        }
414                                }
415                        }
416
417                        // Interceptor broadcast: STORAGE_PRESHOW_RESOURCES
418                        // Note that this will only fire if someone actually goes to use the
419                        // resource in a response (it's their responsibility to call
420                        // outcome.fireResourceViewCallback())
421                        outcome.registerResourceViewCallback(() -> {
422                                if (outcome.getResource() != null) {
423                                        IInterceptorBroadcaster compositeBroadcaster =
424                                                        CompositeInterceptorBroadcaster.newCompositeBroadcaster(
425                                                                        getInterceptorBroadcaster(), theRequest);
426                                        if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) {
427                                                SimplePreResourceShowDetails showDetails =
428                                                                new SimplePreResourceShowDetails(outcome.getResource());
429                                                HookParams params = new HookParams()
430                                                                .add(IPreResourceShowDetails.class, showDetails)
431                                                                .add(RequestDetails.class, theRequest)
432                                                                .addIfMatchesType(ServletRequestDetails.class, theRequest);
433                                                compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params);
434                                                outcome.setResource(showDetails.getResource(0));
435                                        }
436                                }
437                        });
438                });
439
440                return outcome;
441        }
442
443        protected void doCallHooks(
444                        TransactionDetails theTransactionDetails,
445                        RequestDetails theRequestDetails,
446                        Pointcut thePointcut,
447                        HookParams theParams) {
448                if (theTransactionDetails.isAcceptingDeferredInterceptorBroadcasts(thePointcut)) {
449                        theTransactionDetails.addDeferredInterceptorBroadcast(thePointcut, theParams);
450                } else {
451                        IInterceptorBroadcaster compositeBroadcaster = CompositeInterceptorBroadcaster.newCompositeBroadcaster(
452                                        getInterceptorBroadcaster(), theRequestDetails);
453                        compositeBroadcaster.callHooks(thePointcut, theParams);
454                }
455        }
456
457        protected abstract IInterceptorBroadcaster getInterceptorBroadcaster();
458
459        public IBaseOperationOutcome createErrorOperationOutcome(String theMessage, String theCode) {
460                return createOperationOutcome(OO_SEVERITY_ERROR, theMessage, theCode);
461        }
462
463        public IBaseOperationOutcome createInfoOperationOutcome(String theMessage) {
464                return createInfoOperationOutcome(theMessage, null);
465        }
466
467        public IBaseOperationOutcome createInfoOperationOutcome(
468                        String theMessage, @Nullable StorageResponseCodeEnum theStorageResponseCode) {
469                return createOperationOutcome(
470                                OO_SEVERITY_INFO, theMessage, OO_ISSUE_CODE_INFORMATIONAL, theStorageResponseCode);
471        }
472
473        private IBaseOperationOutcome createOperationOutcome(String theSeverity, String theMessage, String theCode) {
474                return createOperationOutcome(theSeverity, theMessage, theCode, null);
475        }
476
477        protected IBaseOperationOutcome createOperationOutcome(
478                        String theSeverity,
479                        String theMessage,
480                        String theCode,
481                        @Nullable StorageResponseCodeEnum theStorageResponseCode) {
482                IBaseOperationOutcome oo = OperationOutcomeUtil.newInstance(getContext());
483                String detailSystem = null;
484                String detailCode = null;
485                String detailDescription = null;
486                if (theStorageResponseCode != null) {
487                        detailSystem = theStorageResponseCode.getSystem();
488                        detailCode = theStorageResponseCode.getCode();
489                        detailDescription = theStorageResponseCode.getDisplay();
490                }
491                OperationOutcomeUtil.addIssue(
492                                getContext(), oo, theSeverity, theMessage, null, theCode, detailSystem, detailCode, detailDescription);
493                return oo;
494        }
495
496        /**
497         * Creates a base method outcome for a delete request for the provided ID.
498         * <p>
499         * Additional information may be set on the outcome.
500         *
501         * @param theResourceId - the id of the object being deleted. Eg: Patient/123
502         */
503        protected DaoMethodOutcome createMethodOutcomeForResourceId(
504                        String theResourceId, String theMessageKey, StorageResponseCodeEnum theStorageResponseCode) {
505                DaoMethodOutcome outcome = new DaoMethodOutcome();
506
507                IIdType id = getContext().getVersion().newIdType();
508                id.setValue(theResourceId);
509                outcome.setId(id);
510
511                String message = getContext().getLocalizer().getMessage(BaseStorageDao.class, theMessageKey, id);
512                String severity = "information";
513                String code = "informational";
514                IBaseOperationOutcome oo = createOperationOutcome(severity, message, code, theStorageResponseCode);
515                outcome.setOperationOutcome(oo);
516
517                return outcome;
518        }
519
520        @Nonnull
521        protected ResourceGoneException createResourceGoneException(IBasePersistedResource theResourceEntity) {
522                StringBuilder b = new StringBuilder();
523                b.append("Resource was deleted at ");
524                b.append(new InstantType(theResourceEntity.getDeleted()).getValueAsString());
525                ResourceGoneException retVal = new ResourceGoneException(b.toString());
526                retVal.setResourceId(theResourceEntity.getIdDt());
527                return retVal;
528        }
529
530        /**
531         * Provide the JpaStorageSettings
532         */
533        protected abstract JpaStorageSettings getStorageSettings();
534
535        /**
536         * Returns the resource type for this DAO, or null if this is a system-level DAO
537         */
538        @Nullable
539        protected abstract String getResourceName();
540
541        /**
542         * Provides the FHIR context
543         */
544        protected abstract FhirContext getContext();
545
546        @Transactional(propagation = Propagation.SUPPORTS)
547        public void translateRawParameters(Map<String, List<String>> theSource, SearchParameterMap theTarget) {
548                if (theSource == null || theSource.isEmpty()) {
549                        return;
550                }
551
552                ResourceSearchParams searchParams = mySearchParamRegistry.getActiveSearchParams(
553                                getResourceName(), ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
554
555                Set<String> paramNames = theSource.keySet();
556                for (String nextParamName : paramNames) {
557                        QualifierDetails qualifiedParamName = QualifierDetails.extractQualifiersFromParameterName(nextParamName);
558                        RuntimeSearchParam param = searchParams.get(qualifiedParamName.getParamName());
559                        if (param == null) {
560                                Collection<String> validNames = mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(
561                                                getResourceName(), ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
562                                RuntimeSearchParam notEnabledForSearchParam = mySearchParamRegistry.getActiveSearchParam(
563                                                getResourceName(),
564                                                qualifiedParamName.getParamName(),
565                                                ISearchParamRegistry.SearchParamLookupContextEnum.ALL);
566                                if (notEnabledForSearchParam != null) {
567                                        String msg = getContext()
568                                                        .getLocalizer()
569                                                        .getMessageSanitized(
570                                                                        BaseStorageDao.class,
571                                                                        "invalidSearchParameterNotEnabledForSearch",
572                                                                        qualifiedParamName.getParamName(),
573                                                                        getResourceName(),
574                                                                        validNames);
575                                        throw new InvalidRequestException(Msg.code(2539) + msg);
576                                } else {
577                                        String msg = getContext()
578                                                        .getLocalizer()
579                                                        .getMessageSanitized(
580                                                                        BaseStorageDao.class,
581                                                                        "invalidSearchParameter",
582                                                                        qualifiedParamName.getParamName(),
583                                                                        getResourceName(),
584                                                                        validNames);
585                                        throw new InvalidRequestException(Msg.code(524) + msg);
586                                }
587                        }
588
589                        // Should not be null since the check above would have caught it
590                        RuntimeSearchParam paramDef = mySearchParamRegistry.getActiveSearchParam(
591                                        getResourceName(),
592                                        qualifiedParamName.getParamName(),
593                                        ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
594
595                        for (String nextValue : theSource.get(nextParamName)) {
596                                QualifiedParamList qualifiedParam = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(
597                                                qualifiedParamName.getWholeQualifier(), nextValue);
598                                List<QualifiedParamList> paramList = Collections.singletonList(qualifiedParam);
599                                IQueryParameterAnd<?> parsedParam = JpaParamUtil.parseQueryParams(
600                                                mySearchParamRegistry, getContext(), paramDef, nextParamName, paramList);
601                                theTarget.add(qualifiedParamName.getParamName(), parsedParam);
602                        }
603                }
604        }
605
606        protected void populateOperationOutcomeForUpdate(
607                        @Nullable StopWatch theItemStopwatch,
608                        DaoMethodOutcome theMethodOutcome,
609                        String theMatchUrl,
610                        RestOperationTypeEnum theOperationType,
611                        TransactionDetails theTransactionDetails) {
612                String msg;
613                StorageResponseCodeEnum outcome;
614
615                if (theOperationType == RestOperationTypeEnum.PATCH) {
616
617                        if (theMatchUrl != null) {
618                                if (theMethodOutcome.isNop()) {
619                                        outcome = StorageResponseCodeEnum.SUCCESSFUL_CONDITIONAL_PATCH_NO_CHANGE;
620                                        msg = getContext()
621                                                        .getLocalizer()
622                                                        .getMessageSanitized(
623                                                                        BaseStorageDao.class,
624                                                                        "successfulPatchConditionalNoChange",
625                                                                        theMethodOutcome.getId(),
626                                                                        UrlUtil.sanitizeUrlPart(theMatchUrl),
627                                                                        theMethodOutcome.getId());
628                                } else {
629                                        outcome = StorageResponseCodeEnum.SUCCESSFUL_CONDITIONAL_PATCH;
630                                        msg = getContext()
631                                                        .getLocalizer()
632                                                        .getMessageSanitized(
633                                                                        BaseStorageDao.class,
634                                                                        "successfulPatchConditional",
635                                                                        theMethodOutcome.getId(),
636                                                                        UrlUtil.sanitizeUrlPart(theMatchUrl),
637                                                                        theMethodOutcome.getId());
638                                }
639                        } else {
640                                if (theMethodOutcome.isNop()) {
641                                        outcome = StorageResponseCodeEnum.SUCCESSFUL_PATCH_NO_CHANGE;
642                                        msg = getContext()
643                                                        .getLocalizer()
644                                                        .getMessageSanitized(
645                                                                        BaseStorageDao.class, "successfulPatchNoChange", theMethodOutcome.getId());
646                                } else {
647                                        outcome = StorageResponseCodeEnum.SUCCESSFUL_PATCH;
648                                        msg = getContext()
649                                                        .getLocalizer()
650                                                        .getMessageSanitized(BaseStorageDao.class, "successfulPatch", theMethodOutcome.getId());
651                                }
652                        }
653
654                } else if (theOperationType == RestOperationTypeEnum.CREATE) {
655
656                        if (theMatchUrl == null) {
657                                outcome = StorageResponseCodeEnum.SUCCESSFUL_CREATE;
658                                msg = getContext()
659                                                .getLocalizer()
660                                                .getMessageSanitized(BaseStorageDao.class, "successfulCreate", theMethodOutcome.getId());
661                        } else if (theMethodOutcome.isNop()) {
662                                outcome = StorageResponseCodeEnum.SUCCESSFUL_CREATE_WITH_CONDITIONAL_MATCH;
663                                msg = getContext()
664                                                .getLocalizer()
665                                                .getMessageSanitized(
666                                                                BaseStorageDao.class,
667                                                                "successfulCreateConditionalWithMatch",
668                                                                theMethodOutcome.getId(),
669                                                                UrlUtil.sanitizeUrlPart(theMatchUrl));
670                        } else {
671                                outcome = StorageResponseCodeEnum.SUCCESSFUL_CREATE_NO_CONDITIONAL_MATCH;
672                                msg = getContext()
673                                                .getLocalizer()
674                                                .getMessageSanitized(
675                                                                BaseStorageDao.class,
676                                                                "successfulCreateConditionalNoMatch",
677                                                                theMethodOutcome.getId(),
678                                                                UrlUtil.sanitizeUrlPart(theMatchUrl));
679                        }
680
681                } else if (theMethodOutcome.isNop()) {
682
683                        if (theMatchUrl != null) {
684                                outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_WITH_CONDITIONAL_MATCH_NO_CHANGE;
685                                msg = getContext()
686                                                .getLocalizer()
687                                                .getMessageSanitized(
688                                                                BaseStorageDao.class,
689                                                                "successfulUpdateConditionalNoChangeWithMatch",
690                                                                theMethodOutcome.getId(),
691                                                                theMatchUrl);
692                        } else {
693                                outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_NO_CHANGE;
694                                msg = getContext()
695                                                .getLocalizer()
696                                                .getMessageSanitized(
697                                                                BaseStorageDao.class, "successfulUpdateNoChange", theMethodOutcome.getId());
698                        }
699
700                } else {
701
702                        if (theMatchUrl != null) {
703                                if (theMethodOutcome.getCreated() == Boolean.TRUE) {
704                                        outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_NO_CONDITIONAL_MATCH;
705                                        msg = getContext()
706                                                        .getLocalizer()
707                                                        .getMessageSanitized(
708                                                                        BaseStorageDao.class,
709                                                                        "successfulUpdateConditionalNoMatch",
710                                                                        theMethodOutcome.getId());
711                                } else {
712                                        outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_WITH_CONDITIONAL_MATCH;
713                                        msg = getContext()
714                                                        .getLocalizer()
715                                                        .getMessageSanitized(
716                                                                        BaseStorageDao.class,
717                                                                        "successfulUpdateConditionalWithMatch",
718                                                                        theMethodOutcome.getId(),
719                                                                        theMatchUrl);
720                                }
721                        } else if (theMethodOutcome.getCreated() == Boolean.TRUE) {
722                                outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE_AS_CREATE;
723                                msg = getContext()
724                                                .getLocalizer()
725                                                .getMessageSanitized(
726                                                                BaseStorageDao.class, "successfulUpdateAsCreate", theMethodOutcome.getId());
727                        } else {
728                                outcome = StorageResponseCodeEnum.SUCCESSFUL_UPDATE;
729                                msg = getContext()
730                                                .getLocalizer()
731                                                .getMessageSanitized(BaseStorageDao.class, "successfulUpdate", theMethodOutcome.getId());
732                        }
733                }
734
735                if (theItemStopwatch != null) {
736                        String msgSuffix = getContext()
737                                        .getLocalizer()
738                                        .getMessageSanitized(BaseStorageDao.class, "successfulTimingSuffix", theItemStopwatch.getMillis());
739                        msg = msg + " " + msgSuffix;
740                }
741
742                IBaseOperationOutcome oo = createInfoOperationOutcome(msg, outcome);
743
744                if (theTransactionDetails != null) {
745                        List<IIdType> autoCreatedPlaceholderResources =
746                                        theTransactionDetails.getAutoCreatedPlaceholderResourcesAndClear();
747                        for (IIdType next : autoCreatedPlaceholderResources) {
748                                msg = addIssueToOperationOutcomeForAutoCreatedPlaceholder(getContext(), next, oo);
749                        }
750                }
751
752                theMethodOutcome.setOperationOutcome(oo);
753                ourLog.debug(msg);
754        }
755
756        public static String addIssueToOperationOutcomeForAutoCreatedPlaceholder(
757                        FhirContext theFhirContext, IIdType thePlaceholderId, IBaseOperationOutcome theOperationOutcomeToPopulate) {
758                String msg;
759                msg = theFhirContext
760                                .getLocalizer()
761                                .getMessageSanitized(BaseStorageDao.class, "successfulAutoCreatePlaceholder", thePlaceholderId);
762                String detailSystem = StorageResponseCodeEnum.AUTOMATICALLY_CREATED_PLACEHOLDER_RESOURCE.getSystem();
763                String detailCode = StorageResponseCodeEnum.AUTOMATICALLY_CREATED_PLACEHOLDER_RESOURCE.getCode();
764                String detailDescription = StorageResponseCodeEnum.AUTOMATICALLY_CREATED_PLACEHOLDER_RESOURCE.getDisplay();
765                OperationOutcomeUtil.addIssue(
766                                theFhirContext,
767                                theOperationOutcomeToPopulate,
768                                OO_SEVERITY_INFO,
769                                msg,
770                                null,
771                                OO_ISSUE_CODE_INFORMATIONAL,
772                                detailSystem,
773                                detailCode,
774                                detailDescription);
775                return msg;
776        }
777
778        /**
779         * Extracts a list of references that have versions in their ID whose versions should not be stripped
780         *
781         * @return A set of references that should not have their client-given versions stripped according to the
782         *                 versioned references settings.
783         */
784        public static Set<IBaseReference> extractReferencesToAvoidReplacement(
785                        FhirContext theFhirContext, IBaseResource theResource) {
786                if (!theFhirContext
787                                .getParserOptions()
788                                .getDontStripVersionsFromReferencesAtPaths()
789                                .isEmpty()) {
790                        String theResourceType = theFhirContext.getResourceType(theResource);
791                        Set<String> versionReferencesPaths = theFhirContext
792                                        .getParserOptions()
793                                        .getDontStripVersionsFromReferencesAtPathsByResourceType(theResourceType);
794                        return getReferencesWithOrWithoutVersionId(versionReferencesPaths, theFhirContext, theResource, false);
795                }
796                return Collections.emptySet();
797        }
798
799        /**
800         * Extracts a list of references that should be auto-versioned.
801         *
802         * @return A set of references that should be versioned according to both storage settings
803         *                 and auto-version reference extensions, or it may also be empty.
804         */
805        @Nonnull
806        public static Set<IBaseReference> extractReferencesToAutoVersion(
807                        FhirContext theFhirContext, StorageSettings theStorageSettings, IBaseResource theResource) {
808                Set<IBaseReference> referencesToAutoVersionFromConfig =
809                                getReferencesToAutoVersionFromConfig(theFhirContext, theStorageSettings, theResource);
810
811                Set<IBaseReference> referencesToAutoVersionFromExtensions =
812                                getReferencesToAutoVersionFromExtension(theFhirContext, theResource);
813
814                return Stream.concat(referencesToAutoVersionFromConfig.stream(), referencesToAutoVersionFromExtensions.stream())
815                                .collect(Collectors.toMap(ref -> ref, ref -> ref, (oldRef, newRef) -> oldRef, IdentityHashMap::new))
816                                .keySet();
817        }
818
819        /**
820         * Extracts a list of references that should be auto-versioned according to
821         * <code>auto-version-references-at-path</code> extensions.
822         * @see HapiExtensions#EXTENSION_AUTO_VERSION_REFERENCES_AT_PATH
823         */
824        @Nonnull
825        private static Set<IBaseReference> getReferencesToAutoVersionFromExtension(
826                        FhirContext theFhirContext, IBaseResource theResource) {
827                String resourceType = theFhirContext.getResourceType(theResource);
828                Set<String> autoVersionReferencesAtPaths =
829                                MetaUtil.getAutoVersionReferencesAtPath(theResource.getMeta(), resourceType);
830
831                if (!autoVersionReferencesAtPaths.isEmpty()) {
832                        return getReferencesWithOrWithoutVersionId(autoVersionReferencesAtPaths, theFhirContext, theResource, true);
833                }
834                return Collections.emptySet();
835        }
836
837        /**
838         * Extracts a list of references that should be auto-versioned according to storage configuration.
839         * @see StorageSettings#getAutoVersionReferenceAtPaths()
840         */
841        @Nonnull
842        private static Set<IBaseReference> getReferencesToAutoVersionFromConfig(
843                        FhirContext theFhirContext, StorageSettings theStorageSettings, IBaseResource theResource) {
844                if (!theStorageSettings.getAutoVersionReferenceAtPaths().isEmpty()) {
845                        String resourceName = theFhirContext.getResourceType(theResource);
846                        Set<String> autoVersionReferencesPaths =
847                                        theStorageSettings.getAutoVersionReferenceAtPathsByResourceType(resourceName);
848                        return getReferencesWithOrWithoutVersionId(autoVersionReferencesPaths, theFhirContext, theResource, true);
849                }
850                return Collections.emptySet();
851        }
852
853        /**
854         * Extracts references from given resource and filters references by those with versions, or those without versions.
855         *
856         * @param theVersionReferencesPaths the paths from which to extract references from
857         * @param theFhirContext the FHIR context
858         * @param theResource the resource from which to extract references from
859         * @param theShouldFilterByRefsWithoutVersionId If true, this method will return only references without a version. If false, this method will return only references with a version.
860         * @return Set of references contained in the resource with or without versions
861         */
862        private static Set<IBaseReference> getReferencesWithOrWithoutVersionId(
863                        Set<String> theVersionReferencesPaths,
864                        FhirContext theFhirContext,
865                        IBaseResource theResource,
866                        boolean theShouldFilterByRefsWithoutVersionId) {
867                return theVersionReferencesPaths.stream()
868                                .map(fullPath -> theFhirContext.newTerser().getValues(theResource, fullPath, IBaseReference.class))
869                                .flatMap(Collection::stream)
870                                .filter(reference ->
871                                                reference.getReferenceElement().hasVersionIdPart() ^ theShouldFilterByRefsWithoutVersionId)
872                                .collect(Collectors.toMap(ref -> ref, ref -> ref, (oldRef, newRef) -> oldRef, IdentityHashMap::new))
873                                .keySet();
874        }
875
876        public static void clearRequestAsProcessingSubRequest(RequestDetails theRequestDetails) {
877                if (theRequestDetails != null) {
878                        theRequestDetails.getUserData().remove(PROCESSING_SUB_REQUEST);
879                }
880        }
881
882        public static void markRequestAsProcessingSubRequest(RequestDetails theRequestDetails) {
883                if (theRequestDetails != null) {
884                        theRequestDetails.getUserData().put(PROCESSING_SUB_REQUEST, Boolean.TRUE);
885                }
886        }
887}