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}