001package ca.uhn.fhir.jpa.subscription.match.deliver.resthook;
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.context.RuntimeResourceDefinition;
024import ca.uhn.fhir.i18n.Msg;
025import ca.uhn.fhir.interceptor.api.HookParams;
026import ca.uhn.fhir.interceptor.api.Pointcut;
027import ca.uhn.fhir.interceptor.model.RequestPartitionId;
028import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
029import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
030import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
031import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
032import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
033import ca.uhn.fhir.jpa.subscription.match.deliver.BaseSubscriptionDeliverySubscriber;
034import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
035import ca.uhn.fhir.jpa.subscription.model.ResourceDeliveryMessage;
036import ca.uhn.fhir.rest.api.EncodingEnum;
037import ca.uhn.fhir.rest.api.RequestTypeEnum;
038import ca.uhn.fhir.rest.api.server.IBundleProvider;
039import ca.uhn.fhir.rest.client.api.Header;
040import ca.uhn.fhir.rest.client.api.IGenericClient;
041import ca.uhn.fhir.rest.client.api.IHttpClient;
042import ca.uhn.fhir.rest.client.api.IHttpRequest;
043import ca.uhn.fhir.rest.client.api.IHttpResponse;
044import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
045import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor;
046import ca.uhn.fhir.rest.gclient.IClientExecutable;
047import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
048import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
049import ca.uhn.fhir.rest.server.messaging.BaseResourceModifiedMessage;
050import ca.uhn.fhir.util.BundleBuilder;
051import org.apache.commons.text.StringSubstitutor;
052import org.hl7.fhir.instance.model.api.IBaseResource;
053import org.hl7.fhir.instance.model.api.IIdType;
054import org.slf4j.Logger;
055import org.slf4j.LoggerFactory;
056import org.springframework.beans.factory.annotation.Autowired;
057import org.springframework.context.annotation.Scope;
058import org.springframework.messaging.MessagingException;
059
060import javax.annotation.Nullable;
061import java.io.IOException;
062import java.util.ArrayList;
063import java.util.Collections;
064import java.util.HashMap;
065import java.util.List;
066import java.util.Map;
067
068import static ca.uhn.fhir.jpa.subscription.util.SubscriptionUtil.createRequestDetailForPartitionedRequest;
069import static org.apache.commons.lang3.StringUtils.isNotBlank;
070
071@Scope("prototype")
072public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDeliverySubscriber {
073        private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionDeliveringRestHookSubscriber.class);
074
075        @Autowired
076        private DaoRegistry myDaoRegistry;
077
078        @Autowired
079        private MatchUrlService myMatchUrlService;
080
081        /**
082         * Constructor
083         */
084        public SubscriptionDeliveringRestHookSubscriber() {
085                super();
086        }
087
088        protected void deliverPayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription, EncodingEnum thePayloadType, IGenericClient theClient) {
089                IBaseResource payloadResource = getAndMassagePayload(theMsg, theSubscription);
090
091                // Regardless of whether we have a payload, the rest-hook should be sent.
092                doDelivery(theMsg, theSubscription, thePayloadType, theClient, payloadResource);
093        }
094
095        protected void doDelivery(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription, EncodingEnum thePayloadType, IGenericClient theClient, IBaseResource thePayloadResource) {
096                IClientExecutable<?, ?> operation;
097
098                if (isNotBlank(theSubscription.getPayloadSearchCriteria())) {
099                        operation = createDeliveryRequestTransaction(theSubscription, theClient, thePayloadResource);
100                } else if (thePayloadType != null) {
101                        operation = createDeliveryRequestNormal(theMsg, theClient, thePayloadResource);
102                } else {
103                        sendNotification(theMsg);
104                        operation = null;
105                }
106
107                if (operation != null) {
108
109                        if (thePayloadType != null) {
110                                operation.encoded(thePayloadType);
111                        }
112
113                        String payloadId = thePayloadResource.getIdElement().toUnqualified().getValue();
114                        ourLog.info("Delivering {} rest-hook payload {} for {}", theMsg.getOperationType(), payloadId, theSubscription.getIdElement(myFhirContext).toUnqualifiedVersionless().getValue());
115
116                        try {
117                                operation.execute();
118                        } catch (ResourceNotFoundException e) {
119                                ourLog.error("Cannot reach {} ", theMsg.getSubscription().getEndpointUrl());
120                                ourLog.error("Exception: ", e);
121                                throw e;
122                        }
123
124                }
125        }
126
127        @Nullable
128        private IClientExecutable<?, ?> createDeliveryRequestNormal(ResourceDeliveryMessage theMsg, IGenericClient theClient, IBaseResource thePayloadResource) {
129                IClientExecutable<?, ?> operation;
130                switch (theMsg.getOperationType()) {
131                        case CREATE:
132                        case UPDATE:
133                                operation = theClient.update().resource(thePayloadResource);
134                                break;
135                        case DELETE:
136                                operation = theClient.delete().resourceById(theMsg.getPayloadId(myFhirContext));
137                                break;
138                        default:
139                                ourLog.warn("Ignoring delivery message of type: {}", theMsg.getOperationType());
140                                operation = null;
141                                break;
142                }
143                return operation;
144        }
145
146        private IClientExecutable<?, ?> createDeliveryRequestTransaction(CanonicalSubscription theSubscription, IGenericClient theClient, IBaseResource thePayloadResource) {
147                IClientExecutable<?, ?> operation;
148                String resType = theSubscription.getPayloadSearchCriteria().substring(0, theSubscription.getPayloadSearchCriteria().indexOf('?'));
149                IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resType);
150                RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(resType);
151
152                String payloadUrl = theSubscription.getPayloadSearchCriteria();
153                Map<String, String> valueMap = new HashMap<>(1);
154                valueMap.put("matched_resource_id", thePayloadResource.getIdElement().toUnqualifiedVersionless().getValue());
155                payloadUrl = new StringSubstitutor(valueMap).replace(payloadUrl);
156                SearchParameterMap payloadSearchMap = myMatchUrlService.translateMatchUrl(payloadUrl, resourceDefinition, MatchUrlService.processIncludes());
157                payloadSearchMap.setLoadSynchronous(true);
158
159                IBundleProvider searchResults = dao.search(payloadSearchMap, createRequestDetailForPartitionedRequest(theSubscription));
160
161                BundleBuilder builder = new BundleBuilder(myFhirContext);
162                for (IBaseResource next : searchResults.getAllResources()) {
163                        builder.addTransactionUpdateEntry(next);
164                }
165
166                operation = theClient.transaction().withBundle(builder.getBundle());
167                return operation;
168        }
169
170        public IBaseResource getResource(IIdType payloadId, RequestPartitionId thePartitionId, boolean theDeletedOK) throws ResourceGoneException {
171                RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(payloadId.getResourceType());
172                SystemRequestDetails systemRequestDetails = new SystemRequestDetails().setRequestPartitionId(thePartitionId);
173                IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceDef.getImplementingClass());
174                return dao.read(payloadId.toVersionless(), systemRequestDetails, theDeletedOK);
175        }
176
177
178        protected IBaseResource getAndMassagePayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription) {
179                IBaseResource payloadResource = theMsg.getPayload(myFhirContext);
180
181                if (payloadResource == null || theSubscription.getRestHookDetails().isDeliverLatestVersion()) {
182                        IIdType payloadId = theMsg.getPayloadId(myFhirContext);
183
184                        try {
185                                if (payloadId != null) {
186                                        boolean deletedOK = theMsg.getOperationType() == BaseResourceModifiedMessage.OperationTypeEnum.DELETE;
187                                        payloadResource = getResource(payloadId.toVersionless(), theMsg.getRequestPartitionId(), deletedOK);
188                                } else {
189                                        return null;
190                                }
191                        } catch (ResourceGoneException e) {
192                                ourLog.warn("Resource {} is deleted, not going to deliver for subscription {}", payloadId.toVersionless(), theSubscription.getIdElement(myFhirContext));
193                                return null;
194                        }
195                }
196
197                IIdType resourceId = payloadResource.getIdElement();
198                if (theSubscription.getRestHookDetails().isStripVersionId()) {
199                        resourceId = resourceId.toVersionless();
200                        payloadResource.setId(resourceId);
201                }
202                return payloadResource;
203        }
204
205        @Override
206        public void handleMessage(ResourceDeliveryMessage theMessage) throws MessagingException {
207                CanonicalSubscription subscription = theMessage.getSubscription();
208
209                // Interceptor call: SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY
210                HookParams params = new HookParams()
211                        .add(CanonicalSubscription.class, subscription)
212                        .add(ResourceDeliveryMessage.class, theMessage);
213                if (!getInterceptorBroadcaster().callHooks(Pointcut.SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY, params)) {
214                        return;
215                }
216
217                // Grab the endpoint from the subscription
218                String endpointUrl = subscription.getEndpointUrl();
219
220                // Grab the payload type (encoding mimetype) from the subscription
221                String payloadString = subscription.getPayloadString();
222                EncodingEnum payloadType = null;
223                if (payloadString != null) {
224                        payloadType = EncodingEnum.forContentType(payloadString);
225                }
226
227                // Create the client request
228                myFhirContext.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
229                IGenericClient client = null;
230                if (isNotBlank(endpointUrl)) {
231                        client = myFhirContext.newRestfulGenericClient(endpointUrl);
232
233                        // Additional headers specified in the subscription
234                        List<String> headers = subscription.getHeaders();
235                        for (String next : headers) {
236                                if (isNotBlank(next)) {
237                                        client.registerInterceptor(new SimpleRequestHeaderInterceptor(next));
238                                }
239                        }
240                }
241
242                deliverPayload(theMessage, subscription, payloadType, client);
243
244                // Interceptor call: SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY
245                params = new HookParams()
246                        .add(CanonicalSubscription.class, subscription)
247                        .add(ResourceDeliveryMessage.class, theMessage);
248                if (!getInterceptorBroadcaster().callHooks(Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY, params)) {
249                        //noinspection UnnecessaryReturnStatement
250                        return;
251                }
252
253        }
254
255        /**
256         * Sends a POST notification without a payload
257         */
258        protected void sendNotification(ResourceDeliveryMessage theMsg) {
259                Map<String, List<String>> params = new HashMap<>();
260                CanonicalSubscription subscription = theMsg.getSubscription();
261                List<Header> headers = parseHeadersFromSubscription(subscription);
262
263                StringBuilder url = new StringBuilder(subscription.getEndpointUrl());
264                IHttpClient client = myFhirContext.getRestfulClientFactory().getHttpClient(url, params, "", RequestTypeEnum.POST, headers);
265                IHttpRequest request = client.createParamRequest(myFhirContext, params, null);
266                try {
267                        IHttpResponse response = request.execute();
268                        // close connection in order to return a possible cached connection to the connection pool
269                        response.close();
270                } catch (IOException e) {
271                        ourLog.error("Error trying to reach {}: {}", theMsg.getSubscription().getEndpointUrl(), e.toString());
272                        throw new ResourceNotFoundException(Msg.code(5) + e.getMessage());
273                }
274        }
275
276        public static List<Header> parseHeadersFromSubscription(CanonicalSubscription subscription) {
277                List<Header> headers = null;
278                if (subscription != null) {
279                        for (String h : subscription.getHeaders()) {
280                                if (h != null) {
281                                        final int sep = h.indexOf(':');
282                                        if (sep > 0) {
283                                                final String name = h.substring(0, sep);
284                                                final String value = h.substring(sep + 1);
285                                                if (isNotBlank(name)) {
286                                                        if (headers == null) {
287                                                                headers = new ArrayList<>();
288                                                        }
289                                                        headers.add(new Header(name.trim(), value.trim()));
290                                                }
291                                        }
292                                }
293                        }
294                }
295                if (headers == null) {
296                        headers = Collections.emptyList();
297                } else {
298                        headers = Collections.unmodifiableList(headers);
299                }
300                return headers;
301        }
302
303}