001/*-
002 * #%L
003 * HAPI FHIR Subscription Server
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.topic;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.interceptor.api.Hook;
025import ca.uhn.fhir.interceptor.api.Pointcut;
026import ca.uhn.fhir.interceptor.model.RequestPartitionId;
027import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
028import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator;
029import ca.uhn.fhir.parser.DataFormatException;
030import ca.uhn.fhir.rest.api.server.RequestDetails;
031import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
032import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
033import ca.uhn.fhir.util.Logs;
034import com.google.common.annotations.VisibleForTesting;
035import org.hl7.fhir.instance.model.api.IBaseResource;
036import org.hl7.fhir.r5.model.SubscriptionTopic;
037import org.slf4j.Logger;
038
039public class SubscriptionTopicValidatingInterceptor {
040        private static final Logger ourLog = Logs.getSubscriptionTopicLog();
041
042        private final FhirContext myFhirContext;
043        private final SubscriptionQueryValidator mySubscriptionQueryValidator;
044
045        public SubscriptionTopicValidatingInterceptor(
046                        FhirContext theFhirContext, SubscriptionQueryValidator theSubscriptionQueryValidator) {
047                myFhirContext = theFhirContext;
048                mySubscriptionQueryValidator = theSubscriptionQueryValidator;
049        }
050
051        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED)
052        public void resourcePreCreate(
053                        IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) {
054                validateSubmittedSubscriptionTopic(
055                                theResource, theRequestDetails, theRequestPartitionId, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED);
056        }
057
058        @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED)
059        public void resourceUpdated(
060                        IBaseResource theOldResource,
061                        IBaseResource theResource,
062                        RequestDetails theRequestDetails,
063                        RequestPartitionId theRequestPartitionId) {
064                validateSubmittedSubscriptionTopic(
065                                theResource, theRequestDetails, theRequestPartitionId, Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED);
066        }
067
068        @VisibleForTesting
069        void validateSubmittedSubscriptionTopic(
070                        IBaseResource theSubscription,
071                        RequestDetails theRequestDetails,
072                        RequestPartitionId theRequestPartitionId,
073                        Pointcut thePointcut) {
074                if (Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED != thePointcut
075                                && Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED != thePointcut) {
076                        throw new UnprocessableEntityException(Msg.code(2340)
077                                        + "Expected Pointcut to be either STORAGE_PRESTORAGE_RESOURCE_CREATED or STORAGE_PRESTORAGE_RESOURCE_UPDATED but was: "
078                                        + thePointcut);
079                }
080
081                if (!"SubscriptionTopic".equals(myFhirContext.getResourceType(theSubscription))) {
082                        return;
083                }
084
085                SubscriptionTopic subscriptionTopic =
086                                SubscriptionTopicCanonicalizer.canonicalizeTopic(myFhirContext, theSubscription);
087
088                boolean finished = false;
089                if (subscriptionTopic.getStatus() == null) {
090                        throw new UnprocessableEntityException(
091                                        Msg.code(2338)
092                                                        + "Can not process submitted SubscriptionTopic - SubscriptionTopic.status must be populated on this server");
093                }
094
095                switch (subscriptionTopic.getStatus()) {
096                        case ACTIVE:
097                                break;
098                        default:
099                                finished = true;
100                                break;
101                }
102
103                // WIP STR5 add cross-partition support like in SubscriptionValidatingInterceptor
104
105                // WIP STR5 warn if the SubscriptionTopic criteria can't be evaluated in memory?  Do we want to annotate the
106                //  strategy with an extension like Subscription?
107
108                if (!finished) {
109                        subscriptionTopic.getResourceTrigger().stream().forEach(t -> validateQueryCriteria(t.getQueryCriteria()));
110                }
111        }
112
113        private void validateQueryCriteria(
114                        SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent theQueryCriteria) {
115                if (theQueryCriteria.getPrevious() != null) {
116                        validateCriteria(
117                                        theQueryCriteria.getPrevious(), "SubscriptionTopic.resourceTrigger.queryCriteria.previous");
118                }
119                if (theQueryCriteria.getCurrent() != null) {
120                        validateCriteria(theQueryCriteria.getCurrent(), "SubscriptionTopic.resourceTrigger.queryCriteria.current");
121                }
122        }
123
124        public void validateCriteria(String theCriteria, String theFieldName) {
125                try {
126                        mySubscriptionQueryValidator.validateCriteria(theCriteria, theFieldName);
127                        SubscriptionMatchingStrategy strategy = mySubscriptionQueryValidator.determineStrategy(theCriteria);
128                        if (strategy != SubscriptionMatchingStrategy.IN_MEMORY) {
129                                ourLog.warn(
130                                                "Warning: Query Criteria '{}' in {} cannot be evaluated in-memory", theCriteria, theFieldName);
131                        }
132                } catch (InvalidRequestException | DataFormatException e) {
133                        throw new UnprocessableEntityException(Msg.code(2339) + "Invalid SubscriptionTopic criteria '" + theCriteria
134                                        + "' in " + theFieldName + ": " + e.getMessage());
135                }
136        }
137}