diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java index 7d95055d548..a073f2a0dcf 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java @@ -196,11 +196,7 @@ public class BundleBuilder { public UpdateBuilder addTransactionUpdateEntry(IBaseResource theResource) { Validate.notNull(theResource, "theResource must not be null"); - IIdType id = theResource.getIdElement(); - if (id.hasIdPart() && !id.hasResourceType()) { - String resourceType = myContext.getResourceType(theResource); - id = id.withResourceType(resourceType); - } + IIdType id = getIdTypeForUpdate(theResource); String requestUrl = id.toUnqualifiedVersionless().getValue(); String fullUrl = id.getValue(); @@ -225,13 +221,29 @@ public class BundleBuilder { myEntryRequestUrlChild.getMutator().setValue(request, url); // Bundle.entry.request.method - IPrimitiveType method = (IPrimitiveType) - myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments()); - method.setValueAsString(theHttpVerb); - myEntryRequestMethodChild.getMutator().setValue(request, method); + addRequestMethod(request, theHttpVerb); return url; } + /** + * Adds an entry containing an update (UPDATE) request without the body of the resource. + * Also sets the Bundle.type value to "transaction" if it is not already set. + * + * @param theResource The resource to update. + */ + public void addTransactionUpdateIdOnlyEntry(IBaseResource theResource) { + setBundleField("type", "transaction"); + + Validate.notNull(theResource, "theResource must not be null"); + + IIdType id = getIdTypeForUpdate(theResource); + String requestUrl = id.toUnqualifiedVersionless().getValue(); + String fullUrl = id.getValue(); + String httpMethod = "PUT"; + + addIdOnlyEntry(requestUrl, httpMethod, fullUrl); + } + /** * Adds an entry containing an create (POST) request. * Also sets the Bundle.type value to "transaction" if it is not already set. @@ -247,20 +259,47 @@ public class BundleBuilder { String resourceType = myContext.getResourceType(theResource); // Bundle.entry.request.url - IPrimitiveType url = - (IPrimitiveType) myContext.getElementDefinition("uri").newInstance(); - url.setValueAsString(resourceType); - myEntryRequestUrlChild.getMutator().setValue(request, url); + addRequestUrl(request, resourceType); - // Bundle.entry.request.url - IPrimitiveType method = (IPrimitiveType) - myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments()); - method.setValueAsString("POST"); - myEntryRequestMethodChild.getMutator().setValue(request, method); + // Bundle.entry.request.method + addRequestMethod(request, "POST"); return new CreateBuilder(request); } + /** + * Adds an entry containing a create (POST) request without the body of the resource. + * Also sets the Bundle.type value to "transaction" if it is not already set. + * + * @param theResource The resource to create + */ + public void addTransactionCreateEntryIdOnly(IBaseResource theResource) { + setBundleField("type", "transaction"); + + String requestUrl = myContext.getResourceType(theResource); + String fullUrl = theResource.getIdElement().getValue(); + String httpMethod = "POST"; + + addIdOnlyEntry(requestUrl, httpMethod, fullUrl); + } + + private void addIdOnlyEntry(String theRequestUrl, String theHttpMethod, String theFullUrl) { + IBase entry = addEntry(); + + // Bundle.entry.request + IBase request = myEntryRequestDef.newInstance(); + myEntryRequestChild.getMutator().setValue(entry, request); + + // Bundle.entry.request.url + addRequestUrl(request, theRequestUrl); + + // Bundle.entry.request.method + addRequestMethod(request, theHttpMethod); + + // Bundle.entry.fullUrl + addFullUrl(entry, theFullUrl); + } + /** * Adds an entry containing a delete (DELETE) request. * Also sets the Bundle.type value to "transaction" if it is not already set. @@ -341,20 +380,44 @@ public class BundleBuilder { IBase request = addEntryAndReturnRequest(); // Bundle.entry.request.url - IPrimitiveType url = - (IPrimitiveType) myContext.getElementDefinition("uri").newInstance(); - url.setValueAsString(theDeleteUrl); - myEntryRequestUrlChild.getMutator().setValue(request, url); + addRequestUrl(request, theDeleteUrl); // Bundle.entry.request.method - IPrimitiveType method = (IPrimitiveType) - myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments()); - method.setValueAsString("DELETE"); - myEntryRequestMethodChild.getMutator().setValue(request, method); + addRequestMethod(request, "DELETE"); return new DeleteBuilder(); } + private IIdType getIdTypeForUpdate(IBaseResource theResource) { + IIdType id = theResource.getIdElement(); + if (id.hasIdPart() && !id.hasResourceType()) { + String resourceType = myContext.getResourceType(theResource); + id = id.withResourceType(resourceType); + } + return id; + } + + private void addFullUrl(IBase theEntry, String theFullUrl) { + IPrimitiveType fullUrl = + (IPrimitiveType) myContext.getElementDefinition("uri").newInstance(); + fullUrl.setValueAsString(theFullUrl); + myEntryFullUrlChild.getMutator().setValue(theEntry, fullUrl); + } + + private void addRequestUrl(IBase request, String theRequestUrl) { + IPrimitiveType url = + (IPrimitiveType) myContext.getElementDefinition("uri").newInstance(); + url.setValueAsString(theRequestUrl); + myEntryRequestUrlChild.getMutator().setValue(request, url); + } + + private void addRequestMethod(IBase theRequest, String theMethod) { + IPrimitiveType method = (IPrimitiveType) + myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments()); + method.setValueAsString(theMethod); + myEntryRequestMethodChild.getMutator().setValue(theRequest, method); + } + /** * Adds an entry for a Collection bundle type */ @@ -406,10 +469,7 @@ public class BundleBuilder { IBase entry = addEntry(); // Bundle.entry.fullUrl - IPrimitiveType fullUrl = - (IPrimitiveType) myContext.getElementDefinition("uri").newInstance(); - fullUrl.setValueAsString(theFullUrl); - myEntryFullUrlChild.getMutator().setValue(entry, fullUrl); + addFullUrl(entry, theFullUrl); // Bundle.entry.resource myEntryResourceChild.getMutator().setValue(entry, theResource); diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5498-add-empty-and-id-only-payload-content-support-for-topic-subscriptions.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5498-add-empty-and-id-only-payload-content-support-for-topic-subscriptions.yaml new file mode 100644 index 00000000000..55ed0de5ec6 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5498-add-empty-and-id-only-payload-content-support-for-topic-subscriptions.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 5498 +title: "Added support for `id-only` and `empty` payload content types for notifications +triggered by R5, R4B, and R4 back-ported topic subscriptions." diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilder.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilder.java index 2efab3bb355..39b3c5c4e5f 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilder.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilder.java @@ -23,12 +23,14 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription; +import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; import ca.uhn.fhir.jpa.topic.status.INotificationStatusBuilder; import ca.uhn.fhir.jpa.topic.status.R4BNotificationStatusBuilder; import ca.uhn.fhir.jpa.topic.status.R4NotificationStatusBuilder; import ca.uhn.fhir.jpa.topic.status.R5NotificationStatusBuilder; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.util.BundleBuilder; +import org.apache.commons.lang3.ObjectUtils; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r5.model.Bundle; @@ -37,6 +39,8 @@ import org.slf4j.LoggerFactory; import java.util.List; +import static org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE; + public class SubscriptionTopicPayloadBuilder { private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTopicPayloadBuilder.class); private final FhirContext myFhirContext; @@ -73,7 +77,7 @@ public class SubscriptionTopicPayloadBuilder { myNotificationStatusBuilder.buildNotificationStatus(theResources, theActiveSubscription, theTopicUrl); bundleBuilder.addCollectionEntry(notificationStatus); - addResources(bundleBuilder, theResources, theRestOperationType); + addResources(theResources, theActiveSubscription.getSubscription(), theRestOperationType, bundleBuilder); // WIP STR5 add support for notificationShape include, revinclude // Note we need to set the bundle type after we add the resources since adding the resources automatically sets @@ -87,7 +91,46 @@ public class SubscriptionTopicPayloadBuilder { return retval; } - private static void addResources( + private void addResources( + List theResources, + CanonicalSubscription theCanonicalSubscription, + RestOperationTypeEnum theRestOperationType, + BundleBuilder theBundleBuilder) { + + org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent content = + ObjectUtils.defaultIfNull(theCanonicalSubscription.getContent(), FULLRESOURCE); + + switch (content) { + case EMPTY: + // skip adding resource to the Bundle + break; + case IDONLY: + addIdOnly(theBundleBuilder, theResources, theRestOperationType); + break; + case FULLRESOURCE: + addFullResources(theBundleBuilder, theResources, theRestOperationType); + break; + } + } + + private void addIdOnly( + BundleBuilder bundleBuilder, List theResources, RestOperationTypeEnum theRestOperationType) { + for (IBaseResource resource : theResources) { + switch (theRestOperationType) { + case CREATE: + bundleBuilder.addTransactionCreateEntryIdOnly(resource); + break; + case UPDATE: + bundleBuilder.addTransactionUpdateIdOnlyEntry(resource); + break; + case DELETE: + bundleBuilder.addTransactionDeleteEntry(resource); + break; + } + } + } + + private void addFullResources( BundleBuilder bundleBuilder, List theResources, RestOperationTypeEnum theRestOperationType) { for (IBaseResource resource : theResources) { switch (theRestOperationType) { diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicUtil.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicUtil.java index 0a05e8f44aa..95908704ad0 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicUtil.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicUtil.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.topic; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage; import ca.uhn.fhir.rest.server.messaging.BaseResourceMessage; import ca.uhn.fhir.util.BundleUtil; @@ -72,4 +73,14 @@ public class SubscriptionTopicUtil { .findFirst() .orElse(null); } + + /** + * Checks if {@link CanonicalSubscription} has EMPTY {@link org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent} + * Used for R5/R4B/R4 Notification Status object building. + */ + public static boolean isEmptyContentTopicSubscription(CanonicalSubscription theCanonicalSubscription) { + return theCanonicalSubscription.isTopicSubscription() + && org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.EMPTY + == theCanonicalSubscription.getTopicSubscription().getContent(); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/status/R4NotificationStatusBuilder.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/status/R4NotificationStatusBuilder.java index 68da40e3b99..16300900bda 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/status/R4NotificationStatusBuilder.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/status/R4NotificationStatusBuilder.java @@ -21,6 +21,8 @@ package ca.uhn.fhir.jpa.topic.status; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription; +import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; +import ca.uhn.fhir.jpa.topic.SubscriptionTopicUtil; import ca.uhn.fhir.subscription.SubscriptionConstants; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.CanonicalType; @@ -46,6 +48,7 @@ public class R4NotificationStatusBuilder implements INotificationStatusBuilder

theResources, ActiveSubscription theActiveSubscription, String theTopicUrl) { Long eventNumber = theActiveSubscription.getDeliveriesCount(); + CanonicalSubscription canonicalSubscription = theActiveSubscription.getSubscription(); // See http://build.fhir.org/ig/HL7/fhir-subscription-backport-ig/Parameters-r4-notification-status.json.html // and @@ -66,12 +69,12 @@ public class R4NotificationStatusBuilder implements INotificationStatusBuilder

0) { + if (!theResources.isEmpty() && !SubscriptionTopicUtil.isEmptyContentTopicSubscription(canonicalSubscription)) { IBaseResource firstResource = theResources.get(0); - notificationEvent - .addPart() - .setName("focus") - .setValue(new Reference(firstResource.getIdElement().toUnqualifiedVersionless())); + Reference resourceReference = + new Reference(firstResource.getIdElement().toUnqualifiedVersionless()); + + notificationEvent.addPart().setName("focus").setValue(resourceReference); } return parameters; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/status/R5NotificationStatusBuilder.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/status/R5NotificationStatusBuilder.java index c9128d9f638..8140066b929 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/status/R5NotificationStatusBuilder.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/status/R5NotificationStatusBuilder.java @@ -21,6 +21,8 @@ package ca.uhn.fhir.jpa.topic.status; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription; +import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; +import ca.uhn.fhir.jpa.topic.SubscriptionTopicUtil; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r5.model.Enumerations; import org.hl7.fhir.r5.model.Reference; @@ -40,6 +42,7 @@ public class R5NotificationStatusBuilder implements INotificationStatusBuilder theResources, ActiveSubscription theActiveSubscription, String theTopicUrl) { long eventNumber = theActiveSubscription.getDeliveriesCount(); + CanonicalSubscription canonicalSubscription = theActiveSubscription.getSubscription(); SubscriptionStatus subscriptionStatus = new SubscriptionStatus(); subscriptionStatus.setId(UUID.randomUUID().toString()); @@ -50,7 +53,7 @@ public class R5NotificationStatusBuilder implements INotificationStatusBuilder 0) { + if (!theResources.isEmpty() && !SubscriptionTopicUtil.isEmptyContentTopicSubscription(canonicalSubscription)) { event.setFocus(new Reference(theResources.get(0).getIdElement())); } subscriptionStatus.setSubscription( diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilderR4BTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilderR4BTest.java index 20a6dd42e45..a37808dbfa5 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilderR4BTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilderR4BTest.java @@ -8,74 +8,177 @@ import ca.uhn.fhir.util.BundleUtil; import org.hl7.fhir.r4b.model.Bundle; import org.hl7.fhir.r4b.model.Encounter; import org.hl7.fhir.r4b.model.Resource; -import org.junit.jupiter.api.Test; +import org.hl7.fhir.r4b.model.SubscriptionStatus; +import org.hl7.fhir.r5.model.Subscription; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class SubscriptionTopicPayloadBuilderR4BTest { private static final String TEST_TOPIC_URL = "test-builder-topic-url"; FhirContext ourFhirContext = FhirContext.forR4BCached(); - @Test - public void testBuildPayloadDelete() { + private SubscriptionTopicPayloadBuilder myStPayloadBuilder; + private Encounter myEncounter; + private CanonicalSubscription myCanonicalSubscription; + private ActiveSubscription myActiveSubscription; + + @BeforeEach + void before() { + myStPayloadBuilder = new SubscriptionTopicPayloadBuilder(ourFhirContext); + myEncounter = new Encounter(); + myEncounter.setId("Encounter/1"); + myCanonicalSubscription = new CanonicalSubscription(); + myCanonicalSubscription.setTopicSubscription(true); + myActiveSubscription = new ActiveSubscription(myCanonicalSubscription, "test"); + } + + @ParameterizedTest + @ValueSource(strings = { + "full-resource", + "" // payload content not provided + }) + public void testBuildPayload_deleteWithFullResourceContent_returnsCorrectPayload(String thePayloadContent) { // setup - var svc = new SubscriptionTopicPayloadBuilder(ourFhirContext); - var encounter = new Encounter(); - encounter.setId("Encounter/1"); - CanonicalSubscription sub = new CanonicalSubscription(); - ActiveSubscription subscription = new ActiveSubscription(sub, "test"); + Subscription.SubscriptionPayloadContent payloadContent = + Subscription.SubscriptionPayloadContent.fromCode(thePayloadContent); + myCanonicalSubscription.getTopicSubscription().setContent(payloadContent); // run - Bundle payload = (Bundle)svc.buildPayload(List.of(encounter), subscription, TEST_TOPIC_URL, RestOperationTypeEnum.DELETE); + Bundle payload = (Bundle) myStPayloadBuilder.buildPayload(List.of(myEncounter), myActiveSubscription, TEST_TOPIC_URL, RestOperationTypeEnum.DELETE); - // verify + // verify Bundle size + assertEquals(2, payload.getEntry().size()); List resources = BundleUtil.toListOfResourcesOfType(ourFhirContext, payload, Resource.class); assertEquals(1, resources.size()); - assertEquals("SubscriptionStatus", resources.get(0).getResourceType().name()); - assertEquals(Bundle.HTTPVerb.DELETE, payload.getEntry().get(1).getRequest().getMethod()); + // verify SubscriptionStatus.notificationEvent.focus + verifySubscriptionStatusNotificationEvent(resources.get(0)); + + // verify Encounter entry + Bundle.BundleEntryComponent encounterEntry = payload.getEntry().get(1); + assertNull(encounterEntry.getResource()); + assertNull(encounterEntry.getFullUrl()); + verifyRequestParameters(encounterEntry, org.hl7.fhir.r5.model.Bundle.HTTPVerb.DELETE.name(), "Encounter/1"); } - @Test - public void testBuildPayloadUpdate() { + @ParameterizedTest + @CsvSource({ + "create, POST , full-resource, Encounter/1, Encounter", + "update, PUT , full-resource, Encounter/1, Encounter/1", + "create, POST , , Encounter/1, Encounter", + "update, PUT , , Encounter/1, Encounter/1", + }) + public void testBuildPayload_createUpdateWithFullResourceContent_returnsCorrectPayload(String theRestOperationType, + String theHttpMethod, + String thePayloadContent, + String theFullUrl, + String theRequestUrl) { // setup - var svc = new SubscriptionTopicPayloadBuilder(ourFhirContext); - var encounter = new Encounter(); - encounter.setId("Encounter/1"); - CanonicalSubscription sub = new CanonicalSubscription(); - ActiveSubscription subscription = new ActiveSubscription(sub, "test"); + Subscription.SubscriptionPayloadContent payloadContent = + Subscription.SubscriptionPayloadContent.fromCode(thePayloadContent); + + myCanonicalSubscription.getTopicSubscription().setContent(payloadContent); + RestOperationTypeEnum restOperationType = RestOperationTypeEnum.forCode(theRestOperationType); // run - Bundle payload = (Bundle)svc.buildPayload(List.of(encounter), subscription, TEST_TOPIC_URL, RestOperationTypeEnum.UPDATE); + Bundle payload = (Bundle) myStPayloadBuilder.buildPayload(List.of(myEncounter), myActiveSubscription, TEST_TOPIC_URL, restOperationType); - // verify + // verify Bundle size + assertEquals(2, payload.getEntry().size()); List resources = BundleUtil.toListOfResourcesOfType(ourFhirContext, payload, Resource.class); assertEquals(2, resources.size()); - assertEquals("SubscriptionStatus", resources.get(0).getResourceType().name()); - assertEquals("Encounter", resources.get(1).getResourceType().name()); - assertEquals(Bundle.HTTPVerb.PUT, payload.getEntry().get(1).getRequest().getMethod()); + // verify SubscriptionStatus.notificationEvent.focus + verifySubscriptionStatusNotificationEvent(resources.get(0)); + + // verify Encounter entry + Bundle.BundleEntryComponent encounterEntry = payload.getEntry().get(1); + assertEquals("Encounter", resources.get(1).getResourceType().name()); + assertEquals(myEncounter, resources.get(1)); + assertEquals(theFullUrl, encounterEntry.getFullUrl()); + verifyRequestParameters(encounterEntry, theHttpMethod, theRequestUrl); } - @Test - public void testBuildPayloadCreate() { + @ParameterizedTest + @CsvSource({ + "create, POST , Encounter/1, Encounter", + "update, PUT , Encounter/1, Encounter/1", + "delete, DELETE, , Encounter/1" + }) + public void testBuildPayload_withIdOnlyContent_returnsCorrectPayload(String theRestOperationType, + String theHttpMethod, String theFullUrl, + String theRequestUrl) { // setup - var svc = new SubscriptionTopicPayloadBuilder(ourFhirContext); - var encounter = new Encounter(); - encounter.setId("Encounter/1"); - CanonicalSubscription sub = new CanonicalSubscription(); - ActiveSubscription subscription = new ActiveSubscription(sub, "test"); + myCanonicalSubscription.getTopicSubscription().setContent(Subscription.SubscriptionPayloadContent.IDONLY); + RestOperationTypeEnum restOperationType = RestOperationTypeEnum.forCode(theRestOperationType); // run - Bundle payload = (Bundle)svc.buildPayload(List.of(encounter), subscription, TEST_TOPIC_URL, RestOperationTypeEnum.CREATE); + Bundle payload = (Bundle) myStPayloadBuilder.buildPayload(List.of(myEncounter), myActiveSubscription, TEST_TOPIC_URL, restOperationType); - // verify + // verify Bundle size + assertEquals(2, payload.getEntry().size()); List resources = BundleUtil.toListOfResourcesOfType(ourFhirContext, payload, Resource.class); - assertEquals(2, resources.size()); - assertEquals("SubscriptionStatus", resources.get(0).getResourceType().name()); - assertEquals("Encounter", resources.get(1).getResourceType().name()); + assertEquals(1, resources.size()); - assertEquals(Bundle.HTTPVerb.POST, payload.getEntry().get(1).getRequest().getMethod()); + // verify SubscriptionStatus.notificationEvent.focus + verifySubscriptionStatusNotificationEvent(resources.get(0)); + + // verify Encounter entry + Bundle.BundleEntryComponent encounterEntry = payload.getEntry().get(1); + assertNull(encounterEntry.getResource()); + assertEquals(theFullUrl, encounterEntry.getFullUrl()); + verifyRequestParameters(encounterEntry, theHttpMethod, theRequestUrl); + } + + @ParameterizedTest + @CsvSource({ + "create", + "update", + "delete" + }) + public void testBuildPayload_withEmptyContent_returnsCorrectPayload(String theRestOperationType) { + // setup + myCanonicalSubscription.getTopicSubscription().setContent(Subscription.SubscriptionPayloadContent.EMPTY); + RestOperationTypeEnum restOperationType = RestOperationTypeEnum.forCode(theRestOperationType); + + // run + Bundle payload = (Bundle) myStPayloadBuilder.buildPayload(List.of(myEncounter), myActiveSubscription, TEST_TOPIC_URL, restOperationType); + + // verify Bundle size + assertEquals(1, payload.getEntry().size()); + List resources = BundleUtil.toListOfResourcesOfType(ourFhirContext, payload, Resource.class); + assertEquals(1, resources.size()); + + // verify SubscriptionStatus.notificationEvent.focus + assertEquals("SubscriptionStatus", resources.get(0).getResourceType().name()); + assertEquals(1, ((SubscriptionStatus) resources.get(0)).getNotificationEvent().size()); + SubscriptionStatus.SubscriptionStatusNotificationEventComponent notificationEvent = + ((SubscriptionStatus) resources.get(0)).getNotificationEventFirstRep(); + assertFalse(notificationEvent.hasFocus()); + } + + private void verifyRequestParameters(Bundle.BundleEntryComponent theEncounterEntry, + String theHttpMethod, String theRequestUrl) { + assertNotNull(theEncounterEntry.getRequest()); + assertEquals(theHttpMethod, theEncounterEntry.getRequest().getMethod().name()); + assertEquals(theRequestUrl, theEncounterEntry.getRequest().getUrl()); + } + + private void verifySubscriptionStatusNotificationEvent(Resource theResource) { + assertEquals("SubscriptionStatus", theResource.getResourceType().name()); + assertEquals(1, ((SubscriptionStatus) theResource).getNotificationEvent().size()); + SubscriptionStatus.SubscriptionStatusNotificationEventComponent notificationEvent = + ((SubscriptionStatus) theResource).getNotificationEventFirstRep(); + assertTrue(notificationEvent.hasFocus()); + assertEquals(myEncounter.getId(), notificationEvent.getFocus().getReference()); } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilderR5Test.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilderR5Test.java index 808ed42d8e9..d3f57de55ee 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilderR5Test.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicPayloadBuilderR5Test.java @@ -8,74 +8,177 @@ import ca.uhn.fhir.util.BundleUtil; import org.hl7.fhir.r5.model.Bundle; import org.hl7.fhir.r5.model.Encounter; import org.hl7.fhir.r5.model.Resource; -import org.junit.jupiter.api.Test; +import org.hl7.fhir.r5.model.Subscription; +import org.hl7.fhir.r5.model.SubscriptionStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class SubscriptionTopicPayloadBuilderR5Test { - private static final String TEST_TOPIC_URL = "test-builder-topic-url"; - FhirContext ourFhirContext = FhirContext.forR5Cached(); - @Test - public void testBuildPayloadDelete() { - // setup - var svc = new SubscriptionTopicPayloadBuilder(ourFhirContext); - var encounter = new Encounter(); - encounter.setId("Encounter/1"); - CanonicalSubscription sub = new CanonicalSubscription(); - ActiveSubscription subscription = new ActiveSubscription(sub, "test"); + private static final String TEST_TOPIC_URL = "test-builder-topic-url"; + FhirContext ourFhirContext = FhirContext.forR5Cached(); + private SubscriptionTopicPayloadBuilder myStPayloadBuilder; + private Encounter myEncounter; + private CanonicalSubscription myCanonicalSubscription; + private ActiveSubscription myActiveSubscription; - // run - Bundle payload = (Bundle)svc.buildPayload(List.of(encounter), subscription, TEST_TOPIC_URL, RestOperationTypeEnum.DELETE); + @BeforeEach + void before() { + myStPayloadBuilder = new SubscriptionTopicPayloadBuilder(ourFhirContext); + myEncounter = new Encounter(); + myEncounter.setId("Encounter/1"); + myCanonicalSubscription = new CanonicalSubscription(); + myCanonicalSubscription.setTopicSubscription(true); + myActiveSubscription = new ActiveSubscription(myCanonicalSubscription, "test"); + } - // verify - List resources = BundleUtil.toListOfResourcesOfType(ourFhirContext, payload, Resource.class); - assertEquals(1, resources.size()); - assertEquals("SubscriptionStatus", resources.get(0).getResourceType().name()); + @ParameterizedTest + @ValueSource(strings = { + "full-resource", + "" // payload content not provided + }) + public void testBuildPayload_deleteWithFullResourceContent_returnsCorrectPayload(String thePayloadContent) { + // setup + Subscription.SubscriptionPayloadContent payloadContent = + Subscription.SubscriptionPayloadContent.fromCode(thePayloadContent); + myCanonicalSubscription.getTopicSubscription().setContent(payloadContent); - assertEquals(Bundle.HTTPVerb.DELETE, payload.getEntry().get(1).getRequest().getMethod()); - } + // run + Bundle payload = (Bundle) myStPayloadBuilder.buildPayload(List.of(myEncounter), myActiveSubscription, TEST_TOPIC_URL, RestOperationTypeEnum.DELETE); - @Test - public void testBuildPayloadUpdate() { - // setup - var svc = new SubscriptionTopicPayloadBuilder(ourFhirContext); - var encounter = new Encounter(); - encounter.setId("Encounter/1"); - CanonicalSubscription sub = new CanonicalSubscription(); - ActiveSubscription subscription = new ActiveSubscription(sub, "test"); + // verify Bundle size + assertEquals(2, payload.getEntry().size()); + List resources = BundleUtil.toListOfResourcesOfType(ourFhirContext, payload, Resource.class); + assertEquals(1, resources.size()); - // run - Bundle payload = (Bundle)svc.buildPayload(List.of(encounter), subscription, TEST_TOPIC_URL, RestOperationTypeEnum.UPDATE); + // verify SubscriptionStatus.notificationEvent.focus + verifySubscriptionStatusNotificationEvent(resources.get(0)); - // verify - List resources = BundleUtil.toListOfResourcesOfType(ourFhirContext, payload, Resource.class); - assertEquals(2, resources.size()); - assertEquals("SubscriptionStatus", resources.get(0).getResourceType().name()); - assertEquals("Encounter", resources.get(1).getResourceType().name()); + // verify Encounter entry + Bundle.BundleEntryComponent encounterEntry = payload.getEntry().get(1); + assertNull(encounterEntry.getResource()); + assertNull(encounterEntry.getFullUrl()); + verifyRequestParameters(encounterEntry, Bundle.HTTPVerb.DELETE.name(), "Encounter/1"); + } - assertEquals(Bundle.HTTPVerb.PUT, payload.getEntry().get(1).getRequest().getMethod()); - } + @ParameterizedTest + @CsvSource({ + "create, POST , full-resource, Encounter/1, Encounter", + "update, PUT , full-resource, Encounter/1, Encounter/1", + "create, POST , , Encounter/1, Encounter", + "update, PUT , , Encounter/1, Encounter/1", + }) + public void testBuildPayload_createUpdateWithFullResourceContent_returnsCorrectPayload(String theRestOperationType, + String theHttpMethod, + String thePayloadContent, + String theFullUrl, + String theRequestUrl) { + // setup + Subscription.SubscriptionPayloadContent payloadContent = + Subscription.SubscriptionPayloadContent.fromCode(thePayloadContent); - @Test - public void testBuildPayloadCreate() { - // setup - var svc = new SubscriptionTopicPayloadBuilder(ourFhirContext); - var encounter = new Encounter(); - encounter.setId("Encounter/1"); - CanonicalSubscription sub = new CanonicalSubscription(); - ActiveSubscription subscription = new ActiveSubscription(sub, "test"); + myCanonicalSubscription.getTopicSubscription().setContent(payloadContent); + RestOperationTypeEnum restOperationType = RestOperationTypeEnum.forCode(theRestOperationType); - // run - Bundle payload = (Bundle)svc.buildPayload(List.of(encounter), subscription, TEST_TOPIC_URL, RestOperationTypeEnum.CREATE); + // run + Bundle payload = (Bundle) myStPayloadBuilder.buildPayload(List.of(myEncounter), myActiveSubscription, TEST_TOPIC_URL, restOperationType); - // verify - List resources = BundleUtil.toListOfResourcesOfType(ourFhirContext, payload, Resource.class); - assertEquals(2, resources.size()); - assertEquals("SubscriptionStatus", resources.get(0).getResourceType().name()); - assertEquals("Encounter", resources.get(1).getResourceType().name()); + // verify Bundle size + assertEquals(2, payload.getEntry().size()); + List resources = BundleUtil.toListOfResourcesOfType(ourFhirContext, payload, Resource.class); + assertEquals(2, resources.size()); - assertEquals(Bundle.HTTPVerb.POST, payload.getEntry().get(1).getRequest().getMethod()); - } + // verify SubscriptionStatus.notificationEvent.focus + verifySubscriptionStatusNotificationEvent(resources.get(0)); + + // verify Encounter entry + Bundle.BundleEntryComponent encounterEntry = payload.getEntry().get(1); + assertEquals("Encounter", resources.get(1).getResourceType().name()); + assertEquals(myEncounter, resources.get(1)); + assertEquals(theFullUrl, encounterEntry.getFullUrl()); + verifyRequestParameters(encounterEntry, theHttpMethod, theRequestUrl); + } + + @ParameterizedTest + @CsvSource({ + "create, POST , Encounter/1, Encounter", + "update, PUT , Encounter/1, Encounter/1", + "delete, DELETE, , Encounter/1" + }) + public void testBuildPayload_withIdOnlyContent_returnsCorrectPayload(String theRestOperationType, + String theHttpMethod, String theFullUrl, + String theRequestUrl) { + // setup + myCanonicalSubscription.getTopicSubscription().setContent(Subscription.SubscriptionPayloadContent.IDONLY); + RestOperationTypeEnum restOperationType = RestOperationTypeEnum.forCode(theRestOperationType); + + // run + Bundle payload = (Bundle) myStPayloadBuilder.buildPayload(List.of(myEncounter), myActiveSubscription, TEST_TOPIC_URL, restOperationType); + + // verify Bundle size + assertEquals(2, payload.getEntry().size()); + List resources = BundleUtil.toListOfResourcesOfType(ourFhirContext, payload, Resource.class); + assertEquals(1, resources.size()); + + // verify SubscriptionStatus.notificationEvent.focus + verifySubscriptionStatusNotificationEvent(resources.get(0)); + + // verify Encounter entry + Bundle.BundleEntryComponent encounterEntry = payload.getEntry().get(1); + assertNull(encounterEntry.getResource()); + assertEquals(theFullUrl, encounterEntry.getFullUrl()); + verifyRequestParameters(encounterEntry, theHttpMethod, theRequestUrl); + } + + @ParameterizedTest + @CsvSource({ + "create", + "update", + "delete" + }) + public void testBuildPayload_withEmptyContent_returnsCorrectPayload(String theRestOperationType) { + // setup + myCanonicalSubscription.getTopicSubscription().setContent(Subscription.SubscriptionPayloadContent.EMPTY); + RestOperationTypeEnum restOperationType = RestOperationTypeEnum.forCode(theRestOperationType); + + // run + Bundle payload = (Bundle) myStPayloadBuilder.buildPayload(List.of(myEncounter), myActiveSubscription, TEST_TOPIC_URL, restOperationType); + + // verify Bundle size + assertEquals(1, payload.getEntry().size()); + List resources = BundleUtil.toListOfResourcesOfType(ourFhirContext, payload, Resource.class); + assertEquals(1, resources.size()); + + // verify SubscriptionStatus.notificationEvent.focus + assertEquals("SubscriptionStatus", resources.get(0).getResourceType().name()); + assertEquals(1, ((SubscriptionStatus) resources.get(0)).getNotificationEvent().size()); + SubscriptionStatus.SubscriptionStatusNotificationEventComponent notificationEvent = + ((SubscriptionStatus) resources.get(0)).getNotificationEventFirstRep(); + assertFalse(notificationEvent.hasFocus()); + } + + private void verifyRequestParameters(Bundle.BundleEntryComponent theEncounterEntry, + String theHttpMethod, String theRequestUrl) { + assertNotNull(theEncounterEntry.getRequest()); + assertEquals(theHttpMethod, theEncounterEntry.getRequest().getMethod().name()); + assertEquals(theRequestUrl, theEncounterEntry.getRequest().getUrl()); + } + + private void verifySubscriptionStatusNotificationEvent(Resource theResource) { + assertEquals("SubscriptionStatus", theResource.getResourceType().name()); + assertEquals(1, ((SubscriptionStatus) theResource).getNotificationEvent().size()); + SubscriptionStatus.SubscriptionStatusNotificationEventComponent notificationEvent = + ((SubscriptionStatus) theResource).getNotificationEventFirstRep(); + assertTrue(notificationEvent.hasFocus()); + assertEquals(myEncounter.getId(), notificationEvent.getFocus().getReference()); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicUtilTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicUtilTest.java index adf5b851055..5915f8e4956 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicUtilTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicUtilTest.java @@ -1,6 +1,9 @@ package ca.uhn.fhir.jpa.topic; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription; +import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; +import ca.uhn.fhir.jpa.subscription.model.CanonicalTopicSubscription; import ca.uhn.fhir.rest.server.messaging.BaseResourceMessage; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r5.model.Bundle; @@ -8,9 +11,12 @@ import org.hl7.fhir.r5.model.Enumeration; import org.hl7.fhir.r5.model.Patient; import org.hl7.fhir.r5.model.Reference; import org.hl7.fhir.r5.model.Resource; +import org.hl7.fhir.r5.model.Subscription; import org.hl7.fhir.r5.model.SubscriptionStatus; import org.hl7.fhir.r5.model.SubscriptionTopic; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import java.util.List; @@ -21,6 +27,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class SubscriptionTopicUtilTest { + private static final String TEST_CHANNEL_NAME = "TEST_CHANNEL"; + private final FhirContext myContext = FhirContext.forR5Cached(); @Test @@ -86,4 +94,32 @@ class SubscriptionTopicUtilTest { IBaseResource extractionResult = SubscriptionTopicUtil.extractResourceFromBundle(myContext, new Bundle()); assertNull(extractionResult); } + + @Test + public void testIsEmptyContentTopicSubscription_withEmptySubscription_returnsFalse() { + CanonicalSubscription canonicalSubscription = new CanonicalSubscription(); + boolean result = SubscriptionTopicUtil.isEmptyContentTopicSubscription(canonicalSubscription); + + assertFalse(result); + } + + @ParameterizedTest + @CsvSource({ + "full-resource, false", + "id-only , false", + "empty , true", + " , false", + }) + public void testIsEmptyContentTopicSubscription_withContentPayload_returnsExpectedResult(String thePayloadContent, + boolean theExpectedResult) { + CanonicalTopicSubscription canonicalTopicSubscription = new CanonicalTopicSubscription(); + canonicalTopicSubscription.setContent(Subscription.SubscriptionPayloadContent.fromCode(thePayloadContent)); + CanonicalSubscription canonicalSubscription = new CanonicalSubscription(); + canonicalSubscription.setTopicSubscription(canonicalTopicSubscription); + canonicalSubscription.setTopicSubscription(true); + + boolean actualResult = SubscriptionTopicUtil.isEmptyContentTopicSubscription(canonicalSubscription); + + assertEquals(theExpectedResult, actualResult); + } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java index 29163d8018c..525f909a088 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java @@ -5,7 +5,6 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.subscription.BaseSubscriptionsR4Test; import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.test.util.StoppableSubscriptionDeliveringRestHookSubscriber; -import ca.uhn.fhir.jpa.test.util.SubscriptionTestUtil; import ca.uhn.fhir.jpa.topic.SubscriptionTopicDispatcher; import ca.uhn.fhir.jpa.topic.SubscriptionTopicRegistry; import ca.uhn.fhir.model.primitive.IdDt; @@ -15,6 +14,7 @@ import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.subscription.SubscriptionTestDataHelper; +import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.HapiExtensions; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IIdType; @@ -29,11 +29,12 @@ import org.hl7.fhir.r4.model.Meta; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.SearchParameter; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Subscription; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -42,6 +43,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -66,6 +68,8 @@ import static org.junit.jupiter.api.Assertions.fail; */ public class RestHookTestR4Test extends BaseSubscriptionsR4Test { private static final Logger ourLog = LoggerFactory.getLogger(RestHookTestR4Test.class); + public static final String TEST_PATIENT_ID = "topic-test-patient-id"; + public static final String PATIENT_REFERENCE = "Patient/" + TEST_PATIENT_ID; @Autowired ResourceModifiedSubmitterSvc myResourceModifiedSubmitterSvc; @@ -1306,8 +1310,73 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test { } @Test - public void testTopicSubscription() throws Exception { - Subscription subscription = SubscriptionTestDataHelper.buildR4TopicSubscription(); + public void testRestHoodTopicSubscription_withEmptyPayloadContent_generateCorrectPayload() throws Exception { + String payloadContent = "empty"; + + // execute + Bundle bundle = createAndDispatchTopicSubscription(payloadContent); + + // verify Bundle size + assertEquals(1, bundle.getEntry().size()); + List resources = BundleUtil.toListOfResourcesOfType(myFhirContext, bundle, Resource.class); + assertEquals(1, resources.size()); + + // verify SubscriptionStatus.notificationEvent.focus + Optional focus = getNotificationEventFocus(resources); + assertFalse(focus.isPresent()); + } + + @Test + public void testRestHoodTopicSubscription_withIdOnlyPayloadContent_generateCorrectPayload() throws Exception { + String payloadContent = "id-only"; + + // execute + Bundle bundle = createAndDispatchTopicSubscription(payloadContent); + + // verify Bundle size + assertEquals(2, bundle.getEntry().size()); + List resources = BundleUtil.toListOfResourcesOfType(myFhirContext, bundle, Resource.class); + assertEquals(1, resources.size()); + + // verify SubscriptionStatus.notificationEvent.focus + Optional focus = getNotificationEventFocus(resources); + assertTrue(focus.isPresent()); + assertEquals(TEST_PATIENT_ID, ((Reference) focus.get().getValue()).getReference()); + + // verify Patient Entry + Bundle.BundleEntryComponent patientEntry = bundle.getEntry().get(1); + validateRequestParameters(patientEntry); + Patient bundlePatient = (Patient) patientEntry.getResource(); + assertNull(bundlePatient); + } + + @Test + public void testRestHoodTopicSubscription_withFullResourcePayloadContent_generateCorrectPayload() throws Exception { + String payloadContent = "full-resource"; + + // execute + Bundle bundle = createAndDispatchTopicSubscription(payloadContent); + + // verify Bundle size + assertEquals(2, bundle.getEntry().size()); + List resources = BundleUtil.toListOfResourcesOfType(myFhirContext, bundle, Resource.class); + assertEquals(2, resources.size()); + + // verify SubscriptionStatus.notificationEvent.focus + Optional focus = getNotificationEventFocus(resources); + assertTrue(focus.isPresent()); + assertEquals(PATIENT_REFERENCE, ((Reference) focus.get().getValue()).getReference()); + + // verify Patient Entry + Bundle.BundleEntryComponent patientEntry = bundle.getEntry().get(1); + validateRequestParameters(patientEntry); + Patient bundlePatient = (Patient) patientEntry.getResource(); + assertTrue(bundlePatient.getActive()); + assertEquals(Enumerations.AdministrativeGender.FEMALE, bundlePatient.getGender()); + } + + private Bundle createAndDispatchTopicSubscription(String thePayloadContent) throws Exception { + Subscription subscription = SubscriptionTestDataHelper.buildR4TopicSubscriptionWithContent(thePayloadContent); subscription.setIdElement(null); subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED); Subscription.SubscriptionChannelComponent channel = subscription.getChannel(); @@ -1319,9 +1388,8 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test { mySubscriptionIds.add(methodOutcome.getId()); waitForActivatedSubscriptionCount(1); - String patientId = "topic-test-patient-id"; Patient patient = new Patient(); - patient.setId(patientId); + patient.setId(TEST_PATIENT_ID); patient.setActive(true); patient.setGender(Enumerations.AdministrativeGender.FEMALE); @@ -1332,12 +1400,22 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test { ourTransactionProvider.waitForTransactionCount(1); - Bundle bundle = ourTransactionProvider.getTransactions().get(0); - assertEquals(2, bundle.getEntry().size()); - Parameters parameters = (Parameters) bundle.getEntry().get(0).getResource(); - // WIP STR5 assert parameters contents - Patient bundlePatient = (Patient) bundle.getEntry().get(1).getResource(); - assertTrue(bundlePatient.getActive()); - assertEquals(Enumerations.AdministrativeGender.FEMALE, bundlePatient.getGender()); + return ourTransactionProvider.getTransactions().get(0); + } + + private Optional getNotificationEventFocus(List theResources) { + assertEquals("Parameters", theResources.get(0).getResourceType().name()); + Parameters parameters = (Parameters) theResources.get(0); + Parameters.ParametersParameterComponent notificationEvent = parameters.getParameter("notification-event"); + assertNotNull(notificationEvent); + return notificationEvent.getPart().stream() + .filter(part -> part.getName().equals("focus")) + .findFirst(); + } + + private void validateRequestParameters(Bundle.BundleEntryComponent thePatientEntry) { + assertNotNull(thePatientEntry.getRequest()); + assertEquals("POST", thePatientEntry.getRequest().getMethod().name()); + assertEquals("Patient", thePatientEntry.getRequest().getUrl()); } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizer.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizer.java index b4c37fd59ea..7801d9a7993 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizer.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizer.java @@ -60,6 +60,7 @@ import java.util.Map; import java.util.stream.Collectors; import static ca.uhn.fhir.util.HapiExtensions.EX_SEND_DELETE_MESSAGES; +import static java.util.Objects.nonNull; import static java.util.stream.Collectors.mapping; import static java.util.stream.Collectors.toList; @@ -305,8 +306,6 @@ public class SubscriptionCanonicalizer { CanonicalTopicSubscription topicSubscription = retVal.getTopicSubscription(); topicSubscription.setTopic(getCriteria(theSubscription)); - // WIP STR5 support other content types - topicSubscription.setContent(org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE); retVal.setEndpointUrl(channel.getEndpoint()); retVal.setChannelType(getChannelType(subscription)); @@ -320,31 +319,37 @@ public class SubscriptionCanonicalizer { } if (channel.hasExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_HEARTBEAT_PERIOD_URL)) { - org.hl7.fhir.r4.model.Extension timeoutExtension = channel.getExtensionByUrl( + org.hl7.fhir.r4.model.Extension channelHeartbeatPeriotUrlExtension = channel.getExtensionByUrl( SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_HEARTBEAT_PERIOD_URL); - topicSubscription.setHeartbeatPeriod( - Integer.valueOf(timeoutExtension.getValue().primitiveValue())); + topicSubscription.setHeartbeatPeriod(Integer.valueOf( + channelHeartbeatPeriotUrlExtension.getValue().primitiveValue())); } if (channel.hasExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_TIMEOUT_URL)) { - org.hl7.fhir.r4.model.Extension timeoutExtension = + org.hl7.fhir.r4.model.Extension channelTimeoutUrlExtension = channel.getExtensionByUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_TIMEOUT_URL); topicSubscription.setTimeout( - Integer.valueOf(timeoutExtension.getValue().primitiveValue())); + Integer.valueOf(channelTimeoutUrlExtension.getValue().primitiveValue())); } if (channel.hasExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_MAX_COUNT)) { - org.hl7.fhir.r4.model.Extension timeoutExtension = + org.hl7.fhir.r4.model.Extension channelMaxCountExtension = channel.getExtensionByUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_MAX_COUNT); topicSubscription.setMaxCount( - Integer.valueOf(timeoutExtension.getValue().primitiveValue())); - } - if (channel.getPayloadElement() - .hasExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_PAYLOAD_CONTENT)) { - org.hl7.fhir.r4.model.Extension timeoutExtension = channel.getPayloadElement() - .getExtensionByUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_PAYLOAD_CONTENT); - topicSubscription.setContent(org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.fromCode( - timeoutExtension.getValue().primitiveValue())); + Integer.valueOf(channelMaxCountExtension.getValue().primitiveValue())); } + // setting full-resource PayloadContent if backport-payload-content is not provided + org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent payloadContent = + org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE; + + org.hl7.fhir.r4.model.Extension channelPayloadContentExtension = channel.getPayloadElement() + .getExtensionByUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_PAYLOAD_CONTENT); + + if (nonNull(channelPayloadContentExtension)) { + payloadContent = org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.fromCode( + channelPayloadContentExtension.getValue().primitiveValue()); + } + + topicSubscription.setContent(payloadContent); } else { retVal.setCriteriaString(getCriteria(theSubscription)); retVal.setEndpointUrl(channel.getEndpoint()); @@ -423,13 +428,25 @@ public class SubscriptionCanonicalizer { } if (retVal.isTopicSubscription()) { - retVal.getTopicSubscription().setTopic(getCriteria(theSubscription)); + CanonicalTopicSubscription topicSubscription = retVal.getTopicSubscription(); + topicSubscription.setTopic(getCriteria(theSubscription)); - // WIP STR5 support other content types - retVal.getTopicSubscription() - .setContent(org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE); retVal.setEndpointUrl(channel.getEndpoint()); retVal.setChannelType(getChannelType(subscription)); + + // setting full-resource PayloadContent if backport-payload-content is not provided + org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent payloadContent = + org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE; + + org.hl7.fhir.r4b.model.Extension channelPayloadContentExtension = channel.getPayloadElement() + .getExtensionByUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_PAYLOAD_CONTENT); + + if (nonNull(channelPayloadContentExtension)) { + payloadContent = org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.fromCode( + channelPayloadContentExtension.getValue().primitiveValue()); + } + + topicSubscription.setContent(payloadContent); } else { retVal.setCriteriaString(getCriteria(theSubscription)); retVal.setEndpointUrl(channel.getEndpoint()); diff --git a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizerTest.java b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizerTest.java index b48e2ca5198..1c34a8e7e73 100644 --- a/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizerTest.java +++ b/hapi-fhir-storage/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionCanonicalizerTest.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType; import ca.uhn.fhir.jpa.subscription.model.CanonicalTopicSubscriptionFilter; import ca.uhn.fhir.model.api.ExtensionDt; import ca.uhn.fhir.model.primitive.BooleanDt; +import ca.uhn.fhir.subscription.SubscriptionConstants; import ca.uhn.fhir.subscription.SubscriptionTestDataHelper; import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Extension; @@ -14,6 +15,8 @@ import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.Enumerations; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import static ca.uhn.fhir.rest.api.Constants.CT_FHIR_JSON_NEW; import static ca.uhn.fhir.util.HapiExtensions.EX_SEND_DELETE_MESSAGES; @@ -83,16 +86,12 @@ class SubscriptionCanonicalizerTest { assertTrue(canonicalize.getSendDeleteMessages()); } - @Test - public void testR5() { - // setup - SubscriptionCanonicalizer r5Canonicalizer = new SubscriptionCanonicalizer(FhirContext.forR5Cached()); + private org.hl7.fhir.r5.model.Subscription buildR5Subscription(org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent thePayloadContent) { org.hl7.fhir.r5.model.Subscription subscription = new org.hl7.fhir.r5.model.Subscription(); subscription.setStatus(Enumerations.SubscriptionStatusCodes.ACTIVE); subscription.setContentType(CT_FHIR_JSON_NEW); - // WIP STR5 support different content types - subscription.setContent(org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE); + subscription.setContent(thePayloadContent); subscription.setEndpoint("http://foo"); subscription.setTopic(SubscriptionTestDataHelper.TEST_TOPIC); Coding channelType = new Coding().setSystem("http://terminology.hl7.org/CodeSystem/subscription-channel-type").setCode("rest-hook"); @@ -102,13 +101,25 @@ class SubscriptionCanonicalizerTest { subscription.setHeartbeatPeriod(123); subscription.setMaxCount(456); + return subscription; + } + + @ParameterizedTest + @ValueSource(strings = {"full-resource", "id-only", "empty"}) + public void testR5Canonicalize_returnsCorrectCanonicalSubscription(String thePayloadContent) { + // setup + SubscriptionCanonicalizer r5Canonicalizer = new SubscriptionCanonicalizer(FhirContext.forR5Cached()); + org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent payloadContent = + org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.fromCode(thePayloadContent); + org.hl7.fhir.r5.model.Subscription subscription = buildR5Subscription(payloadContent); + // execute CanonicalSubscription canonical = r5Canonicalizer.canonicalize(subscription); // verify assertEquals(Subscription.SubscriptionStatus.ACTIVE, canonical.getStatus()); assertEquals(CT_FHIR_JSON_NEW, canonical.getContentType()); - assertEquals(org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE, canonical.getContent()); + assertEquals(payloadContent, canonical.getContent()); assertEquals("http://foo", canonical.getEndpointUrl()); assertEquals(SubscriptionTestDataHelper.TEST_TOPIC, canonical.getTopic()); assertEquals(CanonicalSubscriptionChannelType.RESTHOOK, canonical.getChannelType()); @@ -131,37 +142,72 @@ class SubscriptionCanonicalizerTest { assertEquals(456, canonical.getMaxCount()); } - @Test - void testR4Backport() { + @ParameterizedTest + @ValueSource(strings = {"full-resource", "id-only", "empty"}) + void testR4BCanonicalize_returnsCorrectCanonicalSubscription(String thePayloadContent) { + // Example drawn from http://build.fhir.org/ig/HL7/fhir-subscription-backport-ig/Subscription-subscription-zulip.json.html + + // setup + SubscriptionCanonicalizer r4bCanonicalizer = new SubscriptionCanonicalizer(FhirContext.forR4BCached()); + org.hl7.fhir.r4b.model.Subscription subscription = buildR4BSubscription(thePayloadContent); + + // execute + CanonicalSubscription canonical = r4bCanonicalizer.canonicalize(subscription); + + // verify + assertEquals(Subscription.SubscriptionStatus.ACTIVE, canonical.getStatus()); + verifyStandardSubscriptionParameters(canonical); + verifyChannelParameters(canonical, thePayloadContent); + } + + private org.hl7.fhir.r4b.model.Subscription buildR4BSubscription(String thePayloadContent) { + org.hl7.fhir.r4b.model.Subscription subscription = new org.hl7.fhir.r4b.model.Subscription(); + + subscription.setId("testId"); + subscription.getMeta().addTag("http://a", "b", "c"); + subscription.getMeta().addTag("http://d", "e", "f"); + subscription.setStatus(org.hl7.fhir.r4b.model.Enumerations.SubscriptionStatus.ACTIVE); + subscription.getChannel().setPayload(CT_FHIR_JSON_NEW); + subscription.getChannel().setType(org.hl7.fhir.r4b.model.Subscription.SubscriptionChannelType.RESTHOOK); + subscription.getChannel().setEndpoint(SubscriptionTestDataHelper.TEST_ENDPOINT); + + subscription.getMeta().addProfile(SubscriptionConstants.SUBSCRIPTION_TOPIC_PROFILE_URL); + subscription.setCriteria(SubscriptionTestDataHelper.TEST_TOPIC); + + subscription.getChannel().setPayload(CT_FHIR_JSON_NEW); + subscription.getChannel().addHeader(SubscriptionTestDataHelper.TEST_HEADER1); + subscription.getChannel().addHeader(SubscriptionTestDataHelper.TEST_HEADER2); + subscription.setStatus(org.hl7.fhir.r4b.model.Enumerations.SubscriptionStatus.ACTIVE); + + subscription + .getChannel() + .getPayloadElement() + .addExtension( + SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_PAYLOAD_CONTENT, + new org.hl7.fhir.r4b.model.CodeType(thePayloadContent)); + + return subscription; + } + + @ParameterizedTest + @ValueSource(strings = {"full-resource", "id-only", "empty"}) + void testR4canonicalize_withBackPortedSubscription_returnsCorrectCanonicalSubscription(String thePayloadContent) { // Example drawn from http://build.fhir.org/ig/HL7/fhir-subscription-backport-ig/Subscription-subscription-zulip.json.html // setup SubscriptionCanonicalizer r4Canonicalizer = new SubscriptionCanonicalizer(FhirContext.forR4Cached()); // execute - - CanonicalSubscription canonical = r4Canonicalizer.canonicalize(SubscriptionTestDataHelper.buildR4TopicSubscription()); + Subscription subscription = SubscriptionTestDataHelper.buildR4TopicSubscriptionWithContent(thePayloadContent); + CanonicalSubscription canonical = r4Canonicalizer.canonicalize(subscription); // verify // Standard R4 stuff - assertEquals(2, canonical.getTags().size()); - assertEquals("b", canonical.getTags().get("http://a")); - assertEquals("e", canonical.getTags().get("http://d")); - assertEquals("testId", canonical.getIdPart()); - assertEquals("testId", canonical.getIdElementString()); - assertEquals(SubscriptionTestDataHelper.TEST_ENDPOINT, canonical.getEndpointUrl()); - assertEquals(CT_FHIR_JSON_NEW, canonical.getContentType()); - assertThat(canonical.getHeaders(), hasSize(2)); - assertEquals(SubscriptionTestDataHelper.TEST_HEADER1, canonical.getHeaders().get(0)); - assertEquals(SubscriptionTestDataHelper.TEST_HEADER2, canonical.getHeaders().get(1)); + verifyStandardSubscriptionParameters(canonical); assertEquals(Subscription.SubscriptionStatus.ACTIVE, canonical.getStatus()); + verifyChannelParameters(canonical, thePayloadContent); - assertEquals(CT_FHIR_JSON_NEW, canonical.getContentType()); - assertEquals(org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE, canonical.getContent()); - assertEquals(SubscriptionTestDataHelper.TEST_ENDPOINT, canonical.getEndpointUrl()); - assertEquals(SubscriptionTestDataHelper.TEST_TOPIC, canonical.getTopic()); - assertEquals(CanonicalSubscriptionChannelType.RESTHOOK, canonical.getChannelType()); assertThat(canonical.getFilters(), hasSize(2)); CanonicalTopicSubscriptionFilter filter1 = canonical.getFilters().get(0); @@ -183,6 +229,26 @@ class SubscriptionCanonicalizerTest { assertEquals(20, canonical.getMaxCount()); } + private void verifyChannelParameters(CanonicalSubscription theCanonicalSubscriptions, String thePayloadContent) { + assertThat(theCanonicalSubscriptions.getHeaders(), hasSize(2)); + assertEquals(SubscriptionTestDataHelper.TEST_HEADER1, theCanonicalSubscriptions.getHeaders().get(0)); + assertEquals(SubscriptionTestDataHelper.TEST_HEADER2, theCanonicalSubscriptions.getHeaders().get(1)); + + assertEquals(CT_FHIR_JSON_NEW, theCanonicalSubscriptions.getContentType()); + assertEquals(thePayloadContent, theCanonicalSubscriptions.getContent().toCode()); + assertEquals(SubscriptionTestDataHelper.TEST_ENDPOINT, theCanonicalSubscriptions.getEndpointUrl()); + assertEquals(SubscriptionTestDataHelper.TEST_TOPIC, theCanonicalSubscriptions.getTopic()); + assertEquals(CanonicalSubscriptionChannelType.RESTHOOK, theCanonicalSubscriptions.getChannelType()); + } + + private void verifyStandardSubscriptionParameters(CanonicalSubscription theCanonicalSubscription) { + assertEquals(2, theCanonicalSubscription.getTags().size()); + assertEquals("b", theCanonicalSubscription.getTags().get("http://a")); + assertEquals("e", theCanonicalSubscription.getTags().get("http://d")); + assertEquals("testId", theCanonicalSubscription.getIdPart()); + assertEquals("testId", theCanonicalSubscription.getIdElementString()); + } + @NotNull private static org.hl7.fhir.r5.model.Subscription.SubscriptionFilterByComponent buildFilter(String theResourceType, String theParam, String theValue) { org.hl7.fhir.r5.model.Subscription.SubscriptionFilterByComponent filter = new org.hl7.fhir.r5.model.Subscription.SubscriptionFilterByComponent(); diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/subscription/SubscriptionTestDataHelper.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/subscription/SubscriptionTestDataHelper.java index 5edbd951d3a..19ba45e2fd8 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/subscription/SubscriptionTestDataHelper.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/subscription/SubscriptionTestDataHelper.java @@ -36,6 +36,10 @@ public class SubscriptionTestDataHelper { public static final String TEST_HEADER2 = "X-Bar: BAR"; public static Subscription buildR4TopicSubscription() { + return buildR4TopicSubscriptionWithContent("full-resource"); + } + + public static Subscription buildR4TopicSubscriptionWithContent(String theChannelPayloadContent) { Subscription subscription = new Subscription(); // Standard R4 stuff @@ -75,7 +79,7 @@ public class SubscriptionTestDataHelper { .getPayloadElement() .addExtension( SubscriptionConstants.SUBSCRIPTION_TOPIC_CHANNEL_PAYLOAD_CONTENT, - new CodeType("full-resource")); + new CodeType(theChannelPayloadContent)); return subscription; }