001package ca.uhn.fhir.jpa.subscription.match.matcher.subscriber; 002 003import ca.uhn.fhir.context.FhirContext; 004import ca.uhn.fhir.i18n.Msg; 005import ca.uhn.fhir.interceptor.api.HookParams; 006import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 007import ca.uhn.fhir.interceptor.api.Pointcut; 008import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult; 009import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelRegistry; 010import ca.uhn.fhir.jpa.subscription.match.matcher.matching.ISubscriptionMatcher; 011import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription; 012import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry; 013import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; 014import ca.uhn.fhir.jpa.subscription.model.ResourceDeliveryJsonMessage; 015import ca.uhn.fhir.jpa.subscription.model.ResourceDeliveryMessage; 016import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage; 017import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; 018import ca.uhn.fhir.rest.api.EncodingEnum; 019import org.hl7.fhir.instance.model.api.IBaseResource; 020import org.hl7.fhir.instance.model.api.IIdType; 021import org.slf4j.Logger; 022import org.slf4j.LoggerFactory; 023import org.springframework.beans.factory.annotation.Autowired; 024import org.springframework.messaging.Message; 025import org.springframework.messaging.MessageChannel; 026import org.springframework.messaging.MessageHandler; 027import org.springframework.messaging.MessagingException; 028 029import javax.annotation.Nonnull; 030import java.util.Collection; 031 032import static ca.uhn.fhir.rest.server.messaging.BaseResourceMessage.OperationTypeEnum.DELETE; 033import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; 034import static org.apache.commons.lang3.StringUtils.isNotBlank; 035 036/*- 037 * #%L 038 * HAPI FHIR Subscription Server 039 * %% 040 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 041 * %% 042 * Licensed under the Apache License, Version 2.0 (the "License"); 043 * you may not use this file except in compliance with the License. 044 * You may obtain a copy of the License at 045 * 046 * http://www.apache.org/licenses/LICENSE-2.0 047 * 048 * Unless required by applicable law or agreed to in writing, software 049 * distributed under the License is distributed on an "AS IS" BASIS, 050 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 051 * See the License for the specific language governing permissions and 052 * limitations under the License. 053 * #L% 054 */ 055 056public class SubscriptionMatchingSubscriber implements MessageHandler { 057 private final Logger ourLog = LoggerFactory.getLogger(SubscriptionMatchingSubscriber.class); 058 public static final String SUBSCRIPTION_MATCHING_CHANNEL_NAME = "subscription-matching"; 059 060 @Autowired 061 private ISubscriptionMatcher mySubscriptionMatcher; 062 @Autowired 063 private FhirContext myFhirContext; 064 @Autowired 065 private SubscriptionRegistry mySubscriptionRegistry; 066 @Autowired 067 private IInterceptorBroadcaster myInterceptorBroadcaster; 068 @Autowired 069 private SubscriptionChannelRegistry mySubscriptionChannelRegistry; 070 071 /** 072 * Constructor 073 */ 074 public SubscriptionMatchingSubscriber() { 075 super(); 076 } 077 078 @Override 079 public void handleMessage(@Nonnull Message<?> theMessage) throws MessagingException { 080 ourLog.trace("Handling resource modified message: {}", theMessage); 081 082 if (!(theMessage instanceof ResourceModifiedJsonMessage)) { 083 ourLog.warn("Unexpected message payload type: {}", theMessage); 084 return; 085 } 086 087 ResourceModifiedMessage msg = ((ResourceModifiedJsonMessage) theMessage).getPayload(); 088 matchActiveSubscriptionsAndDeliver(msg); 089 } 090 091 public void matchActiveSubscriptionsAndDeliver(ResourceModifiedMessage theMsg) { 092 switch (theMsg.getOperationType()) { 093 case CREATE: 094 case UPDATE: 095 case MANUALLY_TRIGGERED: 096 case DELETE: 097 break; 098 default: 099 ourLog.trace("Not processing modified message for {}", theMsg.getOperationType()); 100 // ignore anything else 101 return; 102 } 103 104 // Interceptor call: SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED 105 HookParams params = new HookParams() 106 .add(ResourceModifiedMessage.class, theMsg); 107 if (!myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED, params)) { 108 return; 109 } 110 111 try { 112 doMatchActiveSubscriptionsAndDeliver(theMsg); 113 } finally { 114 // Interceptor call: SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED 115 myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_PERSISTED_RESOURCE_CHECKED, params); 116 } 117 } 118 119 private void doMatchActiveSubscriptionsAndDeliver(ResourceModifiedMessage theMsg) { 120 IIdType resourceId = theMsg.getPayloadId(myFhirContext); 121 122 Collection<ActiveSubscription> subscriptions = mySubscriptionRegistry.getAll(); 123 124 ourLog.trace("Testing {} subscriptions for applicability", subscriptions.size()); 125 boolean resourceMatched = false; 126 127 for (ActiveSubscription nextActiveSubscription : subscriptions) { 128 // skip if the partitions don't match 129 CanonicalSubscription subscription = nextActiveSubscription.getSubscription(); 130 if (subscription != null && subscription.getRequestPartitionId() != null && theMsg.getPartitionId() != null && 131 theMsg.getPartitionId().hasPartitionIds() && !subscription.getCrossPartitionEnabled() && 132 !theMsg.getPartitionId().hasPartitionId(subscription.getRequestPartitionId())) { 133 continue; 134 } 135 String nextSubscriptionId = getId(nextActiveSubscription); 136 137 if (isNotBlank(theMsg.getSubscriptionId())) { 138 if (!theMsg.getSubscriptionId().equals(nextSubscriptionId)) { 139 // TODO KHS we should use a hash to look it up instead of this full table scan 140 ourLog.debug("Ignoring subscription {} because it is not {}", nextSubscriptionId, theMsg.getSubscriptionId()); 141 continue; 142 } 143 } 144 145 if (!resourceTypeIsAppropriateForSubscription(nextActiveSubscription, resourceId)) { 146 continue; 147 } 148 149 if (theMsg.getOperationType().equals(DELETE)) { 150 if (!nextActiveSubscription.getSubscription().getSendDeleteMessages()) { 151 ourLog.trace("Not processing modified message for {}", theMsg.getOperationType()); 152 return; 153 } 154 } 155 156 InMemoryMatchResult matchResult; 157 if (nextActiveSubscription.getCriteria().getType() == SubscriptionCriteriaParser.TypeEnum.SEARCH_EXPRESSION) { 158 matchResult = mySubscriptionMatcher.match(nextActiveSubscription.getSubscription(), theMsg); 159 if (!matchResult.matched()) { 160 continue; 161 } 162 ourLog.debug("Subscription {} was matched by resource {} {}", 163 nextActiveSubscription.getId(), 164 resourceId.toUnqualifiedVersionless().getValue(), 165 matchResult.isInMemory() ? "in-memory" : "by querying the repository"); 166 } else { 167 matchResult = InMemoryMatchResult.successfulMatch(); 168 matchResult.setInMemory(true); 169 } 170 171 IBaseResource payload = theMsg.getNewPayload(myFhirContext); 172 173 EncodingEnum encoding = null; 174 if (subscription != null && subscription.getPayloadString() != null && !subscription.getPayloadString().isEmpty()) { 175 encoding = EncodingEnum.forContentType(subscription.getPayloadString()); 176 } 177 encoding = defaultIfNull(encoding, EncodingEnum.JSON); 178 179 ResourceDeliveryMessage deliveryMsg = new ResourceDeliveryMessage(); 180 deliveryMsg.setPartitionId(theMsg.getPartitionId()); 181 182 if (payload != null) { 183 deliveryMsg.setPayload(myFhirContext, payload, encoding); 184 } else { 185 deliveryMsg.setPayloadId(theMsg.getPayloadId(myFhirContext)); 186 } 187 deliveryMsg.setSubscription(subscription); 188 deliveryMsg.setOperationType(theMsg.getOperationType()); 189 deliveryMsg.setTransactionId(theMsg.getTransactionId()); 190 deliveryMsg.copyAdditionalPropertiesFrom(theMsg); 191 192 // Interceptor call: SUBSCRIPTION_RESOURCE_MATCHED 193 HookParams params = new HookParams() 194 .add(CanonicalSubscription.class, nextActiveSubscription.getSubscription()) 195 .add(ResourceDeliveryMessage.class, deliveryMsg) 196 .add(InMemoryMatchResult.class, matchResult); 197 if (!myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_RESOURCE_MATCHED, params)) { 198 return; 199 } 200 201 resourceMatched |= sendToDeliveryChannel(nextActiveSubscription, deliveryMsg); 202 } 203 204 if (!resourceMatched) { 205 // Interceptor call: SUBSCRIPTION_RESOURCE_MATCHED 206 HookParams params = new HookParams() 207 .add(ResourceModifiedMessage.class, theMsg); 208 myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_RESOURCE_DID_NOT_MATCH_ANY_SUBSCRIPTIONS, params); 209 } 210 } 211 212 private boolean sendToDeliveryChannel(ActiveSubscription nextActiveSubscription, ResourceDeliveryMessage theDeliveryMsg) { 213 boolean retVal = false; 214 ResourceDeliveryJsonMessage wrappedMsg = new ResourceDeliveryJsonMessage(theDeliveryMsg); 215 MessageChannel deliveryChannel = mySubscriptionChannelRegistry.getDeliverySenderChannel(nextActiveSubscription.getChannelName()); 216 if (deliveryChannel != null) { 217 retVal = true; 218 trySendToDeliveryChannel(wrappedMsg, deliveryChannel); 219 } else { 220 ourLog.warn("Do not have delivery channel for subscription {}", nextActiveSubscription.getId()); 221 } 222 return retVal; 223 } 224 225 private void trySendToDeliveryChannel(ResourceDeliveryJsonMessage theWrappedMsg, MessageChannel theDeliveryChannel) { 226 try { 227 boolean success = theDeliveryChannel.send(theWrappedMsg); 228 if (!success) { 229 ourLog.warn("Failed to send message to Delivery Channel."); 230 } 231 } catch (RuntimeException e) { 232 ourLog.error("Failed to send message to Delivery Channel", e); 233 throw new RuntimeException(Msg.code(7) + "Failed to send message to Delivery Channel", e); 234 } 235 } 236 237 private String getId(ActiveSubscription theActiveSubscription) { 238 return theActiveSubscription.getId(); 239 } 240 241 private boolean resourceTypeIsAppropriateForSubscription(ActiveSubscription theActiveSubscription, IIdType theResourceId) { 242 SubscriptionCriteriaParser.SubscriptionCriteria criteria = theActiveSubscription.getCriteria(); 243 String subscriptionId = getId(theActiveSubscription); 244 String resourceType = theResourceId.getResourceType(); 245 246 // see if the criteria matches the created object 247 ourLog.trace("Checking subscription {} for {} with criteria {}", subscriptionId, resourceType, criteria); 248 249 if (criteria == null) { 250 return false; 251 } 252 253 switch (criteria.getType()) { 254 default: 255 case SEARCH_EXPRESSION: 256 case MULTITYPE_EXPRESSION: 257 return criteria.getApplicableResourceTypes().contains(resourceType); 258 case STARTYPE_EXPRESSION: 259 return !resourceType.equals("Subscription"); 260 } 261 262 } 263}