001package ca.uhn.fhir.jpa.subscription.submit.interceptor;
002
003import ca.uhn.fhir.context.FhirContext;
004import ca.uhn.fhir.interceptor.api.Hook;
005import ca.uhn.fhir.interceptor.api.HookParams;
006import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
007import ca.uhn.fhir.interceptor.api.Interceptor;
008import ca.uhn.fhir.interceptor.api.Pointcut;
009import ca.uhn.fhir.interceptor.model.RequestPartitionId;
010import ca.uhn.fhir.jpa.api.config.DaoConfig;
011import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
012import ca.uhn.fhir.jpa.subscription.channel.impl.LinkedBlockingChannel;
013import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory;
014import ca.uhn.fhir.jpa.subscription.match.matcher.matching.IResourceModifiedConsumer;
015import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionMatchingSubscriber;
016import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
017import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
018import ca.uhn.fhir.rest.api.server.RequestDetails;
019import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
020import com.google.common.annotations.VisibleForTesting;
021import org.apache.commons.lang3.Validate;
022import org.hl7.fhir.instance.model.api.IBaseResource;
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025import org.springframework.beans.factory.annotation.Autowired;
026import org.springframework.context.event.ContextRefreshedEvent;
027import org.springframework.context.event.EventListener;
028import org.springframework.messaging.MessageChannel;
029import org.springframework.transaction.support.TransactionSynchronizationAdapter;
030import org.springframework.transaction.support.TransactionSynchronizationManager;
031
032import static org.apache.commons.lang3.StringUtils.isNotBlank;
033
034/*-
035 * #%L
036 * HAPI FHIR Subscription Server
037 * %%
038 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
039 * %%
040 * Licensed under the Apache License, Version 2.0 (the "License");
041 * you may not use this file except in compliance with the License.
042 * You may obtain a copy of the License at
043 *
044 *      http://www.apache.org/licenses/LICENSE-2.0
045 *
046 * Unless required by applicable law or agreed to in writing, software
047 * distributed under the License is distributed on an "AS IS" BASIS,
048 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
049 * See the License for the specific language governing permissions and
050 * limitations under the License.
051 * #L%
052 */
053
054@Interceptor
055public class SubscriptionMatcherInterceptor implements IResourceModifiedConsumer {
056        private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherInterceptor.class);
057        @Autowired
058        private FhirContext myFhirContext;
059        @Autowired
060        private IInterceptorBroadcaster myInterceptorBroadcaster;
061        @Autowired
062        private SubscriptionChannelFactory mySubscriptionChannelFactory;
063        @Autowired
064        private DaoConfig myDaoConfig;
065        @Autowired
066        private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
067
068        private volatile MessageChannel myMatchingChannel;
069
070        /**
071         * Constructor
072         */
073        public SubscriptionMatcherInterceptor() {
074                super();
075        }
076
077        @EventListener(classes = {ContextRefreshedEvent.class})
078        public void startIfNeeded() {
079                if (myDaoConfig.getSupportedSubscriptionTypes().isEmpty()) {
080                        ourLog.debug("Subscriptions are disabled on this server.  Skipping {} channel creation.", SubscriptionMatchingSubscriber.SUBSCRIPTION_MATCHING_CHANNEL_NAME);
081                        return;
082                }
083                if (myMatchingChannel == null) {
084                        myMatchingChannel = mySubscriptionChannelFactory.newMatchingSendingChannel(SubscriptionMatchingSubscriber.SUBSCRIPTION_MATCHING_CHANNEL_NAME, null);
085                }
086        }
087
088        @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)
089        public void resourceCreated(IBaseResource theResource, RequestDetails theRequest) {
090                startIfNeeded();
091                submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE, theRequest);
092        }
093
094        @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED)
095        public void resourceDeleted(IBaseResource theResource, RequestDetails theRequest) {
096                startIfNeeded();
097                submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE, theRequest);
098        }
099
100        @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)
101        public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource, RequestDetails theRequest) {
102                startIfNeeded();
103                if (!myDaoConfig.isTriggerSubscriptionsForNonVersioningChanges()) {
104                        if (theOldResource != null && theNewResource != null) {
105                                String oldVersion = theOldResource.getIdElement().getVersionIdPart();
106                                String newVersion = theNewResource.getIdElement().getVersionIdPart();
107                                if (isNotBlank(oldVersion) && isNotBlank(newVersion) && oldVersion.equals(newVersion)) {
108                                        return;
109                                }
110                        }
111                }
112
113                submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE, theRequest);
114        }
115
116        /**
117         * This is an internal API - Use with caution!
118         */
119        @Override
120        public void submitResourceModified(IBaseResource theNewResource, ResourceModifiedMessage.OperationTypeEnum theOperationType, RequestDetails theRequest) {
121                // Even though the resource is being written, the subscription will be interacting with it by effectively "reading" it so we set the RequestPartitionId as a read request
122                RequestPartitionId requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForRead(theRequest, theNewResource.getIdElement().getResourceType(), theNewResource.getIdElement());
123                ResourceModifiedMessage msg = new ResourceModifiedMessage(myFhirContext, theNewResource, theOperationType, theRequest, requestPartitionId);
124
125                // Interceptor call: SUBSCRIPTION_RESOURCE_MODIFIED
126                HookParams params = new HookParams()
127                        .add(ResourceModifiedMessage.class, msg);
128                boolean outcome = CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.SUBSCRIPTION_RESOURCE_MODIFIED, params);
129                if (!outcome) {
130                        return;
131                }
132
133                submitResourceModified(msg);
134        }
135
136        /**
137         * This is an internal API - Use with caution!
138         */
139        @Override
140        public void submitResourceModified(final ResourceModifiedMessage theMsg) {
141                /*
142                 * We only want to submit the message to the processing queue once the
143                 * transaction is committed. We do this in order to make sure that the
144                 * data is actually in the DB, in case it's the database matcher.
145                 */
146                if (TransactionSynchronizationManager.isSynchronizationActive()) {
147                        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
148                                @Override
149                                public int getOrder() {
150                                        return 0;
151                                }
152
153                                @Override
154                                public void afterCommit() {
155                                        sendToProcessingChannel(theMsg);
156                                }
157                        });
158                } else {
159                        sendToProcessingChannel(theMsg);
160                }
161        }
162
163        protected void sendToProcessingChannel(final ResourceModifiedMessage theMessage) {
164                ourLog.trace("Sending resource modified message to processing channel");
165                Validate.notNull(myMatchingChannel, "A SubscriptionMatcherInterceptor has been registered without calling start() on it.");
166                myMatchingChannel.send(new ResourceModifiedJsonMessage(theMessage));
167        }
168
169        public void setFhirContext(FhirContext theCtx) {
170                myFhirContext = theCtx;
171        }
172
173        @VisibleForTesting
174        public LinkedBlockingChannel getProcessingChannelForUnitTest() {
175                return (LinkedBlockingChannel) myMatchingChannel;
176        }
177}