This page outlines various options that can be used to control how notification delivery works.
By default, the REST HOOK Subscription delivery involves performing an HTTP PUT (FHIR update) operation where the payload is the individual resource that triggered the subscription (i.e. the resource will be of the type that was identified by the Subscription criteria). Similarly, MESSAGE Subscription delivery involves writing an individual resource to the target queue.
Some scenarios require the delivery of a collection of resources as a Bundle to the target server. When Payload Search Result Mode is used, the REST HOOK delivery module will perform a search using user-defined criteria and then delivers the results of the search as a FHIR Transaction operation. Similarly, the search results will be delivered to the user-specified endpoint channel as bundle resources when Payload Search Result Mode is applied in MESSAGE delivery module.
To enable Payload Search Result Mode, an extension is added to the Subscription resource with the following URL:
Name | URL | Type | Description |
---|---|---|---|
Payload Search Criteria | string |
The value to this extension is a string containing a FHIR search expression (i.e. a URL fragment without the base URL portion, as shown in the example below). It may contain the following substitutions:
|
The example below shows a sample Rest Hook Subscription configured to use Payload Search Result Mode. This subscription has a criteria of Observation?
, meaning it will match all Observations.
{
"resourceType": "Subscription",
"extension": [ {
"url": "http://hapifhir.io/fhir/StructureDefinition/subscription-payload-search-criteria",
"valueString": "Observation?_id=${matched_resource_id}&_include=*"
} ],
"status": "requested",
"criteria": "Observation?",
"channel": {
"type": "rest-hook",
"endpoint": "http://example.com/baseR4",
"payload": "application/fhir+json"
}
}
The example below shows a sample Message Subscription configured to use Payload Search Result Mode. This subscription has a criteria of Observation?
and an extension of "Observation?_id=${matched_resource_id}&_include=Observation:subject"
, meaning it will match all Observations and return a bundle that contains both the matched Observation and the subject.
{
"resourceType": "Subscription",
"extension": [ {
"url": "http://hapifhir.io/fhir/StructureDefinition/subscription-payload-search-criteria",
"valueString": "Observation?_id=${matched_resource_id}&_include=Observation:subject"
} ],
"status": "requested",
"criteria": "Observation?",
"channel": {
"type": "message",
"endpoint": "channel:example-queue-name",
"payload": "application/json"
}
}
When using this mode, a transaction bundle resembling the following example will be POSTed to the base URL of the FHIR server (ie. the endpoint configured in the Subscription resource) or the endpoint channel in case of message subscription per the FHIR transaction operation semantics.
{
"resourceType": "Bundle",
"id": "6a5be3a8-04d0-4c9a-b179-161fced75ab7",
"type": "transaction",
"entry": [ {
"fullUrl": "Observation/3/_history/1",
"resource": {
"resourceType": "Observation",
"id": "3",
"meta": {
"versionId": "1",
"lastUpdated": "2020-06-29T18:08:18.729-04:00"
},
"subject": {
"reference": "Patient/2"
}
},
"request": {
"method": "PUT",
"url": "Observation/3"
}
}, {
"fullUrl": "Patient/2/_history/1",
"resource": {
"resourceType": "Patient",
"id": "2",
"meta": {
"versionId": "1",
"lastUpdated": "2020-06-29T18:08:18.701-04:00"
},
"active": true
},
"request": {
"method": "PUT",
"url": "Patient/2"
}
} ]
}
In replication mode, Smile CDR assumes that it is replicating all matching resources from one Smile CDR instance to another Smile CDR instance. This allows the server to make assumptions about how operations may be batched and sequenced. The following Subscription example shows a replication subscription:
{
"resourceType": "Subscription",
"status": "active",
"reason": "Monitor new neonatal function (note, age will be determined by the monitor)",
"criteria": "Observation?code=SNOMED-CT|1000000050",
"channel": {
"extension": [
{
"url": "https://smilecdr.com/fhir/ns/StructureDefinition/subscription-channel-rest-replicate-mode",
"valueBoolean": true
},
{
"url": "https://smilecdr.com/fhir/ns/StructureDefinition/subscription-channel-rest-replicate-id-prefix",
"valueString": "CDR1-"
}
],
"type": "rest-hook",
"endpoint": "http://localhost:40243/fhir/context",
"payload": "application/json"
}
}
When running in replication mode, the following extensions may be used.
Name | URL | Type | Description |
---|---|---|---|
Enable Replication Mode | https://smilecdr.com/fhir/ns/StructureDefinition/subscription-channel-rest-replicate-mode | boolean |
If set to true , this subscription will be delivered in replication mode.
|
Add ID Prefixes | https://smilecdr.com/fhir/ns/StructureDefinition/subscription-channel-rest-replicate-id-prefix | string | If specified, all resource IDs (or a specific set of resource IDs if the selector property below is also set) will be prefixed with the given prefix (both in IDs, and in references to those IDs from other resources). This is useful in situations where you want to be able to replicate data from several source CDR instances to one target CDR instance, or in cases where the source CDR instance is using numeric IDs (since Smile CDR will not allow client-provided numeric IDs). |
Add ID Prefixes - Selector | https://smilecdr.com/fhir/ns/StructureDefinition/subscription-channel-rest-replicate-id-prefix-selector | string | If specified, only resource IDs matching the given pattern will have a prefix appended. The format of the pattern is a regular expresion and the pattern must match the entire string including the resource type and the ID part. For example, the following pattern will match all purely numeric IDs but will not prefix any IDs with non-numeric characters in them: `[a-zA-Z]+/[0-9]+` |
Strip ID Prefixes | https://smilecdr.com/fhir/ns/StructureDefinition/subscription-channel-rest-replicate-strip-id-prefix | string | If specified, all resource IDs will have the specified prefix removed from any resource IDs and references that are found within resources being delivered. This is helpful when subscriptions need to be delivered from a server that uses prefixes to a server that does not. For example, this could be used when delivering resources from a server that was populated by a subscription that uses the Add ID Prefixes on its own Subscription. |
Strip ID Base URLs | https://smilecdr.com/fhir/ns/StructureDefinition/subscription-channel-rest-replicate-strip-reference-base-url | string | If specified, all references containing this base URL will have the base URL removed prior to delivery. |
If the replication is being sent to a second Smile CDR instance, you may wish to enable the dao_config.auto_create_placeholder_reference_targets.enabled configuration property on the replication target persistence module. This means that if payloads are received out-of-order by the receiving system, no payload will be rejected simply because it contains a reference to a resource that has not yet been received.
By default, the resource version ID is included when delivering a resource to the REST HOOK receiving endpoint. This can be disabled using an extension on the REST HOOK channel.
{
"resourceType": "Subscription",
"status": "active",
"reason": "Monitor new neonatal function (note, age will be determined by the monitor)",
"criteria": "Observation?code=SNOMED-CT|1000000050",
"channel": {
"extension": [
{
"url": "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-strip-version-ids",
"valueBoolean": true
}
],
"type": "rest-hook",
"endpoint": "http://localhost:40243/fhir/context",
"payload": "application/json"
}
}
Note the following extension:
URL | Type | Description |
---|---|---|
http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-strip-version-ids | boolean |
If set to true , the deliverer will strip the resource version ID before sending the resource
|
By default if a resource is created and then updated in rapid succession, the deliverer will deliver versions 1 and 2 to the REST HOOK target.
This can be adjusted so that if the resource gets updated before the deliverer manages to transmit the resource, only the latest version will be transmitted (and it may be delivered more than once).
{
"resourceType": "Subscription",
"status": "active",
"reason": "Monitor new neonatal function (note, age will be determined by the monitor)",
"criteria": "Observation?code=SNOMED-CT|1000000050",
"channel": {
"extension": [
{
"url": "http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-deliver-latest-version",
"valueBoolean": true
}
],
"type": "rest-hook",
"endpoint": "http://localhost:40243/fhir/context",
"payload": "application/json"
}
}
Note the following extension:
URL | Type | Description |
---|---|---|
http://hapifhir.io/fhir/StructureDefinition/subscription-resthook-deliver-latest-version | boolean |
If set to true , the deliverer will attempt to send only the latest version of a resource.
|
By default, subscription doesn't deliver resource delete events.
This can be adjusted so resource delete events also get delivered.
{
"resourceType": "Subscription",
"id": "1",
"meta": {
"versionId": "1",
"lastUpdated": "2017-08-19T16:52:11.041-04:00"
},
"status": "active",
"reason": "Monitor resource persistence events",
"criteria": "Patient",
"channel": {
"extension": [
{
"url": "http://hapifhir.io/fhir/StructureDefinition/subscription-send-delete-messages",
"valueBoolean": "true"
}
],
"type": "rest-hook",
"payload": "application/json",
"header": [
"Authorization: Bearer 123"
]
}
}
Note the following extension:
URL | Type | Description |
---|---|---|
http://hapifhir.io/fhir/StructureDefinition/subscription-send-delete-messages | boolean |
If set to true , the deliverer will also send messages for resource delete events.
|
Users may also supply a custom Java class that will be used to deliver the resource instead of the built-in FHIR client. This is useful in cases where you want to customize the delivery (e.g. by adding special attributes or filters, or by using another protocol altogether instead of FHIR/REST).
The following example shows a subscription that uses a custom delivery class:
{
"resourceType": "Subscription",
"id": "1",
"meta": {
"versionId": "1",
"lastUpdated": "2017-08-19T16:52:11.041-04:00"
},
"status": "active",
"reason": "Monitor new neonatal function (note, age will be determined by the monitor)",
"criteria": "Observation?code=SNOMED-CT|1000000050&_format=xml",
"channel": {
"extension": [
{
"url": "https://smilecdr.com/fhir/ns/StructureDefinition/subscription-channel-rest-delivery-class",
"valueString": "org.example.ExampleDeliverer"
}
],
"type": "rest-hook",
"payload": "application/json",
"header": [
"Authorization: Bearer 123"
]
}
}
Note the following extension:
URL | Type | Description |
---|---|---|
https://smilecdr.com/fhir/ns/StructureDefinition/subscription-channel-rest-delivery-class | string |
This is the class name for the Java class that implements the ISubscriptionDeliverer interface.
|
The following example shows a simple delivery class:
package org.example;
public class ExampleDeliverer implements ISubscriptionDeliverer {
private Logger ourLog = LoggerFactory.getLogger(ExampleDeliverer.class);
/**
* Deliver the resource
*
* @param theOperation The operation triggering this delivery
* @param theSubscription The subscription
* @param theResource The resource
*/
@Override
public void deliver(RestOperationTypeEnum theOperation, CanonicalSubscription theSubscription, IBaseResource theResource) {
String endpointUrl = theSubscription.getEndpointUrl();
String payloadString = theSubscription.getPayloadString();
EncodingEnum payloadType = EncodingEnum.forContentType(payloadString);
IGenericClient client = FhirContext.forR4().newRestfulGenericClient(endpointUrl);
List<String> headers = getSubscriptionHeaders(theSubscription);
for (String next : headers) {
if (isNotBlank(next)) {
client.registerInterceptor(new SimpleRequestHeaderInterceptor(next));
}
}
IClientExecutable operation = client.create().resource(theResource);
operation.encoded(payloadType);
try {
operation.execute();
} catch (ResourceNotFoundException e) {
ourLog.error("Cannot reach {} ", endpointUrl);
ourLog.error("Exception: ", e);
throw e;
}
}
private List<String> getSubscriptionHeaders(CanonicalSubscription theSubscription) {
return Collections.emptyList();
}
}
By default, the MESSAGE delivery type will deliver the resource to the broker as a record. This record will contain no headers. If you want to add headers to the record, there is a mechanism to do so. First, note the anatomy of an outgoing ResourceModifiedMessage:
{
"headers": {
"retryCount": 0,
"customHeaders": {}
},
"payload": {
"operationType": "CREATE",
"transactionId": "eebf29cf-3775-4832-a054-df72e5d399ca",
"payload": "{\"resourceType\":\"Patient\",\"id\":\"1354\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2022-07-19T17:27:56.198-07:00\"}}",
"payloadId": "Patient/1354/_history/1",
"partitionId": {
"allPartitions": true
}
}
}
If you are using kafka as a broker, any elements placed in the customHeaders
section will be added to the record's headers.
Smile CDR provides a pointcut, SUBSCRIPTION_BEFORE_MESSAGE_DELIVERY
, which permits you to modify this outgoing message just before it is delivered to the broker.
By writing an interceptor which implements the SUBSCRIPTION_BEFORE_MESSAGE_DELIVERY
pointcut, you can add custom headers to the outgoing message. Below is an example of such a below interceptor, which adds a resource type header, and a resource ID header, as well as headers for each tag, and the patient's family name.
//package com.smilecdr.demo.subscription;
// Uncomment the line above when trying out this example from the documentation
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
import ca.uhn.fhir.jpa.subscription.model.ResourceDeliveryMessage;
import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage;
import org.hl7.fhir.dstu3.model.Patient;
// To use R4, remove the above line and uncomment the following line
//import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.slf4j.LoggerFactory.getLogger;
/**
* This interceptor customizes an outgoing Message Sending subscription in order to modify the
* outgoing Kafka record by adding custom headers.
*
* This interceptor should be registered against the Subscription module (not the FHIR Storage module).
*/
@Interceptor
public class KafkaRecordHeaderAddingInterceptor {
private static final Logger ourLog = getLogger(KafkaRecordHeaderAddingInterceptor.class);
private FhirContext myFhirContext = FhirContext.forDstu3();
// To use R4, remove the above line and uncomment the following line
//private FhirContext myFhirContext = FhirContext.forR4();
@Hook(Pointcut.SUBSCRIPTION_BEFORE_MESSAGE_DELIVERY)
public boolean beforeMessageDelivery(CanonicalSubscription theSubscription, ResourceDeliveryMessage theMessage, ResourceModifiedJsonMessage theWrapper) {
/*
* If you want this interceptor to only apply to a specific subscription, you
* can filter here. In this case we're only adding kafka record headers to deliveries for
* the subscription with ID: Subscription/patient-sender
*/
if (!"patient-sender".equals(theSubscription.getIdPart())) {
return true;
}
Map<String, Object> customHeaders = theWrapper.getHapiHeaders().getCustomHeaders();
IBaseResource payload = theMessage.getPayload(myFhirContext);
//Add some basic information
customHeaders.put("resource-id", payload.getIdElement().getValue());
customHeaders.put("resource-type", myFhirContext.getResourceType(payload));
//Add a list of all tags. In Kafka, this will materialize as N headers, where N is the number of tags..
List<String> tags = payload.getMeta()
.getTag()
.stream()
.map(tag -> tag.getSystem() + "|" + tag.getCode())
.collect(Collectors.toList());
customHeaders.put("tags ", tags);
//Now some specific patient headers, since we know this subscription only works for patients
Patient patient = (Patient) payload;
customHeaders.put("patient-family-name", patient.getNameFirstRep().getFamily());
return true;
}
}
Take this interceptor in conjunction with the following Patient record:
{
"resourceType": "Patient",
"meta": {
"tag": [ {
"system": "system-1",
"code": "code-1",
"display": "display-1"
}, {
"system": "system-2",
"code": "code-2",
"display": "display-2"
}, {
"system": "system-3",
"code": "code-3",
"display": "display-3"
} ]
},
"active": true,
"name": [ {
"family": "Graham"
} ]
}
After being sent to the remote message channel, the following record headers would be shown for the newly-arrived record:
[
{"tag":"system-1|code-1"},
{"tag": "system-2|code-2"},
{"tag": "system-3|code-3"},
{"resourceType": "Patient"},
{"resourceId": "Patient/1354/_history/1"},
{"patient-family-name": "Graham"}
]
Note that if you place a value into the customHeaders
section which is a Collection (e.g. List/Set), the values will be unrolled into individaul record headers for kafka.
For an example of this you can see the tags above.
Related to the custom delivery class, users may alternatively choose to have the resource delivered to a queue or topic in the Message Broker defined and managed separately from Smile CDR. This is useful in cases where you want to be able to retrieve resources directly off of a message queue or topic or if you have a custom handler defined in your Message Broker to manage delivery.
The following example shows a subscription that uses a site-defined external topic or queue:
{
"resourceType": "Subscription",
"id": "1",
"meta": {
"versionId": "1",
"lastUpdated": "2017-08-19T16:52:11.041-04:00"
},
"status": "active",
"reason": "Monitor new neonatal function (note, age will be determined by the monitor)",
"criteria": "Observation?code=SNOMED-CT|1000000050&_format=xml",
"channel": {
"extension": [
{
"url": "https://smilecdr.com/fhir/ns/StructureDefinition/subscription-named-channel-delivery",
"valueString": "MyQueueName"
}
],
"type": "rest-hook",
"endpoint": "http://localhost:40243/fhir/context",
"payload": "application/json",
"header": [
"Authorization: Bearer 123"
]
}
}
Note the following extension:
URL | Type | Description |
---|---|---|
https://smilecdr.com/fhir/ns/StructureDefinition/subscription-named-channel-delivery | string | This is the name for the external message queue or topic that resources will be copied to. |