001package ca.uhn.fhir.jpa.subscription.match.registry;
002
003/*-
004 * #%L
005 * HAPI FHIR Subscription Server
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.interceptor.api.HookParams;
024import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
025import ca.uhn.fhir.interceptor.api.Pointcut;
026import ca.uhn.fhir.jpa.subscription.channel.subscription.ISubscriptionDeliveryChannelNamer;
027import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelRegistry;
028import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
029import ca.uhn.fhir.jpa.subscription.model.ChannelRetryConfiguration;
030import ca.uhn.fhir.util.HapiExtensions;
031import org.apache.commons.lang3.Validate;
032import org.hl7.fhir.instance.model.api.IBaseResource;
033import org.hl7.fhir.instance.model.api.IIdType;
034import org.hl7.fhir.r4.model.Subscription;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037import org.springframework.beans.factory.annotation.Autowired;
038
039import javax.annotation.PreDestroy;
040import java.util.Collection;
041import java.util.Collections;
042import java.util.List;
043import java.util.Optional;
044
045/**
046 * Cache of active subscriptions.  When a new subscription is added to the cache, a new Spring Channel is created
047 * and a new MessageHandler for that subscription is subscribed to that channel.  These subscriptions, channels, and
048 * handlers are all caches in this registry so they can be removed it the subscription is deleted.
049 */
050
051public class SubscriptionRegistry {
052        private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionRegistry.class);
053        private final ActiveSubscriptionCache myActiveSubscriptionCache = new ActiveSubscriptionCache();
054        @Autowired
055        private SubscriptionCanonicalizer mySubscriptionCanonicalizer;
056        @Autowired
057        private ISubscriptionDeliveryChannelNamer mySubscriptionDeliveryChannelNamer;
058        @Autowired
059        private SubscriptionChannelRegistry mySubscriptionChannelRegistry;
060        @Autowired
061        private IInterceptorBroadcaster myInterceptorBroadcaster;
062
063        /**
064         * Constructor
065         */
066        public SubscriptionRegistry() {
067                super();
068        }
069
070        public ActiveSubscription get(String theIdPart) {
071                return myActiveSubscriptionCache.get(theIdPart);
072        }
073
074        public Collection<ActiveSubscription> getAll() {
075                return myActiveSubscriptionCache.getAll();
076        }
077
078        private Optional<CanonicalSubscription> hasSubscription(IIdType theId) {
079                Validate.notNull(theId);
080                Validate.notBlank(theId.getIdPart());
081                Optional<ActiveSubscription> activeSubscription = Optional.ofNullable(myActiveSubscriptionCache.get(theId.getIdPart()));
082                return activeSubscription.map(ActiveSubscription::getSubscription);
083        }
084
085        /**
086         * Extracts the retry configuration settings from the CanonicalSubscription object.
087         *
088         * Returns the configuration, or null, if no retry (or a bad retry value)
089         * is specified.
090         *
091         * @param theSubscription
092         * @return
093         */
094        private ChannelRetryConfiguration getRetryConfigurationFromSubscriptionExtensions(CanonicalSubscription theSubscription) {
095                ChannelRetryConfiguration configuration = new ChannelRetryConfiguration();
096
097                List<String> retryCount = theSubscription.getChannelExtensions(HapiExtensions.EX_RETRY_COUNT);
098                if (retryCount.size() == 1) {
099                        String val = retryCount.get(0);
100                        configuration.setRetryCount(Integer.parseInt(val));
101                }
102                // else - 0 or more than 1 means no retry policy at all
103
104                // retry count is required for any retry policy
105                if (configuration.getRetryCount() == null || configuration.getRetryCount() < 0) {
106                        configuration = null;
107                }
108
109                return configuration;
110        }
111
112        private void registerSubscription(IIdType theId, CanonicalSubscription theCanonicalSubscription) {
113                Validate.notNull(theId);
114                String subscriptionId = theId.getIdPart();
115                Validate.notBlank(subscriptionId);
116                Validate.notNull(theCanonicalSubscription);
117
118                String channelName = mySubscriptionDeliveryChannelNamer.nameFromSubscription(theCanonicalSubscription);
119
120                // get the actual retry configuration
121                ChannelRetryConfiguration configuration = getRetryConfigurationFromSubscriptionExtensions(theCanonicalSubscription);
122
123                ActiveSubscription activeSubscription = new ActiveSubscription(theCanonicalSubscription, channelName);
124                activeSubscription.setRetryConfiguration(configuration);
125
126                // add to our registries
127                mySubscriptionChannelRegistry.add(activeSubscription);
128                myActiveSubscriptionCache.put(subscriptionId, activeSubscription);
129
130                ourLog.info("Registered active subscription Subscription/{} - Have {} registered", subscriptionId, myActiveSubscriptionCache.size());
131
132                // Interceptor call: SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED
133                HookParams params = new HookParams()
134                        .add(CanonicalSubscription.class, theCanonicalSubscription);
135                myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED, params);
136        }
137
138        public void unregisterSubscriptionIfRegistered(String theSubscriptionId) {
139                Validate.notNull(theSubscriptionId);
140
141                ActiveSubscription activeSubscription = myActiveSubscriptionCache.remove(theSubscriptionId);
142                if (activeSubscription != null) {
143                        mySubscriptionChannelRegistry.remove(activeSubscription);
144                        ourLog.info("Unregistered active subscription {} - Have {} registered", theSubscriptionId, myActiveSubscriptionCache.size());
145
146                        // Interceptor call: SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_UNREGISTERED
147                        HookParams params = new HookParams();
148                        myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_UNREGISTERED, params);
149                }
150        }
151
152        @PreDestroy
153        public void unregisterAllSubscriptions() {
154                // Once to set flag
155                unregisterAllSubscriptionsNotInCollection(Collections.emptyList());
156                // Twice to remove
157                unregisterAllSubscriptionsNotInCollection(Collections.emptyList());
158        }
159
160        void unregisterAllSubscriptionsNotInCollection(Collection<String> theAllIds) {
161
162                List<String> idsToDelete = myActiveSubscriptionCache.markAllSubscriptionsNotInCollectionForDeletionAndReturnIdsToDelete(theAllIds);
163                for (String id : idsToDelete) {
164                        unregisterSubscriptionIfRegistered(id);
165                }
166        }
167
168        public synchronized boolean registerSubscriptionUnlessAlreadyRegistered(IBaseResource theSubscription) {
169                Validate.notNull(theSubscription);
170                Optional<CanonicalSubscription> existingSubscription = hasSubscription(theSubscription.getIdElement());
171                CanonicalSubscription newSubscription = mySubscriptionCanonicalizer.canonicalize(theSubscription);
172
173                if (existingSubscription.isPresent()) {
174                        if (newSubscription.equals(existingSubscription.get())) {
175                                // No changes
176                                return false;
177                        }
178                        ourLog.info("Updating already-registered active subscription {}", theSubscription.getIdElement().toUnqualified().getValue());
179                        if (channelTypeSame(existingSubscription.get(), newSubscription)) {
180                                ourLog.info("Channel type is same.  Updating active subscription and re-using existing channel and handlers.");
181                                updateSubscription(theSubscription);
182                                return true;
183                        }
184                        unregisterSubscriptionIfRegistered(theSubscription.getIdElement().getIdPart());
185                }
186                if (Subscription.SubscriptionStatus.ACTIVE.equals(newSubscription.getStatus())) {
187                        registerSubscription(theSubscription.getIdElement(), newSubscription);
188                        return true;
189                } else {
190                        return false;
191                }
192        }
193
194        private void updateSubscription(IBaseResource theSubscription) {
195                IIdType theId = theSubscription.getIdElement();
196                Validate.notNull(theId);
197                Validate.notBlank(theId.getIdPart());
198                ActiveSubscription activeSubscription = myActiveSubscriptionCache.get(theId.getIdPart());
199                Validate.notNull(activeSubscription);
200                CanonicalSubscription canonicalized = mySubscriptionCanonicalizer.canonicalize(theSubscription);
201                activeSubscription.setSubscription(canonicalized);
202
203                // Interceptor call: SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED
204                HookParams params = new HookParams()
205                        .add(CanonicalSubscription.class, canonicalized);
206                myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED, params);
207        }
208
209        private boolean channelTypeSame(CanonicalSubscription theExistingSubscription, CanonicalSubscription theNewSubscription) {
210                return theExistingSubscription.getChannelType().equals(theNewSubscription.getChannelType());
211        }
212
213        public int size() {
214                return myActiveSubscriptionCache.size();
215        }
216}