001/*-
002 * #%L
003 * HAPI FHIR Subscription Server
004 * %%
005 * Copyright (C) 2014 - 2024 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.submit.interceptor;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.interceptor.api.Hook;
024import ca.uhn.fhir.interceptor.api.HookParams;
025import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
026import ca.uhn.fhir.interceptor.api.Interceptor;
027import ca.uhn.fhir.interceptor.api.Pointcut;
028import ca.uhn.fhir.interceptor.model.RequestPartitionId;
029import ca.uhn.fhir.jpa.model.entity.StorageSettings;
030import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
031import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage;
032import ca.uhn.fhir.rest.api.server.RequestDetails;
033import ca.uhn.fhir.rest.server.messaging.BaseResourceMessage;
034import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
035import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc;
036import org.hl7.fhir.instance.model.api.IBaseResource;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039import org.springframework.beans.factory.annotation.Autowired;
040
041import static java.util.Objects.isNull;
042import static org.apache.commons.lang3.StringUtils.isBlank;
043
044/**
045 *
046 * This interceptor is responsible for submitting operations on resources to the subscription pipeline.
047 *
048 */
049@Interceptor
050public class SubscriptionMatcherInterceptor {
051        private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherInterceptor.class);
052
053        @Autowired
054        private FhirContext myFhirContext;
055
056        @Autowired
057        private IInterceptorBroadcaster myInterceptorBroadcaster;
058
059        @Autowired
060        private StorageSettings myStorageSettings;
061
062        @Autowired
063        private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
064
065        @Autowired
066        private IResourceModifiedMessagePersistenceSvc myResourceModifiedMessagePersistenceSvc;
067
068        /**
069         * Constructor
070         */
071        public SubscriptionMatcherInterceptor() {
072                super();
073        }
074
075        @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)
076        public void resourceCreated(IBaseResource theResource, RequestDetails theRequest) {
077
078                processResourceModifiedEvent(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE, theRequest);
079        }
080
081        @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED)
082        public void resourceDeleted(IBaseResource theResource, RequestDetails theRequest) {
083
084                processResourceModifiedEvent(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE, theRequest);
085        }
086
087        @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)
088        public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource, RequestDetails theRequest) {
089                boolean dontTriggerSubscriptionWhenVersionsAreTheSame =
090                                !myStorageSettings.isTriggerSubscriptionsForNonVersioningChanges();
091                boolean resourceVersionsAreTheSame = isSameResourceVersion(theOldResource, theNewResource);
092
093                if (dontTriggerSubscriptionWhenVersionsAreTheSame && resourceVersionsAreTheSame) {
094                        return;
095                }
096
097                processResourceModifiedEvent(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE, theRequest);
098        }
099
100        /**
101         * This is an internal API - Use with caution!
102         *
103         * This method will create a {@link ResourceModifiedMessage}, persist it and arrange for its delivery to the
104         * subscription pipeline after the resource was committed.  The message is persisted to provide asynchronous submission
105         * in the event where submission would fail.
106         */
107        protected void processResourceModifiedEvent(
108                        IBaseResource theNewResource,
109                        ResourceModifiedMessage.OperationTypeEnum theOperationType,
110                        RequestDetails theRequest) {
111
112                ResourceModifiedMessage msg = createResourceModifiedMessage(theNewResource, theOperationType, theRequest);
113
114                // Interceptor call: SUBSCRIPTION_RESOURCE_MODIFIED
115                HookParams params = new HookParams().add(ResourceModifiedMessage.class, msg);
116                boolean outcome = CompositeInterceptorBroadcaster.doCallHooks(
117                                myInterceptorBroadcaster, theRequest, Pointcut.SUBSCRIPTION_RESOURCE_MODIFIED, params);
118
119                if (!outcome) {
120                        return;
121                }
122
123                processResourceModifiedMessage(msg);
124        }
125
126        protected void processResourceModifiedMessage(ResourceModifiedMessage theResourceModifiedMessage) {
127                //      persist the message for async submission to the processing pipeline. see {@link
128                // AsyncResourceModifiedProcessingSchedulerSvc}
129                myResourceModifiedMessagePersistenceSvc.persist(theResourceModifiedMessage);
130        }
131
132        protected ResourceModifiedMessage createResourceModifiedMessage(
133                        IBaseResource theNewResource,
134                        BaseResourceMessage.OperationTypeEnum theOperationType,
135                        RequestDetails theRequest) {
136                // Even though the resource is being written, the subscription will be interacting with it by effectively
137                // "reading" it so we set the RequestPartitionId as a read request
138                RequestPartitionId requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForRead(
139                                theRequest, theNewResource.getIdElement());
140                return new ResourceModifiedMessage(
141                                myFhirContext, theNewResource, theOperationType, theRequest, requestPartitionId);
142        }
143
144        private boolean isSameResourceVersion(IBaseResource theOldResource, IBaseResource theNewResource) {
145                if (isNull(theOldResource) || isNull(theNewResource)) {
146                        return false;
147                }
148
149                String oldVersion = theOldResource.getIdElement().getVersionIdPart();
150                String newVersion = theNewResource.getIdElement().getVersionIdPart();
151
152                if (isBlank(oldVersion) || isBlank(newVersion)) {
153                        return false;
154                }
155
156                return oldVersion.equals(newVersion);
157        }
158
159        public void setFhirContext(FhirContext theCtx) {
160                myFhirContext = theCtx;
161        }
162}