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