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