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.jpa.searchparam.matcher.InMemoryMatchResult;
024import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionDeliveryRequest;
025import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionMatchDeliverer;
026import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription;
027import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry;
028import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
029import ca.uhn.fhir.jpa.subscription.model.CanonicalTopicSubscription;
030import ca.uhn.fhir.jpa.topic.filter.ISubscriptionTopicFilterMatcher;
031import ca.uhn.fhir.jpa.topic.filter.SubscriptionTopicFilterUtil;
032import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
033import ca.uhn.fhir.util.Logs;
034import org.hl7.fhir.instance.model.api.IBaseBundle;
035import org.hl7.fhir.instance.model.api.IBaseResource;
036import org.slf4j.Logger;
037
038import java.util.List;
039import java.util.UUID;
040
041/**
042 * Subscription topic notifications are natively supported in R5, R4B.  They are also partially supported and in R4
043 * via the subscription backport spec <a href="http://build.fhir.org/ig/HL7/fhir-subscription-backport-ig/components.html">Subscription Backport</a>.
044 * In all versions, it is possible for a FHIR Repository to submit topic subscription notifications triggered by some
045 * arbitrary "business event".  In R5 and R4B most subscription topic notifications will be triggered by a SubscriptionTopic
046 * match.  However, in the R4 backport, the SubscriptionTopic is not supported and the SubscriptionTopicDispatcher service
047 * is provided to generate those notifications instead.  Any custom java extension to the FHIR repository can @Autowire this service to
048 * send topic notifications to all Subscription resources subscribed to that topic.
049 */
050public class SubscriptionTopicDispatcher {
051        private static final Logger ourLog = Logs.getSubscriptionTopicLog();
052        private final FhirContext myFhirContext;
053        private final SubscriptionRegistry mySubscriptionRegistry;
054        private final SubscriptionMatchDeliverer mySubscriptionMatchDeliverer;
055        private final SubscriptionTopicPayloadBuilder mySubscriptionTopicPayloadBuilder;
056
057        public SubscriptionTopicDispatcher(
058                        FhirContext theFhirContext,
059                        SubscriptionRegistry theSubscriptionRegistry,
060                        SubscriptionMatchDeliverer theSubscriptionMatchDeliverer,
061                        SubscriptionTopicPayloadBuilder theSubscriptionTopicPayloadBuilder) {
062                myFhirContext = theFhirContext;
063                mySubscriptionRegistry = theSubscriptionRegistry;
064                mySubscriptionMatchDeliverer = theSubscriptionMatchDeliverer;
065                mySubscriptionTopicPayloadBuilder = theSubscriptionTopicPayloadBuilder;
066        }
067
068        /**
069         * Deliver a Subscription topic notification to all subscriptions for the given topic.
070         *
071         * @param theTopicUrl    Deliver to subscriptions for this topic
072         * @param theResources   The list of resources to deliver.  The first resource will be the primary "focus" resource per the Subscription documentation.
073         *                       This list should _not_ include the SubscriptionStatus.  The SubscriptionStatus will be added as the first element to
074         *                       the delivered bundle.  The reason for this is that the SubscriptionStatus needs to reference the subscription ID, which is
075         *                       not known until the bundle is delivered.
076         * @param theRequestType The type of request that led to this dispatch.  This determines the request type of the bundle entries
077         * @return The number of subscription notifications that were successfully queued for delivery
078         */
079        public int dispatch(String theTopicUrl, List<IBaseResource> theResources, RestOperationTypeEnum theRequestType) {
080                SubscriptionTopicDispatchRequest subscriptionTopicDispatchRequest = new SubscriptionTopicDispatchRequest(
081                                theTopicUrl,
082                                theResources,
083                                (f, r) -> InMemoryMatchResult.successfulMatch(),
084                                theRequestType,
085                                null,
086                                null,
087                                null);
088                return dispatch(subscriptionTopicDispatchRequest);
089        }
090
091        /**
092         * Deliver a Subscription topic notification to all subscriptions for the given topic.
093         *
094         * @param theSubscriptionTopicDispatchRequest contains the topic URL, the list of resources to deliver, and the request type
095         * @return The number of subscription notifications that were successfully queued for delivery
096         */
097        public int dispatch(SubscriptionTopicDispatchRequest theSubscriptionTopicDispatchRequest) {
098                int count = 0;
099
100                List<ActiveSubscription> topicSubscriptions =
101                                mySubscriptionRegistry.getTopicSubscriptionsByTopic(theSubscriptionTopicDispatchRequest.getTopicUrl());
102                if (!topicSubscriptions.isEmpty()) {
103                        for (ActiveSubscription activeSubscription : topicSubscriptions) {
104                                boolean success = matchFiltersAndDeliver(theSubscriptionTopicDispatchRequest, activeSubscription);
105                                if (success) {
106                                        count++;
107                                }
108                        }
109                }
110                return count;
111        }
112
113        private boolean matchFiltersAndDeliver(
114                        SubscriptionTopicDispatchRequest theSubscriptionTopicDispatchRequest,
115                        ActiveSubscription theActiveSubscription) {
116
117                String topicUrl = theSubscriptionTopicDispatchRequest.getTopicUrl();
118                List<IBaseResource> resources = theSubscriptionTopicDispatchRequest.getResources();
119                ISubscriptionTopicFilterMatcher subscriptionTopicFilterMatcher =
120                                theSubscriptionTopicDispatchRequest.getSubscriptionTopicFilterMatcher();
121
122                if (resources.size() > 0) {
123                        IBaseResource firstResource = resources.get(0);
124                        String resourceType = myFhirContext.getResourceType(firstResource);
125                        CanonicalSubscription subscription = theActiveSubscription.getSubscription();
126                        CanonicalTopicSubscription topicSubscription = subscription.getTopicSubscription();
127                        if (topicSubscription.hasFilters()) {
128                                ourLog.debug(
129                                                "Checking if resource {} matches {} subscription filters on {}",
130                                                firstResource.getIdElement().toUnqualifiedVersionless().getValue(),
131                                                topicSubscription.getFilters().size(),
132                                                subscription
133                                                                .getIdElement(myFhirContext)
134                                                                .toUnqualifiedVersionless()
135                                                                .getValue());
136
137                                if (!SubscriptionTopicFilterUtil.matchFilters(
138                                                firstResource, resourceType, subscriptionTopicFilterMatcher, topicSubscription)) {
139                                        return false;
140                                }
141                        }
142                }
143                theActiveSubscription.incrementDeliveriesCount();
144                IBaseBundle bundlePayload = mySubscriptionTopicPayloadBuilder.buildPayload(
145                                resources, theActiveSubscription, topicUrl, theSubscriptionTopicDispatchRequest.getRequestType());
146                bundlePayload.setId(UUID.randomUUID().toString());
147                SubscriptionDeliveryRequest subscriptionDeliveryRequest = new SubscriptionDeliveryRequest(
148                                bundlePayload, theActiveSubscription, theSubscriptionTopicDispatchRequest);
149                return mySubscriptionMatchDeliverer.deliverPayload(
150                                subscriptionDeliveryRequest, theSubscriptionTopicDispatchRequest.getInMemoryMatchResult());
151        }
152}