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}