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}