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.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}