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