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.subscription.match.registry;
021
022import ca.uhn.fhir.cache.BaseResourceCacheSynchronizer;
023import ca.uhn.fhir.interceptor.model.RequestPartitionId;
024import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
025import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionActivatingListener;
026import ca.uhn.fhir.rest.param.TokenOrListParam;
027import ca.uhn.fhir.rest.param.TokenParam;
028import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
029import ca.uhn.fhir.subscription.SubscriptionConstants;
030import com.google.common.annotations.VisibleForTesting;
031import jakarta.annotation.Nonnull;
032import org.apache.commons.lang3.StringUtils;
033import org.hl7.fhir.instance.model.api.IBaseResource;
034import org.hl7.fhir.r4.model.Subscription;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037import org.springframework.beans.factory.annotation.Autowired;
038
039import java.util.HashSet;
040import java.util.List;
041import java.util.Set;
042
043public class SubscriptionLoader extends BaseResourceCacheSynchronizer {
044        private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionLoader.class);
045
046        @Autowired
047        private SubscriptionRegistry mySubscriptionRegistry;
048
049        @Autowired
050        private SubscriptionActivatingListener mySubscriptionActivatingInterceptor;
051
052        @Autowired
053        private SubscriptionCanonicalizer mySubscriptionCanonicalizer;
054
055        @Autowired
056        protected ISearchParamRegistry mySearchParamRegistry;
057
058        /**
059         * Constructor
060         */
061        public SubscriptionLoader() {
062                super("Subscription", RequestPartitionId.allPartitions());
063        }
064
065        @VisibleForTesting
066        public int doSyncSubscriptionsForUnitTest() {
067                return super.doSyncResourcesForUnitTest();
068        }
069
070        @Override
071        @Nonnull
072        protected SearchParameterMap getSearchParameterMap() {
073                SearchParameterMap map = new SearchParameterMap();
074
075                if (mySearchParamRegistry.getActiveSearchParam(
076                                                "Subscription", "status", ISearchParamRegistry.SearchParamLookupContextEnum.ALL)
077                                != null) {
078                        map.add(
079                                        Subscription.SP_STATUS,
080                                        new TokenOrListParam()
081                                                        .addOr(new TokenParam(null, Subscription.SubscriptionStatus.REQUESTED.toCode()))
082                                                        .addOr(new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode())));
083                }
084                map.setLoadSynchronousUpTo(SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS);
085                return map;
086        }
087
088        @Override
089        protected void handleInit(List<IBaseResource> resourceList) {
090                updateSubscriptionRegistry(resourceList);
091        }
092
093        @Override
094        protected int syncResourcesIntoCache(List<IBaseResource> resourceList) {
095                return updateSubscriptionRegistry(resourceList);
096        }
097
098        private int updateSubscriptionRegistry(List<IBaseResource> theResourceList) {
099                Set<String> allIds = new HashSet<>();
100                int activatedCount = 0;
101                int registeredCount = 0;
102
103                for (IBaseResource resource : theResourceList) {
104                        String nextId = resource.getIdElement().getIdPart();
105                        allIds.add(nextId);
106
107                        boolean activated = activateSubscriptionIfRequested(resource);
108                        if (activated) {
109                                ++activatedCount;
110                        }
111
112                        boolean registered = mySubscriptionRegistry.registerSubscriptionUnlessAlreadyRegistered(resource);
113                        if (registered) {
114                                registeredCount++;
115                        }
116                }
117
118                mySubscriptionRegistry.unregisterAllSubscriptionsNotInCollection(allIds);
119                ourLog.debug(
120                                "Finished sync subscriptions - activated {} and registered {}",
121                                theResourceList.size(),
122                                registeredCount);
123                return activatedCount;
124        }
125
126        /**
127         * Check status of theSubscription and update to "active" if needed.
128         * @return true if activated
129         */
130        private boolean activateSubscriptionIfRequested(IBaseResource theSubscription) {
131                boolean successfullyActivated = false;
132
133                if (SubscriptionConstants.REQUESTED_STATUS.equals(
134                                mySubscriptionCanonicalizer.getSubscriptionStatus(theSubscription))) {
135                        if (mySubscriptionActivatingInterceptor.isChannelTypeSupported(theSubscription)) {
136                                // internally, subscriptions that cannot activate will be set to error
137                                if (mySubscriptionActivatingInterceptor.activateSubscriptionIfRequired(theSubscription)) {
138                                        successfullyActivated = true;
139                                } else {
140                                        logSubscriptionNotActivatedPlusErrorIfPossible(theSubscription);
141                                }
142                        } else {
143                                ourLog.debug(
144                                                "Could not activate subscription {} because channel type {} is not supported.",
145                                                theSubscription.getIdElement(),
146                                                mySubscriptionCanonicalizer.getChannelType(theSubscription));
147                        }
148                }
149
150                return successfullyActivated;
151        }
152
153        /**
154         * Logs
155         *
156         * @param theSubscription
157         */
158        private void logSubscriptionNotActivatedPlusErrorIfPossible(IBaseResource theSubscription) {
159                String error;
160                if (theSubscription instanceof Subscription) {
161                        error = ((Subscription) theSubscription).getError();
162                } else if (theSubscription instanceof org.hl7.fhir.dstu3.model.Subscription) {
163                        error = ((org.hl7.fhir.dstu3.model.Subscription) theSubscription).getError();
164                } else if (theSubscription instanceof org.hl7.fhir.dstu2.model.Subscription) {
165                        error = ((org.hl7.fhir.dstu2.model.Subscription) theSubscription).getError();
166                } else {
167                        error = "";
168                }
169                ourLog.error(
170                                "Subscription {} could not be activated. "
171                                                + "This will not prevent startup, but it could lead to undesirable outcomes! {}",
172                                theSubscription.getIdElement().getIdPart(),
173                                (StringUtils.isBlank(error) ? "" : "Error: " + error));
174        }
175}