From 6b9af3291efb2d6be0001b2ac67edba594b168ea Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 26 Apr 2023 13:01:00 -0400 Subject: [PATCH] R5 Subscriptions (#4748) * comments from conversation with Gino * rewrite R5 subscription canonicalization. Expect the r5 subscription tests to fail now * SubscriptionTopicR5Test passes now * R4B tests pass now * first two tests in RestHookTestR5Test now pass. just need to convert the rest * third test passes * fourth test passes * tests pass up to line 294 * wow what a marathon. Turns out when we stripped the version we didn't remove it from the meta version. * passes up to 427 * RestHookTestR5Test tests pass up to line 582 * RestHookTestR5Test tests pass up to line 591 Added SubscriptionTopicRegisteringSubscriber * RestHookTestR5Test tests pass up to line 591 Added SubscriptionTopicRegisteringSubscriber * RestHookTestR5Test tests pass up to line 636 Added SubscriptionTopicValidatingInterceptor * RestHookTestR5Test tests pass up to line 689 * RestHookTestR5Test tests pass up to line 758 * 4 failures left * woohoo all tests pass * all tests pass and no PointCutLatch errors * Msg.code * changelog * checkstyle * fix some tests * compile issue * fix test * fix regression * fix test * R5 currently runs tests in multiple threads, so change the sensitive one to an IT * licenses * review feedback --------- Co-authored-by: Ken Stevens --- .../fhir/util/bundle/BundleEntryMutator.java | 6 + .../util/bundle/ModifiableBundleEntry.java | 4 + .../6_6_0/4748-r5-subscriptions.yaml | 8 + ...scriptionDeliveringRestHookSubscriber.java | 60 +- ...aseSubscriberForSubscriptionResources.java | 53 - .../MatchingQueueSubscriberLoader.java | 6 + .../SubscriptionActivatingSubscriber.java | 10 +- .../SubscriptionMatchingSubscriber.java | 2 +- .../SubscriptionRegisteringSubscriber.java | 6 +- .../registry/ActiveSubscriptionCache.java | 17 +- .../match/registry/SubscriptionRegistry.java | 8 +- .../config/SubscriptionSubmitterConfig.java | 8 + .../SubscriptionQueryValidator.java | 78 ++ .../SubscriptionSubmitInterceptorLoader.java | 9 + .../SubscriptionValidatingInterceptor.java | 42 +- .../topic/ActiveSubscriptionTopicCache.java | 4 + .../topic/SubscriptionTopicCanonicalizer.java | 45 + .../jpa/topic/SubscriptionTopicConfig.java | 29 +- .../jpa/topic/SubscriptionTopicMatcher.java | 3 +- .../SubscriptionTopicMatchingSubscriber.java | 14 +- .../SubscriptionTopicPayloadBuilder.java | 12 +- ...ubscriptionTopicRegisteringSubscriber.java | 134 +++ .../jpa/topic/SubscriptionTopicRegistry.java | 4 + ...ubscriptionTopicValidatingInterceptor.java | 117 +++ .../jpa/topic/SubscriptionTriggerMatcher.java | 17 +- .../matching/DaoSubscriptionMatcherTest.java | 3 + .../registry/ActiveSubscriptionCacheTest.java | 16 +- .../SubscriptionMatchingSubscriberTest.java | 6 +- ...SubscriptionValidatingInterceptorTest.java | 7 +- .../jpa/subscription/RestHookTestR4BTest.java | 136 +-- .../SubscriptionTopicR4BTest.java | 4 +- .../subscription/BaseSubscriptionsR5Test.java | 322 ++++-- .../subscription/SubscriptionTopicR5Test.java | 104 +- .../resthook/RestHookTestR5IT.java | 884 ++++++++++++++++ .../resthook/RestHookTestR5Test.java | 991 ------------------ .../BaseResourceModifiedMessage.java | 28 + .../registry/SubscriptionCanonicalizer.java | 90 +- .../model/CanonicalSubscription.java | 61 +- .../model/CanonicalTopicSubscription.java | 135 +++ .../CanonicalTopicSubscriptionFilter.java | 82 ++ .../SubscriptionCanonicalizerTest.java | 67 ++ .../uhn/test/concurrency/PointcutLatch.java | 1 + 42 files changed, 2159 insertions(+), 1474 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4748-r5-subscriptions.yaml delete mode 100644 hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/BaseSubscriberForSubscriptionResources.java create mode 100644 hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionQueryValidator.java create mode 100644 hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicCanonicalizer.java create mode 100644 hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicRegisteringSubscriber.java create mode 100644 hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicValidatingInterceptor.java create mode 100644 hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR5IT.java delete mode 100644 hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR5Test.java create mode 100644 hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/CanonicalTopicSubscription.java create mode 100644 hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/CanonicalTopicSubscriptionFilter.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/bundle/BundleEntryMutator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/bundle/BundleEntryMutator.java index b7ac634feec..4c0b8b16dba 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/bundle/BundleEntryMutator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/bundle/BundleEntryMutator.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.util.ParametersUtil; import org.hl7.fhir.instance.model.api.IBase; +import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; public class BundleEntryMutator { @@ -57,4 +58,9 @@ public class BundleEntryMutator { BaseRuntimeChildDefinition fullUrlChild = myEntryDefinition.getChildByName("fullUrl"); fullUrlChild.getMutator().setValue(myEntry, value); } + + public void setResource(IBaseResource theUpdatedResource) { + BaseRuntimeChildDefinition resourceChild = myEntryDefinition.getChildByName("resource"); + resourceChild.getMutator().setValue(myEntry, theUpdatedResource); + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/bundle/ModifiableBundleEntry.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/bundle/ModifiableBundleEntry.java index 2099c446dab..5c5eee68ad2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/bundle/ModifiableBundleEntry.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/bundle/ModifiableBundleEntry.java @@ -54,4 +54,8 @@ public class ModifiableBundleEntry { public void setFullUrl(String theFullUrl) { myBundleEntryMutator.setFullUrl(theFullUrl); } + + public void setResource(IBaseResource theUpdatedResource) { + myBundleEntryMutator.setResource(theUpdatedResource); + } } diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4748-r5-subscriptions.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4748-r5-subscriptions.yaml new file mode 100644 index 00000000000..182cead67bc --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/6_6_0/4748-r5-subscriptions.yaml @@ -0,0 +1,8 @@ +--- +type: change +issue: 4748 +title: "Previously, HAPI-FHIR converted R5 Subscriptions into R4 Subscriptions and triggered those subscriptions by +resource changes in the same way R4 subscriptions are triggered. Now R5 Subscriptions are triggered based on the topic +they subscribe to and the resource matching happens via the SubscriptionTopic resource. This also means that +R5 Subscription endpoints are now delivered a subscription-notification Bundle as opposed to the resource as is the +case with R4 Subscriptions." diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/deliver/resthook/SubscriptionDeliveringRestHookSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/deliver/resthook/SubscriptionDeliveringRestHookSubscriber.java index c345e2f6ef1..7c18d21300a 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/deliver/resthook/SubscriptionDeliveringRestHookSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/deliver/resthook/SubscriptionDeliveringRestHookSubscriber.java @@ -43,6 +43,7 @@ import ca.uhn.fhir.rest.gclient.IClientExecutable; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.messaging.BaseResourceModifiedMessage; +import ca.uhn.fhir.util.BundleUtil; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -87,7 +88,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe IClientExecutable operation; if (theSubscription.isTopicSubscription()) { - operation = createDeliveryRequestTopic((IBaseBundle) theMsg.getPayload(myFhirContext), theClient, thePayloadResource); + operation = createDeliveryRequestTopic((IBaseBundle) thePayloadResource, theClient); } else if (isNotBlank(theSubscription.getPayloadSearchCriteria())) { operation = createDeliveryRequestTransaction(theSubscription, theClient, thePayloadResource); } else if (thePayloadType != null) { @@ -141,43 +142,76 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe return theClient.transaction().withBundle(bundle); } - private IClientExecutable createDeliveryRequestTopic(IBaseBundle theBundle, IGenericClient theClient, IBaseResource thePayloadResource) { + private IClientExecutable createDeliveryRequestTopic(IBaseBundle theBundle, IGenericClient theClient) { return theClient.transaction().withBundle(theBundle); } - public IBaseResource getResource(IIdType payloadId, RequestPartitionId thePartitionId, boolean theDeletedOK) throws ResourceGoneException { - RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(payloadId.getResourceType()); + public IBaseResource getResource(IIdType thePayloadId, RequestPartitionId thePartitionId, boolean theDeletedOK) throws ResourceGoneException { + RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(thePayloadId.getResourceType()); SystemRequestDetails systemRequestDetails = new SystemRequestDetails().setRequestPartitionId(thePartitionId); IFhirResourceDao dao = myDaoRegistry.getResourceDao(resourceDef.getImplementingClass()); - return dao.read(payloadId.toVersionless(), systemRequestDetails, theDeletedOK); + return dao.read(thePayloadId.toVersionless(), systemRequestDetails, theDeletedOK); } - + /** + * Perform operations on the payload based on various subscription extension settings such as deliver latest version, + * delete and/or strip version id. + * @param theMsg + * @param theSubscription + * @return + */ protected IBaseResource getAndMassagePayload(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription) { IBaseResource payloadResource = theMsg.getPayload(myFhirContext); - if (payloadResource == null || theSubscription.getRestHookDetails().isDeliverLatestVersion()) { - IIdType payloadId = theMsg.getPayloadId(myFhirContext); + if (payloadResource instanceof IBaseBundle) { + return getAndMassageBundle(theMsg, (IBaseBundle) payloadResource, theSubscription); + } else { + return getAndMassageResource(theMsg, payloadResource, theSubscription); + } + } + private IBaseResource getAndMassageBundle(ResourceDeliveryMessage theMsg, IBaseBundle theBundle, CanonicalSubscription theSubscription) { + BundleUtil.processEntries(myFhirContext, theBundle, entry -> { + IBaseResource entryResource = entry.getResource(); + if (entryResource != null) { + // SubscriptionStatus is a "virtual" resource type that is not stored in the repository + if (!"SubscriptionStatus".equals(myFhirContext.getResourceType(entryResource))) { + IBaseResource updatedResource = getAndMassageResource(theMsg, entryResource, theSubscription); + entry.setFullUrl(updatedResource.getIdElement().getValue()); + entry.setResource(updatedResource); + } + } + }); + return theBundle; + } + + private IBaseResource getAndMassageResource(ResourceDeliveryMessage theMsg, IBaseResource thePayloadResource, CanonicalSubscription theSubscription) { + if (thePayloadResource == null || theSubscription.getRestHookDetails().isDeliverLatestVersion()) { + + IIdType payloadId = theMsg.getPayloadId(myFhirContext).toVersionless(); + if (theSubscription.isTopicSubscription()) { + payloadId = thePayloadResource.getIdElement().toVersionless(); + } try { if (payloadId != null) { boolean deletedOK = theMsg.getOperationType() == BaseResourceModifiedMessage.OperationTypeEnum.DELETE; - payloadResource = getResource(payloadId.toVersionless(), theMsg.getRequestPartitionId(), deletedOK); + thePayloadResource = getResource(payloadId, theMsg.getRequestPartitionId(), deletedOK); } else { return null; } } catch (ResourceGoneException e) { - ourLog.warn("Resource {} is deleted, not going to deliver for subscription {}", payloadId.toVersionless(), theSubscription.getIdElement(myFhirContext)); + ourLog.warn("Resource {} is deleted, not going to deliver for subscription {}", payloadId, theSubscription.getIdElement(myFhirContext)); return null; } } - IIdType resourceId = payloadResource.getIdElement(); + IIdType resourceId = thePayloadResource.getIdElement(); if (theSubscription.getRestHookDetails().isStripVersionId()) { resourceId = resourceId.toVersionless(); - payloadResource.setId(resourceId); + thePayloadResource.setId(resourceId); + thePayloadResource.getMeta().setVersionId(null); } - return payloadResource; + return thePayloadResource; } @Override diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/BaseSubscriberForSubscriptionResources.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/BaseSubscriberForSubscriptionResources.java deleted file mode 100644 index e5fc17c86d4..00000000000 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/BaseSubscriberForSubscriptionResources.java +++ /dev/null @@ -1,53 +0,0 @@ -/*- - * #%L - * HAPI FHIR Subscription Server - * %% - * Copyright (C) 2014 - 2023 Smile CDR, Inc. - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ -package ca.uhn.fhir.jpa.subscription.match.matcher.subscriber; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; -import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.messaging.MessageHandler; - -import static org.apache.commons.lang3.StringUtils.isBlank; - -public abstract class BaseSubscriberForSubscriptionResources implements MessageHandler { - - @Autowired - protected FhirContext myFhirContext; - - protected boolean isSubscription(ResourceModifiedMessage theNewResource) { - String payloadIdType = null; - IIdType payloadId = theNewResource.getPayloadId(myFhirContext); - if (payloadId != null) { - payloadIdType = payloadId.getResourceType(); - } - if (isBlank(payloadIdType)) { - IBaseResource payload = theNewResource.getNewPayload(myFhirContext); - if (payload != null) { - payloadIdType = myFhirContext.getResourceType(payload); - } - } - - return ResourceTypeEnum.SUBSCRIPTION.getCode().equals(payloadIdType); - } - -} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/MatchingQueueSubscriberLoader.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/MatchingQueueSubscriberLoader.java index 4ae38f21577..2e0bdedffa5 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/MatchingQueueSubscriberLoader.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/MatchingQueueSubscriberLoader.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.jpa.subscription.channel.api.ChannelConsumerSettings; import ca.uhn.fhir.jpa.subscription.channel.api.IChannelReceiver; import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory; import ca.uhn.fhir.jpa.topic.SubscriptionTopicMatchingSubscriber; +import ca.uhn.fhir.jpa.topic.SubscriptionTopicRegisteringSubscriber; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -50,6 +51,8 @@ public class MatchingQueueSubscriberLoader { private SubscriptionChannelFactory mySubscriptionChannelFactory; @Autowired private SubscriptionRegisteringSubscriber mySubscriptionRegisteringSubscriber; + @Autowired(required = false) + private SubscriptionTopicRegisteringSubscriber mySubscriptionTopicRegisteringSubscriber; @Autowired private SubscriptionActivatingSubscriber mySubscriptionActivatingSubscriber; @Autowired @@ -70,6 +73,9 @@ public class MatchingQueueSubscriberLoader { ourLog.info("Starting SubscriptionTopic Matching Subscriber"); myMatchingChannel.subscribe(mySubscriptionTopicMatchingSubscriber); } + if (mySubscriptionTopicRegisteringSubscriber != null) { + myMatchingChannel.subscribe(mySubscriptionTopicRegisteringSubscriber); + } } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionActivatingSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionActivatingSubscriber.java index b8c3ef41d59..faa84c32c73 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionActivatingSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionActivatingSubscriber.java @@ -19,16 +19,16 @@ */ package ca.uhn.fhir.jpa.subscription.match.matcher.subscriber; -import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.model.entity.StorageSettings; -import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionConstants; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType; import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage; import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.SubscriptionUtil; @@ -49,9 +49,11 @@ import javax.annotation.Nonnull; *

* Also validates criteria. If invalid, rejects the subscription without persisting the subscription. */ -public class SubscriptionActivatingSubscriber extends BaseSubscriberForSubscriptionResources implements MessageHandler { +public class SubscriptionActivatingSubscriber implements MessageHandler { private final Logger ourLog = LoggerFactory.getLogger(SubscriptionActivatingSubscriber.class); @Autowired + private FhirContext myFhirContext; + @Autowired private DaoRegistry myDaoRegistry; @Autowired private SubscriptionCanonicalizer mySubscriptionCanonicalizer; @@ -73,7 +75,7 @@ public class SubscriptionActivatingSubscriber extends BaseSubscriberForSubscript } ResourceModifiedMessage payload = ((ResourceModifiedJsonMessage) theMessage).getPayload(); - if (!isSubscription(payload)) { + if (!payload.hasPayloadType(myFhirContext, "Subscription")) { return; } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionMatchingSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionMatchingSubscriber.java index 1691c88d863..e8c32f7cf8b 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionMatchingSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionMatchingSubscriber.java @@ -111,7 +111,7 @@ public class SubscriptionMatchingSubscriber implements MessageHandler { private void doMatchActiveSubscriptionsAndDeliver(ResourceModifiedMessage theMsg) { IIdType resourceId = theMsg.getPayloadId(myFhirContext); - Collection subscriptions = mySubscriptionRegistry.getAll(); + Collection subscriptions = mySubscriptionRegistry.getAllNonTopicSubscriptions(); ourLog.trace("Testing {} subscriptions for applicability", subscriptions.size()); boolean anySubscriptionsMatchedResource = false; diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionRegisteringSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionRegisteringSubscriber.java index 3eaabe261b5..44fad5830b4 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionRegisteringSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/subscriber/SubscriptionRegisteringSubscriber.java @@ -23,12 +23,12 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; -import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry; import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage; import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -47,7 +47,7 @@ import javax.annotation.Nonnull; *

* Also validates criteria. If invalid, rejects the subscription without persisting the subscription. */ -public class SubscriptionRegisteringSubscriber extends BaseSubscriberForSubscriptionResources implements MessageHandler { +public class SubscriptionRegisteringSubscriber implements MessageHandler { private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionRegisteringSubscriber.class); @Autowired private FhirContext myFhirContext; @@ -74,7 +74,7 @@ public class SubscriptionRegisteringSubscriber extends BaseSubscriberForSubscrip ResourceModifiedMessage payload = ((ResourceModifiedJsonMessage) theMessage).getPayload(); - if (!isSubscription(payload)) { + if (!payload.hasPayloadType(this.myFhirContext, "Subscription")) { return; } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/ActiveSubscriptionCache.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/ActiveSubscriptionCache.java index 23eea8bd6a1..671b0e95b5f 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/ActiveSubscriptionCache.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/ActiveSubscriptionCache.java @@ -85,11 +85,22 @@ class ActiveSubscriptionCache { return retval; } - public List getTopicSubscriptionsForUrl(String theUrl) { - assert !isBlank(theUrl); + /** + * R4B and R5 only + * @param theTopic + * @return a list of all subscriptions that are subscribed to the given topic + */ + public List getTopicSubscriptionsForTopic(String theTopic) { + assert !isBlank(theTopic); return getAll().stream() .filter(as -> as.getSubscription().isTopicSubscription()) - .filter(as -> theUrl.equals(as.getSubscription().getCriteriaString())) + .filter(as -> theTopic.equals(as.getSubscription().getTopic())) + .collect(Collectors.toList()); + } + + public List getAllNonTopicSubscriptions() { + return getAll().stream() + .filter(as -> !as.getSubscription().isTopicSubscription()) .collect(Collectors.toList()); } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistry.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistry.java index dd52043ac12..d701b1ddb7f 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistry.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/match/registry/SubscriptionRegistry.java @@ -74,8 +74,8 @@ public class SubscriptionRegistry { return myActiveSubscriptionCache.getAll(); } - public synchronized List getTopicSubscriptionsByUrl(String theUrl) { - return myActiveSubscriptionCache.getTopicSubscriptionsForUrl(theUrl); + public synchronized List getTopicSubscriptionsByTopic(String theTopic) { + return myActiveSubscriptionCache.getTopicSubscriptionsForTopic(theTopic); } private Optional hasSubscription(IIdType theId) { @@ -213,4 +213,8 @@ public class SubscriptionRegistry { public int size() { return myActiveSubscriptionCache.size(); } + + public synchronized List getAllNonTopicSubscriptions() { + return myActiveSubscriptionCache.getAllNonTopicSubscriptions(); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionSubmitterConfig.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionSubmitterConfig.java index 4c1a9973cbb..412e9c8ecc5 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionSubmitterConfig.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionSubmitterConfig.java @@ -19,8 +19,11 @@ */ package ca.uhn.fhir.jpa.subscription.submit.config; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator; import ca.uhn.fhir.jpa.subscription.model.config.SubscriptionModelConfig; import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator; import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionSubmitInterceptorLoader; import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionValidatingInterceptor; import ca.uhn.fhir.jpa.subscription.triggering.ISubscriptionTriggeringSvc; @@ -48,6 +51,11 @@ public class SubscriptionSubmitterConfig { return new SubscriptionValidatingInterceptor(); } + @Bean + public SubscriptionQueryValidator subscriptionQueryValidator(DaoRegistry theDaoRegistry, SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) { + return new SubscriptionQueryValidator(theDaoRegistry, theSubscriptionStrategyEvaluator); + } + @Bean public SubscriptionSubmitInterceptorLoader subscriptionMatcherInterceptorLoader() { return new SubscriptionSubmitInterceptorLoader(); diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionQueryValidator.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionQueryValidator.java new file mode 100644 index 00000000000..cb78dd85ccb --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionQueryValidator.java @@ -0,0 +1,78 @@ +/*- + * #%L + * HAPI FHIR Subscription Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.subscription.submit.interceptor; + +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy; +import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator; +import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionCriteriaParser; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; + +import static org.apache.commons.lang3.StringUtils.isBlank; + +public class SubscriptionQueryValidator { + private final DaoRegistry myDaoRegistry; + private final SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator; + + public SubscriptionQueryValidator(DaoRegistry theDaoRegistry, SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) { + myDaoRegistry = theDaoRegistry; + mySubscriptionStrategyEvaluator = theSubscriptionStrategyEvaluator; + } + + public void validateCriteria(String theCriteria, String theFieldName) { + if (isBlank(theCriteria)) { + throw new UnprocessableEntityException(Msg.code(11) + theFieldName + " must be populated"); + } + + SubscriptionCriteriaParser.SubscriptionCriteria parsedCriteria = SubscriptionCriteriaParser.parse(theCriteria); + if (parsedCriteria == null) { + throw new UnprocessableEntityException(Msg.code(12) + theFieldName + " can not be parsed"); + } + + if (parsedCriteria.getType() == SubscriptionCriteriaParser.TypeEnum.STARTYPE_EXPRESSION) { + return; + } + + for (String next : parsedCriteria.getApplicableResourceTypes()) { + if (!myDaoRegistry.isResourceTypeSupported(next)) { + throw new UnprocessableEntityException(Msg.code(13) + theFieldName + " contains invalid/unsupported resource type: " + next); + } + } + + if (parsedCriteria.getType() != SubscriptionCriteriaParser.TypeEnum.SEARCH_EXPRESSION) { + return; + } + + int sep = theCriteria.indexOf('?'); + if (sep <= 1) { + throw new UnprocessableEntityException(Msg.code(14) + theFieldName + " must be in the form \"{Resource Type}?[params]\""); + } + + String resType = theCriteria.substring(0, sep); + if (resType.contains("/")) { + throw new UnprocessableEntityException(Msg.code(15) + theFieldName + " must be in the form \"{Resource Type}?[params]\""); + } + } + + public SubscriptionMatchingStrategy determineStrategy(String theCriteriaString) { + return mySubscriptionStrategyEvaluator.determineStrategy(theCriteriaString); + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionSubmitInterceptorLoader.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionSubmitInterceptorLoader.java index 9da2fe47bea..bd285eabd71 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionSubmitInterceptorLoader.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionSubmitInterceptorLoader.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.subscription.submit.interceptor; import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.jpa.model.entity.StorageSettings; +import ca.uhn.fhir.jpa.topic.SubscriptionTopicValidatingInterceptor; import com.google.common.annotations.VisibleForTesting; import org.hl7.fhir.dstu2.model.Subscription; import org.slf4j.Logger; @@ -37,12 +38,15 @@ public class SubscriptionSubmitInterceptorLoader { private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor; @Autowired private SubscriptionValidatingInterceptor mySubscriptionValidatingInterceptor; + @Autowired(required = false) + private SubscriptionTopicValidatingInterceptor mySubscriptionTopicValidatingInterceptor; @Autowired private StorageSettings myStorageSettings; @Autowired private IInterceptorService myInterceptorRegistry; private boolean mySubscriptionValidatingInterceptorRegistered; private boolean mySubscriptionMatcherInterceptorRegistered; + private boolean mySubscriptionTopicValidatingInterceptorRegistered; @PostConstruct public void start() { @@ -62,6 +66,11 @@ public class SubscriptionSubmitInterceptorLoader { myInterceptorRegistry.registerInterceptor(mySubscriptionValidatingInterceptor); mySubscriptionValidatingInterceptorRegistered = true; } + + if (mySubscriptionTopicValidatingInterceptor != null && !mySubscriptionTopicValidatingInterceptorRegistered) { + myInterceptorRegistry.registerInterceptor(mySubscriptionTopicValidatingInterceptor); + mySubscriptionTopicValidatingInterceptorRegistered = true; + } } @VisibleForTesting diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java index 7ae4d70dd6f..1d62141a721 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptor.java @@ -33,7 +33,6 @@ import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy; import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator; -import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionCriteriaParser; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType; @@ -73,6 +72,8 @@ public class SubscriptionValidatingInterceptor { private FhirContext myFhirContext; @Autowired private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; + @Autowired + private SubscriptionQueryValidator mySubscriptionQueryValidator; @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED) public void resourcePreCreate(IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) { @@ -146,9 +147,9 @@ public class SubscriptionValidatingInterceptor { if (!finished) { if (subscription.isTopicSubscription()) { - Optional oTopic = findSubscriptionTopicByUrl(subscription.getCriteriaString()); + Optional oTopic = findSubscriptionTopicByUrl(subscription.getTopic()); if (!oTopic.isPresent()) { - throw new UnprocessableEntityException(Msg.code(2322) + "No SubscriptionTopic exists with url: " + subscription.getCriteriaString()); + throw new UnprocessableEntityException(Msg.code(2322) + "No SubscriptionTopic exists with topic: " + subscription.getTopic()); } } else { validateQuery(subscription.getCriteriaString(), "Subscription.criteria"); @@ -217,39 +218,7 @@ public class SubscriptionValidatingInterceptor { } public void validateQuery(String theQuery, String theFieldName) { - if (isBlank(theQuery)) { - throw new UnprocessableEntityException(Msg.code(11) + theFieldName + " must be populated"); - } - - SubscriptionCriteriaParser.SubscriptionCriteria parsedCriteria = SubscriptionCriteriaParser.parse(theQuery); - if (parsedCriteria == null) { - throw new UnprocessableEntityException(Msg.code(12) + theFieldName + " can not be parsed"); - } - - if (parsedCriteria.getType() == SubscriptionCriteriaParser.TypeEnum.STARTYPE_EXPRESSION) { - return; - } - - for (String next : parsedCriteria.getApplicableResourceTypes()) { - if (!myDaoRegistry.isResourceTypeSupported(next)) { - throw new UnprocessableEntityException(Msg.code(13) + theFieldName + " contains invalid/unsupported resource type: " + next); - } - } - - if (parsedCriteria.getType() != SubscriptionCriteriaParser.TypeEnum.SEARCH_EXPRESSION) { - return; - } - - int sep = theQuery.indexOf('?'); - if (sep <= 1) { - throw new UnprocessableEntityException(Msg.code(14) + theFieldName + " must be in the form \"{Resource Type}?[params]\""); - } - - String resType = theQuery.substring(0, sep); - if (resType.contains("/")) { - throw new UnprocessableEntityException(Msg.code(15) + theFieldName + " must be in the form \"{Resource Type}?[params]\""); - } - + mySubscriptionQueryValidator.validateCriteria(theQuery, theFieldName); } private Optional findSubscriptionTopicByUrl(String theCriteria) { @@ -332,6 +301,7 @@ public class SubscriptionValidatingInterceptor { @SuppressWarnings("WeakerAccess") public void setSubscriptionStrategyEvaluatorForUnitTest(SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) { mySubscriptionStrategyEvaluator = theSubscriptionStrategyEvaluator; + mySubscriptionQueryValidator = new SubscriptionQueryValidator(myDaoRegistry, theSubscriptionStrategyEvaluator); } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/ActiveSubscriptionTopicCache.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/ActiveSubscriptionTopicCache.java index 7cdb4806e03..1bda0e9ea48 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/ActiveSubscriptionTopicCache.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/ActiveSubscriptionTopicCache.java @@ -63,4 +63,8 @@ public class ActiveSubscriptionTopicCache { public Collection getAll() { return myCache.values(); } + + public void remove(String theSubscriptionTopicId) { + myCache.remove(theSubscriptionTopicId); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicCanonicalizer.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicCanonicalizer.java new file mode 100644 index 00000000000..d99d462947e --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicCanonicalizer.java @@ -0,0 +1,45 @@ +/*- + * #%L + * HAPI FHIR Subscription Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.topic; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.i18n.Msg; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r5.model.SubscriptionTopic; + +public final class SubscriptionTopicCanonicalizer { + private static final FhirContext ourFhirContextR5 = FhirContext.forR5(); + + private SubscriptionTopicCanonicalizer() { + } + + // WIP STR5 use elsewhere + public static SubscriptionTopic canonicalize(FhirContext theFhirContext, IBaseResource theSubscriptionTopic) { + switch (theFhirContext.getVersion().getVersion()) { + case R4B: + String encoded = theFhirContext.newJsonParser().encodeResourceToString(theSubscriptionTopic); + return ourFhirContextR5.newJsonParser().parseResource(SubscriptionTopic.class, encoded); + case R5: + return (SubscriptionTopic) theSubscriptionTopic; + default: + throw new UnsupportedOperationException(Msg.code(2337) + "Subscription topics are not supported in FHIR version " + theFhirContext.getVersion().getVersion()); + } + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicConfig.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicConfig.java index 387ef453009..c1008def1b0 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicConfig.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicConfig.java @@ -20,33 +20,52 @@ package ca.uhn.fhir.jpa.topic; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher; +import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelRegistry; +import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionMatchDeliverer; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator; import org.springframework.context.annotation.Bean; public class SubscriptionTopicConfig { @Bean - public SubscriptionTopicMatchingSubscriber subscriptionTopicMatchingSubscriber(FhirContext theFhirContext) { + SubscriptionMatchDeliverer subscriptionMatchDeliverer(FhirContext theFhirContext, IInterceptorBroadcaster theInterceptorBroadcaster, SubscriptionChannelRegistry theSubscriptionChannelRegistry) { + return new SubscriptionMatchDeliverer(theFhirContext, theInterceptorBroadcaster, theSubscriptionChannelRegistry); + } + + @Bean + SubscriptionTopicMatchingSubscriber subscriptionTopicMatchingSubscriber(FhirContext theFhirContext) { return new SubscriptionTopicMatchingSubscriber(theFhirContext); } @Bean - public SubscriptionTopicPayloadBuilder subscriptionTopicPayloadBuilder(FhirContext theFhirContext) { + SubscriptionTopicPayloadBuilder subscriptionTopicPayloadBuilder(FhirContext theFhirContext) { return new SubscriptionTopicPayloadBuilder(theFhirContext); } @Bean - public SubscriptionTopicRegistry subscriptionTopicRegistry() { + SubscriptionTopicRegistry subscriptionTopicRegistry() { return new SubscriptionTopicRegistry(); } @Bean - public SubscriptionTopicSupport subscriptionTopicSupport(FhirContext theFhirContext, DaoRegistry theDaoRegistry, SearchParamMatcher theSearchParamMatcher) { + SubscriptionTopicSupport subscriptionTopicSupport(FhirContext theFhirContext, DaoRegistry theDaoRegistry, SearchParamMatcher theSearchParamMatcher) { return new SubscriptionTopicSupport(theFhirContext, theDaoRegistry, theSearchParamMatcher); } @Bean - public SubscriptionTopicLoader subscriptionTopicLoader() { + SubscriptionTopicLoader subscriptionTopicLoader() { return new SubscriptionTopicLoader(); } + + @Bean + SubscriptionTopicRegisteringSubscriber subscriptionTopicRegisteringSubscriber() { + return new SubscriptionTopicRegisteringSubscriber(); + } + + @Bean + SubscriptionTopicValidatingInterceptor subscriptionTopicValidatingInterceptor(FhirContext theFhirContext, SubscriptionQueryValidator theSubscriptionQueryValidator) { + return new SubscriptionTopicValidatingInterceptor(theFhirContext, theSubscriptionQueryValidator); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatcher.java index d4458a02b09..ffedc145564 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatcher.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatcher.java @@ -45,12 +45,11 @@ public class SubscriptionTopicMatcher { SubscriptionTriggerMatcher matcher = new SubscriptionTriggerMatcher(mySubscriptionTopicSupport, theMsg, next); InMemoryMatchResult result = matcher.match(); if (result.matched()) { + // as soon as one trigger matches, we're done return result; } - // WIP STR5 should we check the other triggers? } } - // WIP STR5 add support for event triggers return InMemoryMatchResult.noMatch(); } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatchingSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatchingSubscriber.java index a9c1d5d7bc8..840b36ffae9 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatchingSubscriber.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicMatchingSubscriber.java @@ -29,6 +29,7 @@ import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry; import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage; import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; +import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r5.model.SubscriptionTopic; import org.slf4j.Logger; @@ -41,6 +42,7 @@ import org.springframework.messaging.MessagingException; import javax.annotation.Nonnull; import java.util.Collection; import java.util.List; +import java.util.UUID; public class SubscriptionTopicMatchingSubscriber implements MessageHandler { private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTopicMatchingSubscriber.class); @@ -95,21 +97,23 @@ public class SubscriptionTopicMatchingSubscriber implements MessageHandler { SubscriptionTopicMatcher matcher = new SubscriptionTopicMatcher(mySubscriptionTopicSupport, topic); InMemoryMatchResult result = matcher.match(theMsg); if (result.matched()) { - ourLog.info("Matched topic {} to message {}", topic.getIdElement().toUnqualifiedVersionless(), theMsg); + ourLog.info("Matched topic {} to message {}", topic.getUrl(), theMsg); deliverToTopicSubscriptions(theMsg, topic, result); } } } private void deliverToTopicSubscriptions(ResourceModifiedMessage theMsg, SubscriptionTopic topic, InMemoryMatchResult result) { - List topicSubscriptions = mySubscriptionRegistry.getTopicSubscriptionsByUrl(topic.getUrl()); + List topicSubscriptions = mySubscriptionRegistry.getTopicSubscriptionsByTopic(topic.getUrl()); if (!topicSubscriptions.isEmpty()) { IBaseResource matchedResource = theMsg.getNewPayload(myFhirContext); for (ActiveSubscription activeSubscription : topicSubscriptions) { - // WIP STR5 apply subscription filter - IBaseResource payload = mySubscriptionTopicPayloadBuilder.buildPayload(matchedResource, theMsg, activeSubscription, topic); - mySubscriptionMatchDeliverer.deliverPayload(payload, theMsg, activeSubscription, result); + // WIP STR5 apply subscription filters + IBaseBundle bundlePayload = mySubscriptionTopicPayloadBuilder.buildPayload(matchedResource, theMsg, activeSubscription, topic); + // WIP STR5 do we need to add a total? If so can do that with R5BundleFactory + bundlePayload.setId(UUID.randomUUID().toString()); + mySubscriptionMatchDeliverer.deliverPayload(bundlePayload, theMsg, activeSubscription, result); } } } 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 9b689569d35..523400b0f90 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 @@ -25,6 +25,7 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription; import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; import ca.uhn.fhir.util.BundleBuilder; +import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r5.model.Bundle; import org.hl7.fhir.r5.model.Enumerations; @@ -32,6 +33,8 @@ import org.hl7.fhir.r5.model.Reference; import org.hl7.fhir.r5.model.SubscriptionStatus; import org.hl7.fhir.r5.model.SubscriptionTopic; +import java.util.UUID; + public class SubscriptionTopicPayloadBuilder { private final FhirContext myFhirContext; @@ -39,7 +42,7 @@ public class SubscriptionTopicPayloadBuilder { myFhirContext = theFhirContext; } - public IBaseResource buildPayload(IBaseResource theMatchedResource, ResourceModifiedMessage theMsg, ActiveSubscription theActiveSubscription, SubscriptionTopic theTopic) { + public IBaseBundle buildPayload(IBaseResource theMatchedResource, ResourceModifiedMessage theMsg, ActiveSubscription theActiveSubscription, SubscriptionTopic theTopic) { BundleBuilder bundleBuilder = new BundleBuilder(myFhirContext); // WIP STR5 set eventsSinceSubscriptionStart from the database @@ -48,6 +51,8 @@ public class SubscriptionTopicPayloadBuilder { FhirVersionEnum fhirVersion = myFhirContext.getVersion().getVersion(); + // WIP STR5 add support for notificationShape include, revinclude + if (fhirVersion == FhirVersionEnum.R4B) { bundleBuilder.setType(Bundle.BundleType.HISTORY.toCode()); String serializedSubscriptionStatus = FhirContext.forR5Cached().newJsonParser().encodeResourceToString(subscriptionStatus); @@ -60,7 +65,8 @@ public class SubscriptionTopicPayloadBuilder { } else { throw new IllegalStateException(Msg.code(2331) + "SubscriptionTopic subscriptions are not supported on FHIR version: " + fhirVersion); } - // WIP STR5 is this the right type of entry? + // WIP STR5 is this the right type of entry? see http://hl7.org/fhir/subscriptionstatus-examples.html + // WIP STR5 Also see http://hl7.org/fhir/R4B/notification-full-resource.json.html need to conform to these bundleBuilder.addCollectionEntry(subscriptionStatus); switch (theMsg.getOperationType()) { case CREATE: @@ -78,9 +84,11 @@ public class SubscriptionTopicPayloadBuilder { private SubscriptionStatus buildSubscriptionStatus(IBaseResource theMatchedResource, ActiveSubscription theActiveSubscription, SubscriptionTopic theTopic, int theEventsSinceSubscriptionStart) { SubscriptionStatus subscriptionStatus = new SubscriptionStatus(); + subscriptionStatus.setId(UUID.randomUUID().toString()); subscriptionStatus.setStatus(Enumerations.SubscriptionStatusCodes.ACTIVE); subscriptionStatus.setType(SubscriptionStatus.SubscriptionNotificationType.EVENTNOTIFICATION); // WIP STR5 count events since subscription start and set eventsSinceSubscriptionStart + // store counts by subscription id subscriptionStatus.setEventsSinceSubscriptionStart(theEventsSinceSubscriptionStart); subscriptionStatus.addNotificationEvent().setEventNumber(theEventsSinceSubscriptionStart).setFocus(new Reference(theMatchedResource.getIdElement())); subscriptionStatus.setSubscription(new Reference(theActiveSubscription.getSubscription().getIdElement(myFhirContext))); diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicRegisteringSubscriber.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicRegisteringSubscriber.java new file mode 100644 index 00000000000..4e6dd812e80 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicRegisteringSubscriber.java @@ -0,0 +1,134 @@ +/*- + * #%L + * HAPI FHIR Subscription Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.topic; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage; +import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r5.model.Enumerations; +import org.hl7.fhir.r5.model.SubscriptionTopic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.MessagingException; + +import javax.annotation.Nonnull; + +/** + * Responsible for transitioning subscription resources from REQUESTED to ACTIVE + * Once activated, the subscription is added to the SubscriptionRegistry. + *

+ * Also validates criteria. If invalid, rejects the subscription without persisting the subscription. + */ +public class SubscriptionTopicRegisteringSubscriber implements MessageHandler { + private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTopicRegisteringSubscriber.class); + @Autowired + private FhirContext myFhirContext; + @Autowired + private SubscriptionTopicRegistry mySubscriptionTopicRegistry; + @Autowired + private DaoRegistry myDaoRegistry; + + /** + * Constructor + */ + public SubscriptionTopicRegisteringSubscriber() { + super(); + } + + @Override + public void handleMessage(@Nonnull Message theMessage) throws MessagingException { + if (!(theMessage instanceof ResourceModifiedJsonMessage)) { + ourLog.warn("Received message of unexpected type on matching channel: {}", theMessage); + return; + } + + ResourceModifiedMessage payload = ((ResourceModifiedJsonMessage) theMessage).getPayload(); + + if (!payload.hasPayloadType(myFhirContext, "SubscriptionTopic")) { + return; + } + + switch (payload.getOperationType()) { + case MANUALLY_TRIGGERED: + case TRANSACTION: + return; + case CREATE: + case UPDATE: + case DELETE: + break; + } + + // We read the resource back from the DB instead of using the supplied copy for + // two reasons: + // - in order to store partition id in the userdata of the resource for partitioned subscriptions + // - in case we're processing out of order and a create-then-delete has been processed backwards (or vice versa) + + IBaseResource payloadResource; + IIdType payloadId = payload.getPayloadId(myFhirContext).toUnqualifiedVersionless(); + try { + IFhirResourceDao subscriptionDao = myDaoRegistry.getResourceDao("SubscriptionTopic"); + RequestDetails systemRequestDetails = getPartitionAwareRequestDetails(payload); + payloadResource = subscriptionDao.read(payloadId, systemRequestDetails); + if (payloadResource == null) { + // Only for unit test + payloadResource = payload.getPayload(myFhirContext); + } + } catch (ResourceGoneException e) { + mySubscriptionTopicRegistry.unregister(payloadId.getIdPart()); + return; + } + + SubscriptionTopic subscriptionTopic = SubscriptionTopicCanonicalizer.canonicalize(myFhirContext, payloadResource); + if (subscriptionTopic.getStatus() == Enumerations.PublicationStatus.ACTIVE) { + mySubscriptionTopicRegistry.register(subscriptionTopic); + } else { + mySubscriptionTopicRegistry.unregister(payloadId.getIdPart()); + } + } + + /** + * There were some situations where the RequestDetails attempted to use the default partition + * and the partition name was a list containing null values (i.e. using the package installer to STORE_AND_INSTALL + * Subscriptions while partitioning was enabled). If any partition matches these criteria, + * {@link RequestPartitionId#defaultPartition()} is used to obtain the default partition. + */ + private RequestDetails getPartitionAwareRequestDetails(ResourceModifiedMessage payload) { + RequestPartitionId payloadPartitionId = payload.getPartitionId(); + if (payloadPartitionId == null || payloadPartitionId.isDefaultPartition()) { + // This may look redundant but the package installer STORE_AND_INSTALL Subscriptions when partitioning is enabled + // creates a corrupt default partition. This resets it to a clean one. + payloadPartitionId = RequestPartitionId.defaultPartition(); + } + return new SystemRequestDetails().setRequestPartitionId(payloadPartitionId); + } + + +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicRegistry.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicRegistry.java index 80ba64a0acd..f696abe6818 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicRegistry.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicRegistry.java @@ -45,4 +45,8 @@ public class SubscriptionTopicRegistry { public Collection getAll() { return myActiveSubscriptionTopicCache.getAll(); } + + public void unregister(String theSubscriptionTopicId) { + myActiveSubscriptionTopicCache.remove(theSubscriptionTopicId); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicValidatingInterceptor.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicValidatingInterceptor.java new file mode 100644 index 00000000000..b8cd552befd --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTopicValidatingInterceptor.java @@ -0,0 +1,117 @@ +/*- + * #%L + * HAPI FHIR Subscription Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.topic; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator; +import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import com.google.common.annotations.VisibleForTesting; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r5.model.SubscriptionTopic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SubscriptionTopicValidatingInterceptor { + private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTopicValidatingInterceptor.class); + private final FhirContext myFhirContext; + private final SubscriptionQueryValidator mySubscriptionQueryValidator; + + public SubscriptionTopicValidatingInterceptor(FhirContext theFhirContext, SubscriptionQueryValidator theSubscriptionQueryValidator) { + myFhirContext = theFhirContext; + mySubscriptionQueryValidator = theSubscriptionQueryValidator; + } + + @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED) + public void resourcePreCreate(IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) { + validateSubmittedSubscriptionTopic(theResource, theRequestDetails, theRequestPartitionId, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED); + } + + @Hook(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED) + public void resourceUpdated(IBaseResource theOldResource, IBaseResource theResource, RequestDetails theRequestDetails, RequestPartitionId theRequestPartitionId) { + validateSubmittedSubscriptionTopic(theResource, theRequestDetails, theRequestPartitionId, Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED); + } + + @VisibleForTesting + void validateSubmittedSubscriptionTopic(IBaseResource theSubscription, + RequestDetails theRequestDetails, + RequestPartitionId theRequestPartitionId, + Pointcut thePointcut) { + if (Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED != thePointcut && Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED != thePointcut) { + throw new UnprocessableEntityException(Msg.code(2340) + "Expected Pointcut to be either STORAGE_PRESTORAGE_RESOURCE_CREATED or STORAGE_PRESTORAGE_RESOURCE_UPDATED but was: " + thePointcut); + } + + if (!"SubscriptionTopic".equals(myFhirContext.getResourceType(theSubscription))) { + return; + } + + SubscriptionTopic subscriptionTopic = SubscriptionTopicCanonicalizer.canonicalize(myFhirContext, theSubscription); + + boolean finished = false; + if (subscriptionTopic.getStatus() == null) { + throw new UnprocessableEntityException(Msg.code(2338) + "Can not process submitted SubscriptionTopic - SubscriptionTopic.status must be populated on this server"); + } + + switch (subscriptionTopic.getStatus()) { + case ACTIVE: + break; + default: + finished = true; + break; + } + + // WIP STR5 add cross-partition support like in SubscriptionValidatingInterceptor + + // WIP STR5 warn if can't be evaluated in memory? + + if (!finished) { + subscriptionTopic.getResourceTrigger().stream() + .forEach(t -> validateQueryCriteria(t.getQueryCriteria())); + } + } + + private void validateQueryCriteria(SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent theQueryCriteria) { + if (theQueryCriteria.getPrevious() != null) { + validateCriteria(theQueryCriteria.getPrevious(), "SubscriptionTopic.resourceTrigger.queryCriteria.previous"); + } + if (theQueryCriteria.getCurrent() != null) { + validateCriteria(theQueryCriteria.getCurrent(), "SubscriptionTopic.resourceTrigger.queryCriteria.current"); + } + } + + public void validateCriteria(String theCriteria, String theFieldName) { + try { + mySubscriptionQueryValidator.validateCriteria(theCriteria, theFieldName); + SubscriptionMatchingStrategy strategy = mySubscriptionQueryValidator.determineStrategy(theCriteria); + if (strategy != SubscriptionMatchingStrategy.IN_MEMORY) { + ourLog.warn("Warning: Query Criteria '{}' in {} cannot be evaluated in-memory", theCriteria, theFieldName); + } + } catch (InvalidRequestException | DataFormatException e) { + throw new UnprocessableEntityException(Msg.code(2339) + "Invalid SubscriptionTopic criteria '" + theCriteria + "' in " + theFieldName + ": " + e.getMessage()); + } + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTriggerMatcher.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTriggerMatcher.java index 77773083ba3..7827095ff13 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTriggerMatcher.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/topic/SubscriptionTriggerMatcher.java @@ -66,10 +66,18 @@ public class SubscriptionTriggerMatcher { } private InMemoryMatchResult match(SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent theQueryCriteria) { - InMemoryMatchResult previousMatches = InMemoryMatchResult.successfulMatch(); - InMemoryMatchResult currentMatches = InMemoryMatchResult.successfulMatch(); String previousCriteria = theQueryCriteria.getPrevious(); String currentCriteria = theQueryCriteria.getCurrent(); + InMemoryMatchResult previousMatches = InMemoryMatchResult.fromBoolean(previousCriteria == null); + InMemoryMatchResult currentMatches = InMemoryMatchResult.fromBoolean(currentCriteria == null); + + // WIP STR5 implement fhirPathCriteria per https://build.fhir.org/subscriptiontopic.html#fhirpath-criteria + if (currentCriteria != null) { + currentMatches = matchResource(myResource, currentCriteria); + } + if (myOperation == ResourceModifiedMessage.OperationTypeEnum.CREATE) { + return currentMatches; + } if (previousCriteria != null) { if (myOperation == ResourceModifiedMessage.OperationTypeEnum.UPDATE || @@ -85,10 +93,7 @@ public class SubscriptionTriggerMatcher { } } } - if (currentCriteria != null) { - currentMatches = matchResource(myResource, currentCriteria); - } - // WIP STR5 is this the correct interpretation of requireBoth? + // WIP STR5 implement resultForCreate and resultForDelete if (theQueryCriteria.getRequireBoth()) { return InMemoryMatchResult.and(previousMatches, currentMatches); } else { diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/matcher/matching/DaoSubscriptionMatcherTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/matcher/matching/DaoSubscriptionMatcherTest.java index e16eca02d8a..ac1b4835199 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/matcher/matching/DaoSubscriptionMatcherTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/matcher/matching/DaoSubscriptionMatcherTest.java @@ -13,6 +13,7 @@ import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory; import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig; import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -49,6 +50,8 @@ public class DaoSubscriptionMatcherTest { private IValidationSupport myValidationSupport; @MockBean private SubscriptionChannelFactory mySubscriptionChannelFactory; + @MockBean + private SubscriptionQueryValidator mySubscriptionQueryValidator; /** * Make sure that if we're only running the {@link SubscriptionSubmitterConfig}, we don't need diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/ActiveSubscriptionCacheTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/ActiveSubscriptionCacheTest.java index 33607879a2a..1210eab3e5f 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/ActiveSubscriptionCacheTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/match/registry/ActiveSubscriptionCacheTest.java @@ -113,22 +113,22 @@ public class ActiveSubscriptionCacheTest { ActiveSubscriptionCache activeSubscriptionCache = new ActiveSubscriptionCache(); ActiveSubscription activeSub1 = buildActiveSubscription(ID1); activeSubscriptionCache.put(ID1, activeSub1); - assertThat(activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL), hasSize(0)); + assertThat(activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL), hasSize(0)); ActiveSubscription activeSub2 = buildTopicSubscription(ID2, TEST_TOPIC_URL); activeSubscriptionCache.put(ID2, activeSub2); - assertThat(activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL), hasSize(1)); - ActiveSubscription match = activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL).get(0); + assertThat(activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL), hasSize(1)); + ActiveSubscription match = activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL).get(0); assertEquals(ID2, match.getId()); ActiveSubscription activeSub3 = buildTopicSubscription(ID3, TEST_TOPIC_URL_OTHER); activeSubscriptionCache.put(ID3, activeSub3); - assertThat(activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL), hasSize(1)); - match = activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL).get(0); + assertThat(activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL), hasSize(1)); + match = activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL).get(0); assertEquals(ID2, match.getId()); - assertThat(activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL_OTHER), hasSize(1)); - match = activeSubscriptionCache.getTopicSubscriptionsForUrl(TEST_TOPIC_URL_OTHER).get(0); + assertThat(activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL_OTHER), hasSize(1)); + match = activeSubscriptionCache.getTopicSubscriptionsForTopic(TEST_TOPIC_URL_OTHER).get(0); assertEquals(ID3, match.getId()); } @@ -136,7 +136,7 @@ public class ActiveSubscriptionCacheTest { private ActiveSubscription buildTopicSubscription(String theId, String theTopicUrl) { ActiveSubscription activeSub2 = buildActiveSubscription(theId); activeSub2.getSubscription().setTopicSubscription(true); - activeSub2.getSubscription().setCriteriaString(theTopicUrl); + activeSub2.getSubscription().getTopicSubscription().setTopic(theTopicUrl); return activeSub2; } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java index 12f19bc7d9a..10c9a092ea8 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/module/subscriber/SubscriptionMatchingSubscriberTest.java @@ -450,7 +450,7 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri when(myInterceptorBroadcaster.callHooks( eq(Pointcut.SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED), any(HookParams.class))).thenReturn(true); when(message.getPayloadId(null)).thenReturn(new IdDt("Patient", 123L)); - when(mySubscriptionRegistry.getAll()).thenReturn(Collections.singletonList(myActiveSubscription)); + when(mySubscriptionRegistry.getAllNonTopicSubscriptions()).thenReturn(Collections.singletonList(myActiveSubscription)); when(myActiveSubscription.getSubscription()).thenReturn(myCanonicalSubscription); when(myActiveSubscription.getCriteria()).thenReturn(mySubscriptionCriteria); when(myActiveSubscription.getId()).thenReturn("Patient/123"); @@ -468,7 +468,7 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri eq(Pointcut.SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED), any(HookParams.class))).thenReturn(true); when(message.getPayloadId(null)).thenReturn(new IdDt("Patient", 123L)); when(myNonDeleteCanonicalSubscription.getSendDeleteMessages()).thenReturn(false); - when(mySubscriptionRegistry.getAll()).thenReturn(List.of(myNonDeleteSubscription, myActiveSubscription)); + when(mySubscriptionRegistry.getAllNonTopicSubscriptions()).thenReturn(List.of(myNonDeleteSubscription, myActiveSubscription)); when(myActiveSubscription.getSubscription()).thenReturn(myCanonicalSubscription); when(myActiveSubscription.getCriteria()).thenReturn(mySubscriptionCriteria); when(myActiveSubscription.getId()).thenReturn("Patient/123"); @@ -489,7 +489,7 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri when(myInterceptorBroadcaster.callHooks( eq(Pointcut.SUBSCRIPTION_BEFORE_PERSISTED_RESOURCE_CHECKED), any(HookParams.class))).thenReturn(true); when(message.getPayloadId(null)).thenReturn(new IdDt("Patient", 123L)); - when(mySubscriptionRegistry.getAll()).thenReturn(Collections.singletonList(myActiveSubscription)); + when(mySubscriptionRegistry.getAllNonTopicSubscriptions()).thenReturn(Collections.singletonList(myActiveSubscription)); when(myActiveSubscription.getSubscription()).thenReturn(myCanonicalSubscription); when(myActiveSubscription.getCriteria()).thenReturn(mySubscriptionCriteria); when(myActiveSubscription.getId()).thenReturn("Patient/123"); diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptorTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptorTest.java index bcb5f18840c..ba45e885981 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptorTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionValidatingInterceptorTest.java @@ -202,7 +202,7 @@ public class SubscriptionValidatingInterceptorTest { mySubscriptionValidatingInterceptor.validateSubmittedSubscription(badSub, null, null, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED); fail(); } catch (UnprocessableEntityException e) { - assertThat(e.getMessage(), is(Msg.code(2322) + "No SubscriptionTopic exists with url: http://topic.url")); + assertThat(e.getMessage(), is(Msg.code(2322) + "No SubscriptionTopic exists with topic: http://topic.url")); } // Happy path @@ -228,6 +228,11 @@ public class SubscriptionValidatingInterceptorTest { SubscriptionCanonicalizer subscriptionCanonicalizer(FhirContext theFhirContext) { return new SubscriptionCanonicalizer(theFhirContext); } + + @Bean + SubscriptionQueryValidator subscriptionQueryValidator(DaoRegistry theDaoRegistry, SubscriptionStrategyEvaluator theSubscriptionStrategyEvaluator) { + return new SubscriptionQueryValidator(theDaoRegistry, theSubscriptionStrategyEvaluator); + } } @Nonnull diff --git a/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestR4BTest.java b/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestR4BTest.java index ce511e8fa5d..2537f195549 100644 --- a/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestR4BTest.java +++ b/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestR4BTest.java @@ -77,8 +77,7 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { public void testReuseSubscriptionIdWithDifferentDatabaseMode() throws Exception { myStorageSettings.setTagStorageMode(JpaStorageSettings.TagStorageModeEnum.NON_VERSIONED); - String payload = "application/fhir+json"; - IdType id = createSubscription("Observation?_has:DiagnosticReport:result:identifier=foo|IDENTIFIER", payload, null, "sub").getIdElement().toUnqualifiedVersionless(); + IdType id = createSubscription("Observation?_has:DiagnosticReport:result:identifier=foo|IDENTIFIER", Constants.CT_FHIR_JSON_NEW, null, "sub").getIdElement().toUnqualifiedVersionless(); waitForActivatedSubscriptionCount(1); Subscription subscription = mySubscriptionDao.read(id, mySrd); @@ -88,8 +87,7 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { mySubscriptionDao.delete(id, mySrd); waitForActivatedSubscriptionCount(0); - payload = "application/fhir+json"; - id = createSubscription("Observation?", payload, null, "sub").getIdElement().toUnqualifiedVersionless(); + id = createSubscription("Observation?", Constants.CT_FHIR_JSON_NEW, null, "sub").getIdElement().toUnqualifiedVersionless(); waitForActivatedSubscriptionCount(1); subscription = mySubscriptionDao.read(id, mySrd); @@ -104,8 +102,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=json"; String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=json"; - createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW); - createSubscription(criteria2, Constants.CT_FHIR_JSON_NEW); + createSubscription(criteria1); + createSubscription(criteria2); waitForActivatedSubscriptionCount(2); sendObservation(code, "SNOMED-CT"); @@ -120,12 +118,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testUpdatesHaveCorrectMetadata() throws Exception { - String payload = "application/fhir+json"; String code = "1000000050"; String criteria1 = "Observation?"; - createSubscription(criteria1, payload); + createSubscription(criteria1); waitForActivatedSubscriptionCount(1); /* @@ -171,11 +168,9 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testPlaceholderReferencesInTransactionAreResolvedCorrectly() throws Exception { - - String payload = "application/fhir+json"; String code = "1000000050"; String criteria1 = "Observation?"; - createSubscription(criteria1, payload); + createSubscription(criteria1); waitForActivatedSubscriptionCount(1); // Create a transaction that should match @@ -204,12 +199,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testUpdatesHaveCorrectMetadataUsingTransactions() throws Exception { - String payload = "application/fhir+json"; - String code = "1000000050"; String criteria1 = "Observation?"; - createSubscription(criteria1, payload); + createSubscription(criteria1); waitForActivatedSubscriptionCount(1); /* @@ -265,12 +258,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testRepeatedDeliveries() throws Exception { - String payload = "application/fhir+json"; - String code = "1000000050"; String criteria1 = "Observation?"; - createSubscription(criteria1, payload); + createSubscription(criteria1); waitForActivatedSubscriptionCount(1); for (int i = 0; i < 100; i++) { @@ -287,12 +278,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testSubscriptionRegistryLoadsSubscriptionsFromDatabase() throws Exception { - String payload = "application/fhir+json"; String code = "1000000050"; String criteria1 = "Observation?"; - createSubscription(criteria1, payload); + createSubscription(criteria1); waitForActivatedSubscriptionCount(1); // Manually unregister all subscriptions @@ -315,8 +305,7 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testActiveSubscriptionShouldntReActivate() throws Exception { String criteria = "Observation?code=111111111&_format=xml"; - String payload = "application/fhir+json"; - createSubscription(criteria, payload); + createSubscription(criteria); waitForActivatedSubscriptionCount(1); for (int i = 0; i < 5; i++) { @@ -327,14 +316,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testRestHookSubscriptionMetaAddDoesntTriggerNewDelivery() throws Exception { - String payload = "application/fhir+json"; - String code = "1000000050"; String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - createSubscription(criteria1, payload); - createSubscription(criteria2, payload); + createSubscription(criteria1); + createSubscription(criteria2); waitForActivatedSubscriptionCount(2); ourLog.info("Sending an Observation"); @@ -381,14 +368,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { public void testRestHookSubscriptionMetaAddDoesTriggerNewDeliveryIfConfiguredToDoSo() throws Exception { myStorageSettings.setTriggerSubscriptionsForNonVersioningChanges(true); - String payload = "application/fhir+json"; - String code = "1000000050"; String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - createSubscription(criteria1, payload); - createSubscription(criteria2, payload); + createSubscription(criteria1); + createSubscription(criteria2); waitForActivatedSubscriptionCount(2); Observation obs = sendObservation(code, "SNOMED-CT"); @@ -431,14 +416,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testRestHookSubscriptionNoopUpdateDoesntTriggerNewDelivery() throws Exception { - String payload = "application/fhir+json"; - String code = "1000000050"; String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - createSubscription(criteria1, payload); - createSubscription(criteria2, payload); + createSubscription(criteria1); + createSubscription(criteria2); waitForActivatedSubscriptionCount(2); Observation obs = sendObservation(code, "SNOMED-CT"); @@ -464,13 +447,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testRestHookSubscriptionApplicationJsonDisableVersionIdInDelivery() throws Exception { - String payload = "application/json"; - String code = "1000000050"; String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; waitForActivatedSubscriptionCount(0); - Subscription subscription1 = createSubscription(criteria1, payload); + Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW); waitForActivatedSubscriptionCount(1); @@ -512,13 +493,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testRestHookSubscriptionDoesntGetLatestVersionByDefault() throws Exception { - String payload = "application/json"; - String code = "1000000050"; String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; waitForActivatedSubscriptionCount(0); - createSubscription(criteria1, payload); + createSubscription(criteria1); waitForActivatedSubscriptionCount(1); myStoppableSubscriptionDeliveringRestHookSubscriber.pause(); @@ -553,18 +532,20 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { assertEquals("changed", observation2.getNoteFirstRep().getText()); } + private void createSubscription(String criteria1) { + createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW); + } + @ParameterizedTest @ValueSource(strings = {"[*]", "[Observation]", "Observation?"}) public void RestHookSubscriptionWithPayloadSendsDeleteRequest(String theCriteria) throws Exception { - String payload = "application/json"; - Extension sendDeleteMessagesExtension = new Extension() .setUrl(EX_SEND_DELETE_MESSAGES) .setValue(new BooleanType(true)); waitForActivatedSubscriptionCount(0); - createSubscription(theCriteria, payload, sendDeleteMessagesExtension); + createSubscription(theCriteria, Constants.CT_FHIR_JSON_NEW, sendDeleteMessagesExtension); waitForActivatedSubscriptionCount(1); ourLog.info("** About to send observation"); @@ -580,14 +561,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testRestHookSubscriptionGetsLatestVersionWithFlag() throws Exception { - String payload = "application/json"; - String code = "1000000050"; String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; waitForActivatedSubscriptionCount(0); - Subscription subscription = newSubscription(criteria1, payload); + Subscription subscription = newSubscription(criteria1, Constants.CT_FHIR_JSON_NEW); subscription .getChannel() .addExtension(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION, new BooleanType("true")); @@ -629,14 +608,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testRestHookSubscriptionApplicationJson() throws Exception { - String payload = "application/json"; - String code = "1000000050"; String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - Subscription subscription1 = createSubscription(criteria1, payload); - Subscription subscription2 = createSubscription(criteria2, payload); + Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW); + Subscription subscription2 = createSubscription(criteria2, Constants.CT_FHIR_JSON_NEW); waitForActivatedSubscriptionCount(2); Observation observation1 = sendObservation(code, "SNOMED-CT"); @@ -709,14 +686,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { public void testRestHookSubscriptionApplicationJsonDatabase() throws Exception { // Same test as above, but now run it using database matching myStorageSettings.setEnableInMemorySubscriptionMatching(false); - String payload = "application/json"; - String code = "1000000050"; String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - Subscription subscription1 = createSubscription(criteria1, payload); - Subscription subscription2 = createSubscription(criteria2, payload); + Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW); + Subscription subscription2 = createSubscription(criteria2, Constants.CT_FHIR_JSON_NEW); waitForActivatedSubscriptionCount(2); Observation observation1 = sendObservation(code, "SNOMED-CT"); @@ -787,14 +762,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testRestHookSubscriptionApplicationXml() throws Exception { - String payload = "application/xml"; - String code = "1000000050"; String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - Subscription subscription1 = createSubscription(criteria1, payload); - Subscription subscription2 = createSubscription(criteria2, payload); + Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_XML_NEW); + Subscription subscription2 = createSubscription(criteria2, Constants.CT_FHIR_XML_NEW); waitForActivatedSubscriptionCount(2); ourLog.info("** About to send observation"); @@ -861,12 +834,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testRestHookSubscriptionStarCriteria() throws Exception { - String payload = "application/json"; - String code = "1000000050"; String criteria1 = "[*]"; - createSubscription(criteria1, payload); + createSubscription(criteria1); waitForActivatedSubscriptionCount(1); sendObservation(code, "SNOMED-CT"); @@ -887,12 +858,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testRestHookSubscriptionMultiTypeCriteria() throws Exception { - String payload = "application/json"; - String code = "1000000050"; String criteria1 = "[Observation,Patient]"; - createSubscription(criteria1, payload); + createSubscription(criteria1); waitForActivatedSubscriptionCount(1); sendOrganization(); @@ -915,12 +884,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testSubscriptionTriggerViaSubscription() throws Exception { - String payload = "application/xml"; - String code = "1000000050"; String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - createSubscription(criteria1, payload); + createSubscription(criteria1, Constants.CT_FHIR_XML_NEW); waitForActivatedSubscriptionCount(1); ourLog.info("** About to send observation"); @@ -966,14 +933,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testUpdateSubscriptionToMatchLater() throws Exception { - String payload = "application/xml"; - String code = "1000000050"; String criteriaBad = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; ourLog.info("** About to create non-matching subscription"); - Subscription subscription2 = createSubscription(criteriaBad, payload); + Subscription subscription2 = createSubscription(criteriaBad, Constants.CT_FHIR_XML_NEW); ourLog.info("** About to send observation that wont match"); @@ -1011,14 +976,12 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testRestHookSubscriptionApplicationXmlJson() throws Exception { - String payload = "application/fhir+xml"; - String code = "1000000050"; String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - Subscription subscription1 = createSubscription(criteria1, payload); - Subscription subscription2 = createSubscription(criteria2, payload); + Subscription subscription1 = createSubscription(criteria1, Constants.CT_FHIR_XML_NEW); + Subscription subscription2 = createSubscription(criteria2, Constants.CT_FHIR_XML_NEW); waitForActivatedSubscriptionCount(2); Observation observation1 = sendObservation(code, "SNOMED-CT"); @@ -1032,12 +995,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testRestHookSubscriptionInvalidCriteria() throws Exception { - String payload = "application/xml"; - String criteria1 = "Observation?codeeeee=SNOMED-CT"; try { - createSubscription(criteria1, payload); + createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW); fail(); } catch (UnprocessableEntityException e) { assertEquals("HTTP 422 Unprocessable Entity: " + Msg.code(9) + "Invalid subscription criteria submitted: Observation?codeeeee=SNOMED-CT " + Msg.code(488) + "Failed to parse match URL[Observation?codeeeee=SNOMED-CT] - Resource type Observation does not have a parameter with name: codeeeee", e.getMessage()); @@ -1046,13 +1007,11 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testSubscriptionWithHeaders() throws Exception { - String payload = "application/fhir+json"; - String code = "1000000050"; String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; // Add some headers, and we'll also turn back to requested status for fun - Subscription subscription = createSubscription(criteria1, payload); + Subscription subscription = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW); waitForActivatedSubscriptionCount(1); subscription.getChannel().addHeader("X-Foo: FOO"); @@ -1074,12 +1033,10 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testDisableSubscription() throws Exception { - String payload = "application/fhir+json"; - String code = "1000000050"; String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - Subscription subscription = createSubscription(criteria1, payload); + Subscription subscription = createSubscription(criteria1, Constants.CT_FHIR_JSON_NEW); waitForActivatedSubscriptionCount(1); sendObservation(code, "SNOMED-CT"); @@ -1107,9 +1064,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testInvalidProvenanceParam() { assertThrows(UnprocessableEntityException.class, () -> { - String payload = "application/fhir+json"; String criteriabad = "Provenance?activity=http://hl7.org/fhir/v3/DocumentCompletion%7CAU"; - Subscription subscription = newSubscription(criteriabad, payload); + Subscription subscription = newSubscription(criteriabad, Constants.CT_FHIR_JSON_NEW); myClient.create().resource(subscription).execute(); }); } @@ -1117,9 +1073,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testInvalidProcedureRequestParam() { assertThrows(UnprocessableEntityException.class, () -> { - String payload = "application/fhir+json"; String criteriabad = "ProcedureRequest?intent=instance-order&category=Laboratory"; - Subscription subscription = newSubscription(criteriabad, payload); + Subscription subscription = newSubscription(criteriabad, Constants.CT_FHIR_JSON_NEW); myClient.create().resource(subscription).execute(); }); } @@ -1127,9 +1082,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testInvalidBodySiteParam() { assertThrows(UnprocessableEntityException.class, () -> { - String payload = "application/fhir+json"; String criteriabad = "BodySite?accessType=Catheter"; - Subscription subscription = newSubscription(criteriabad, payload); + Subscription subscription = newSubscription(criteriabad, Constants.CT_FHIR_JSON_NEW); myClient.create().resource(subscription).execute(); }); } @@ -1137,9 +1091,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testGoodSubscriptionPersists() { assertEquals(0, subscriptionCount()); - String payload = "application/fhir+json"; String criteriaGood = "Patient?gender=male"; - Subscription subscription = newSubscription(criteriaGood, payload); + Subscription subscription = newSubscription(criteriaGood, Constants.CT_FHIR_JSON_NEW); myClient.create().resource(subscription).execute(); await().until(() -> subscriptionCount() == 1); } @@ -1188,9 +1141,8 @@ public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { @Test public void testBadSubscriptionDoesntPersist() { assertEquals(0, subscriptionCount()); - String payload = "application/fhir+json"; String criteriaBad = "BodySite?accessType=Catheter"; - Subscription subscription = newSubscription(criteriaBad, payload); + Subscription subscription = newSubscription(criteriaBad, Constants.CT_FHIR_JSON_NEW); try { myClient.create().resource(subscription).execute(); } catch (UnprocessableEntityException e) { diff --git a/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTopicR4BTest.java b/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTopicR4BTest.java index ea3c0f7f6de..1b4b92a2036 100644 --- a/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTopicR4BTest.java +++ b/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTopicR4BTest.java @@ -175,7 +175,7 @@ public class SubscriptionTopicR4BTest extends BaseSubscriptionsR4BTest { return mySubscriptionTopicRegistry.size() == theTarget; } - private SubscriptionTopic createEncounterSubscriptionTopic(Encounter.EncounterStatus theFrom, Encounter.EncounterStatus theCurrent, SubscriptionTopic.InteractionTrigger... theInteractionTriggers) { + private SubscriptionTopic createEncounterSubscriptionTopic(Encounter.EncounterStatus theFrom, Encounter.EncounterStatus theCurrent, SubscriptionTopic.InteractionTrigger... theInteractionTriggers) throws InterruptedException { SubscriptionTopic retval = new SubscriptionTopic(); retval.setUrl(SUBSCRIPTION_TOPIC_TEST_URL); retval.setStatus(Enumerations.PublicationStatus.ACTIVE); @@ -188,7 +188,9 @@ public class SubscriptionTopicR4BTest extends BaseSubscriptionsR4BTest { queryCriteria.setPrevious("Encounter?status=" + theFrom.toCode()); queryCriteria.setCurrent("Encounter?status=" + theCurrent.toCode()); queryCriteria.setRequireBoth(true); + mySubscriptionTopicsCheckedLatch.setExpectedCount(1); mySubscriptionTopicDao.create(retval, mySrd); + mySubscriptionTopicsCheckedLatch.awaitExpected(); return retval; } diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR5Test.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR5Test.java index bcceda76b8e..932cf9442af 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR5Test.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR5Test.java @@ -1,22 +1,27 @@ package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.interceptor.api.IInterceptorService; +import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.provider.r5.BaseResourceProviderR5Test; import ca.uhn.fhir.jpa.subscription.channel.impl.LinkedBlockingChannel; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType; +import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; import ca.uhn.fhir.jpa.test.util.SubscriptionTestUtil; -import ca.uhn.fhir.rest.annotation.Create; -import ca.uhn.fhir.rest.annotation.ResourceParam; -import ca.uhn.fhir.rest.annotation.Update; +import ca.uhn.fhir.jpa.topic.SubscriptionTopicLoader; +import ca.uhn.fhir.jpa.topic.SubscriptionTopicRegistry; +import ca.uhn.fhir.rest.annotation.Transaction; +import ca.uhn.fhir.rest.annotation.TransactionParam; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.test.utilities.JettyUtil; import ca.uhn.fhir.util.BundleUtil; -import com.google.common.collect.Lists; +import ca.uhn.test.concurrency.PointcutLatch; import net.ttddyy.dsproxy.QueryCount; import net.ttddyy.dsproxy.listener.SingleQueryCountHolder; import org.eclipse.jetty.server.Server; @@ -25,12 +30,10 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r5.model.Bundle; -import org.hl7.fhir.r5.model.CodeableConcept; -import org.hl7.fhir.r5.model.Coding; import org.hl7.fhir.r5.model.Enumerations; -import org.hl7.fhir.r5.model.IdType; import org.hl7.fhir.r5.model.Observation; 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.AfterAll; import org.junit.jupiter.api.AfterEach; @@ -46,15 +49,20 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; @Disabled("abstract") public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseSubscriptionsR5Test.class); + public static final String SUBSCRIPTION_TOPIC_TEST_URL = "http://example.com/topic/test"; + + protected static int ourListenerPort; - protected static List ourContentTypes = Collections.synchronizedList(new ArrayList<>()); - protected static List ourHeaders = Collections.synchronizedList(new ArrayList<>()); - protected static List ourCreatedObservations = Collections.synchronizedList(Lists.newArrayList()); - protected static List ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList()); private static Server ourListenerServer; private static SingleQueryCountHolder ourCountHolder; private static String ourListenerServerBase; @@ -67,37 +75,28 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test protected List mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); @Autowired private SingleQueryCountHolder myCountHolder; + @Autowired + protected SubscriptionTopicRegistry mySubscriptionTopicRegistry; + @Autowired + protected SubscriptionTopicLoader mySubscriptionTopicLoader; + @Autowired + private IInterceptorService myInterceptorService; + private static final SubscriptionTopicR5Test.TestSystemProvider ourTestSystemProvider = new SubscriptionTopicR5Test.TestSystemProvider(); + protected IFhirResourceDao mySubscriptionTopicDao; + protected final PointcutLatch mySubscriptionTopicsCheckedLatch = new PointcutLatch(Pointcut.SUBSCRIPTION_TOPIC_AFTER_PERSISTED_RESOURCE_CHECKED); + protected final PointcutLatch mySubscriptionDeliveredLatch = new PointcutLatch(Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY); - @AfterEach - public void afterUnregisterRestHookListener() { - for (IIdType next : mySubscriptionIds) { - IIdType nextId = next.toUnqualifiedVersionless(); - ourLog.info("Deleting: {}", nextId); - myClient.delete().resourceById(nextId).execute(); - } - mySubscriptionIds.clear(); - - myStorageSettings.setAllowMultipleDelete(true); - ourLog.info("Deleting all subscriptions"); - myClient.delete().resourceConditionalByUrl("Subscription?status=active").execute(); - myClient.delete().resourceConditionalByUrl("Observation?code:missing=false").execute(); - ourLog.info("Done deleting all subscriptions"); - myStorageSettings.setAllowMultipleDelete(new JpaStorageSettings().isAllowMultipleDelete()); - - mySubscriptionTestUtil.unregisterSubscriptionInterceptor(); - } + @Override @BeforeEach - public void beforeRegisterRestHookListener() { + protected void before() throws Exception { + super.before(); + mySubscriptionTopicDao = myDaoRegistry.getResourceDao(SubscriptionTopic.class); mySubscriptionTestUtil.registerRestHookInterceptor(); - } + ourListenerRestServer.unregisterProvider(mySystemProvider); + ourListenerRestServer.registerProvider(ourTestSystemProvider); - @BeforeEach - public void beforeReset() throws Exception { - ourCreatedObservations.clear(); - ourUpdatedObservations.clear(); - ourContentTypes.clear(); - ourHeaders.clear(); + ourTestSystemProvider.clear(); // Delete all Subscriptions if (myClient != null) { @@ -116,34 +115,77 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test if (processingChannel != null) { processingChannel.addInterceptor(myCountingInterceptor); } + myInterceptorService.registerAnonymousInterceptor(Pointcut.SUBSCRIPTION_TOPIC_AFTER_PERSISTED_RESOURCE_CHECKED, mySubscriptionTopicsCheckedLatch); + myInterceptorService.registerAnonymousInterceptor(Pointcut.SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY, mySubscriptionDeliveredLatch); + } + @AfterEach + public void afterUnregisterRestHookListener() { + myInterceptorService.unregisterAllAnonymousInterceptors(); + for (IIdType next : mySubscriptionIds) { + IIdType nextId = next.toUnqualifiedVersionless(); + ourLog.info("Deleting: {}", nextId); + myClient.delete().resourceById(nextId).execute(); + } + mySubscriptionIds.clear(); - protected Subscription createSubscription(String theCriteria, String thePayload) { - Subscription subscription = newSubscription(theCriteria, thePayload); + myStorageSettings.setAllowMultipleDelete(true); + ourLog.info("Deleting all subscriptions"); + myClient.delete().resourceConditionalByUrl("Subscription?status=active").execute(); + myClient.delete().resourceConditionalByUrl("Observation?code:missing=false").execute(); + ourLog.info("Done deleting all subscriptions"); + myStorageSettings.setAllowMultipleDelete(new JpaStorageSettings().isAllowMultipleDelete()); - return postSubscription(subscription); + mySubscriptionTestUtil.unregisterSubscriptionInterceptor(); + ourListenerRestServer.unregisterProvider(ourTestSystemProvider); + ourListenerRestServer.registerProvider(mySystemProvider); + mySubscriptionTopicsCheckedLatch.clear(); + mySubscriptionDeliveredLatch.clear(); + } + + protected int getSystemProviderCount() { + return ourTestSystemProvider.getCount(); + } + + protected List getLastSystemProviderHeaders() { + return ourTestSystemProvider.getLastHeaders(); + } + + protected Bundle getLastSystemProviderBundle() { + return ourTestSystemProvider.getLastBundle(); + } + + protected String getLastSystemProviderContentType() { + return ourTestSystemProvider.getLastContentType(); + } + + protected Set getReceivedObservations() { + return ourTestSystemProvider.receivedBundles.stream() + .flatMap(t -> t.getEntry().stream()) + .filter(t -> t.getResource() instanceof Observation) + .map(t -> (Observation) t.getResource()) + .collect(Collectors.toSet()); } @Nonnull - protected Subscription postSubscription(Subscription subscription) { + protected Subscription postSubscription(Subscription subscription) throws InterruptedException { + mySubscriptionTopicsCheckedLatch.setExpectedCount(1); MethodOutcome methodOutcome = myClient.create().resource(subscription).execute(); + mySubscriptionTopicsCheckedLatch.awaitExpected(); + subscription.setId(methodOutcome.getId().toVersionless()); mySubscriptionIds.add(methodOutcome.getId()); return subscription; } - protected Subscription newSubscription(String theCriteria, String thePayload) { - SubscriptionTopic topic = new SubscriptionTopic(); - topic.getResourceTriggerFirstRep().getQueryCriteria().setCurrent(theCriteria); - topic.setId("1"); + protected Subscription newTopicSubscription(String theTopicUrl, String thePayload) { Subscription subscription = new Subscription(); - subscription.getContained().add(topic); - subscription.setTopic("#1"); + subscription.setTopic(theTopicUrl); subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); - subscription.setStatus(Enumerations.SubscriptionStatusCodes.REQUESTED); + subscription.setStatus(Enumerations.SubscriptionStatusCodes.ACTIVE); subscription.getChannelType() .setSystem(CanonicalSubscriptionChannelType.RESTHOOK.getSystem()) @@ -153,72 +195,56 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test return subscription; } - - protected void waitForQueueToDrain() throws InterruptedException { - mySubscriptionTestUtil.waitForQueueToDrain(); - } - @PostConstruct public void initializeOurCountHolder() { ourCountHolder = myCountHolder; } - - protected Observation sendObservation(String code, String system) { - Observation observation = new Observation(); - CodeableConcept codeableConcept = new CodeableConcept(); - observation.setCode(codeableConcept); - observation.getIdentifierFirstRep().setSystem("foo").setValue("1"); - Coding coding = codeableConcept.addCoding(); - coding.setCode(code); - coding.setSystem(system); - - observation.setStatus(Enumerations.ObservationStatus.FINAL); - - IIdType id = myObservationDao.create(observation).getId(); - observation.setId(id); - - return observation; + // WIP STR5 consolidate with lambda + protected IIdType createResource(IBaseResource theResource, boolean theExpectDelivery) throws InterruptedException { + IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass()); + if (theExpectDelivery) { + mySubscriptionDeliveredLatch.setExpectedCount(1); + } + mySubscriptionTopicsCheckedLatch.setExpectedCount(1); + IIdType id = dao.create(theResource, mySrd).getId(); + mySubscriptionTopicsCheckedLatch.awaitExpected(); + if (theExpectDelivery) { + mySubscriptionDeliveredLatch.awaitExpected(); + } + return id; } - - public static class ObservationListener implements IResourceProvider { - - @Create - public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { - ourLog.info("Received Listener Create"); - ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", "")); - ourCreatedObservations.add(theObservation); - extractHeaders(theRequest); - return new MethodOutcome(new IdType("Observation/1"), true); + protected DaoMethodOutcome updateResource(IBaseResource theResource, boolean theExpectDelivery) throws InterruptedException { + IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass()); + if (theExpectDelivery) { + mySubscriptionDeliveredLatch.setExpectedCount(1); } + mySubscriptionTopicsCheckedLatch.setExpectedCount(1); + DaoMethodOutcome retval = dao.update(theResource, mySrd); - private void extractHeaders(HttpServletRequest theRequest) { - Enumeration headerNamesEnum = theRequest.getHeaderNames(); - while (headerNamesEnum.hasMoreElements()) { - String nextName = headerNamesEnum.nextElement(); - Enumeration valueEnum = theRequest.getHeaders(nextName); - while (valueEnum.hasMoreElements()) { - String nextValue = valueEnum.nextElement(); - ourHeaders.add(nextName + ": " + nextValue); - } - } + mySubscriptionTopicsCheckedLatch.awaitExpected(); + ResourceModifiedMessage lastMessage = mySubscriptionTopicsCheckedLatch.getLatchInvocationParameterOfType(ResourceModifiedMessage.class); + assertEquals(theResource.getIdElement().toVersionless().toString(), lastMessage.getPayloadId()); + + if (theExpectDelivery) { + mySubscriptionDeliveredLatch.awaitExpected(); } + return retval; + } - @Override - public Class getResourceType() { - return Observation.class; + protected Bundle sendTransaction(Bundle theBundle, boolean theExpectDelivery) throws InterruptedException { + int expectedChecks = theBundle.getEntry().size(); + if (theExpectDelivery) { + mySubscriptionDeliveredLatch.setExpectedCount(1); } - - @Update - public MethodOutcome update(@ResourceParam Observation theObservation, HttpServletRequest theRequest) { - ourLog.info("Received Listener Update"); - ourUpdatedObservations.add(theObservation); - ourContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", "")); - extractHeaders(theRequest); - return new MethodOutcome(new IdType("Observation/1"), false); + mySubscriptionTopicsCheckedLatch.setExpectedCount(expectedChecks); + Bundle retval = mySystemDao.transaction(mySrd, theBundle); + mySubscriptionTopicsCheckedLatch.awaitExpected(); + if (theExpectDelivery) { + mySubscriptionDeliveredLatch.awaitExpected(); } - + return retval; } @AfterAll @@ -230,13 +256,45 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test return ourCountHolder.getQueryCountMap().get(""); } + + protected void waitForRegisteredSubscriptionTopicCount(int theTarget) { + await().until(() -> subscriptionTopicRegistryHasSize(theTarget)); + } + + private boolean subscriptionTopicRegistryHasSize(int theTarget) { + int size = mySubscriptionTopicRegistry.size(); + if (size == theTarget) { + return true; + } + mySubscriptionTopicLoader.doSyncResourcessForUnitTest(); + return mySubscriptionTopicRegistry.size() == theTarget; + } + + protected SubscriptionTopic createSubscriptionTopic(SubscriptionTopic theSubscriptionTopic) throws InterruptedException { + mySubscriptionTopicsCheckedLatch.setExpectedCount(1); + SubscriptionTopic retval = (SubscriptionTopic) myClient.create().resource(theSubscriptionTopic).execute().getResource(); + mySubscriptionTopicsCheckedLatch.awaitExpected(); + return retval; + } + + protected static void validateSubscriptionStatus(Subscription subscription, IBaseResource sentResource, SubscriptionStatus ss) { + assertEquals(Enumerations.SubscriptionStatusCodes.ACTIVE, ss.getStatus()); + assertEquals(SubscriptionStatus.SubscriptionNotificationType.EVENTNOTIFICATION, ss.getType()); + assertEquals("1", ss.getEventsSinceSubscriptionStartElement().getValueAsString()); + + List notificationEvents = ss.getNotificationEvent(); + assertEquals(1, notificationEvents.size()); + SubscriptionStatus.SubscriptionStatusNotificationEventComponent notificationEvent = notificationEvents.get(0); + assertEquals(1, notificationEvent.getEventNumber()); + assertEquals(sentResource.getIdElement().toUnqualifiedVersionless(), notificationEvent.getFocus().getReferenceElement()); + + assertEquals(subscription.getIdElement().toUnqualifiedVersionless(), ss.getSubscription().getReferenceElement()); + assertEquals(subscription.getTopic(), ss.getTopic()); + } + @BeforeAll public static void startListenerServer() throws Exception { ourListenerRestServer = new RestfulServer(FhirContext.forR5Cached()); - - ObservationListener obsListener = new ObservationListener(); - ourListenerRestServer.setResourceProviders(obsListener); - ourListenerServer = new Server(0); ServletContextHandler proxyHandler = new ServletContextHandler(); @@ -257,4 +315,56 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test JettyUtil.closeServer(ourListenerServer); } + + static class TestSystemProvider { + AtomicInteger count = new AtomicInteger(0); + final List receivedBundles = new ArrayList<>(); + final List receivedContentTypes = new ArrayList<>(); + final List myHeaders = new ArrayList<>(); + + @Transaction + public Bundle transaction(@TransactionParam Bundle theBundle, HttpServletRequest theRequest) { + ourLog.info("Received Transaction with {} entries", theBundle.getEntry().size()); + count.incrementAndGet(); + receivedContentTypes.add(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE).replaceAll(";.*", "")); + receivedBundles.add(theBundle); + extractHeaders(theRequest); + return theBundle; + } + + private void extractHeaders(HttpServletRequest theRequest) { + Enumeration headerNamesEnum = theRequest.getHeaderNames(); + while (headerNamesEnum.hasMoreElements()) { + String nextName = headerNamesEnum.nextElement(); + Enumeration valueEnum = theRequest.getHeaders(nextName); + while (valueEnum.hasMoreElements()) { + String nextValue = valueEnum.nextElement(); + myHeaders.add(nextName + ": " + nextValue); + } + } + } + + int getCount() { + return count.get(); + } + + public String getLastContentType() { + return receivedContentTypes.get(receivedContentTypes.size() - 1); + } + + public Bundle getLastBundle() { + return receivedBundles.get(receivedBundles.size() - 1); + } + + public List getLastHeaders() { + return Collections.unmodifiableList(myHeaders); + } + + public void clear() { + count.set(0); + receivedBundles.clear(); + receivedContentTypes.clear(); + myHeaders.clear(); + } + } } diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTopicR5Test.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTopicR5Test.java index 20918953cfd..d9a5f934276 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTopicR5Test.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/SubscriptionTopicR5Test.java @@ -1,11 +1,5 @@ package ca.uhn.fhir.jpa.subscription; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionConstants; -import ca.uhn.fhir.jpa.topic.SubscriptionTopicLoader; -import ca.uhn.fhir.jpa.topic.SubscriptionTopicRegistry; -import ca.uhn.fhir.rest.annotation.Transaction; -import ca.uhn.fhir.rest.annotation.TransactionParam; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.util.BundleUtil; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -16,15 +10,11 @@ import org.hl7.fhir.r5.model.Enumerations; 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.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -32,31 +22,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; public class SubscriptionTopicR5Test extends BaseSubscriptionsR5Test { private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionTopicR5Test.class); - public static final String SUBSCRIPTION_TOPIC_TEST_URL = "http://example.com/topic/test"; - - @Autowired - protected SubscriptionTopicRegistry mySubscriptionTopicRegistry; - @Autowired - protected SubscriptionTopicLoader mySubscriptionTopicLoader; - protected IFhirResourceDao mySubscriptionTopicDao; - private static final TestSystemProvider ourTestSystemProvider = new TestSystemProvider(); - - @Override - @BeforeEach - protected void before() throws Exception { - super.before(); - ourListenerRestServer.unregisterProvider(mySystemProvider); - ourListenerRestServer.registerProvider(ourTestSystemProvider); - mySubscriptionTopicDao = myDaoRegistry.getResourceDao(SubscriptionTopic.class); - } - - @Override - @AfterEach - public void after() throws Exception { - ourListenerRestServer.unregisterProvider(ourTestSystemProvider); - ourListenerRestServer.registerProvider(mySystemProvider); - super.after(); - } @Test public void testSubscriptionTopicRegistryBean() { @@ -72,15 +37,12 @@ public class SubscriptionTopicR5Test extends BaseSubscriptionsR5Test { Subscription subscription = createTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL); waitForActivatedSubscriptionCount(1); - assertEquals(0, ourTestSystemProvider.getCount()); - Encounter sentEncounter = sendEncounterWithStatus(Encounter.EncounterStatus.COMPLETED); + assertEquals(0, getSystemProviderCount()); + Encounter sentEncounter = sendEncounterWithStatus(Encounter.EncounterStatus.COMPLETED, false); - // Should see 1 subscription notification - waitForQueueToDrain(); + await().until(() -> getSystemProviderCount() > 0); - await().until(() -> ourTestSystemProvider.getCount() > 0); - - Bundle receivedBundle = ourTestSystemProvider.getLastInput(); + Bundle receivedBundle = getLastSystemProviderBundle(); List resources = BundleUtil.toListOfResources(myFhirCtx, receivedBundle); assertEquals(2, resources.size()); @@ -92,41 +54,14 @@ public class SubscriptionTopicR5Test extends BaseSubscriptionsR5Test { assertEquals(sentEncounter.getIdElement(), encounter.getIdElement()); } - private static void validateSubscriptionStatus(Subscription subscription, Encounter sentEncounter, SubscriptionStatus ss) { - assertEquals(Enumerations.SubscriptionStatusCodes.ACTIVE, ss.getStatus()); - assertEquals(SubscriptionStatus.SubscriptionNotificationType.EVENTNOTIFICATION, ss.getType()); - assertEquals("1", ss.getEventsSinceSubscriptionStartElement().getValueAsString()); - List notificationEvents = ss.getNotificationEvent(); - assertEquals(1, notificationEvents.size()); - SubscriptionStatus.SubscriptionStatusNotificationEventComponent notificationEvent = notificationEvents.get(0); - assertEquals(1, notificationEvent.getEventNumber()); - assertEquals(sentEncounter.getIdElement().toUnqualifiedVersionless(), notificationEvent.getFocus().getReferenceElement()); - assertEquals(subscription.getIdElement().toUnqualifiedVersionless(), ss.getSubscription().getReferenceElement()); - assertEquals(SUBSCRIPTION_TOPIC_TEST_URL, ss.getTopic()); - } - - private Subscription createTopicSubscription(String theTopicUrl) { - Subscription subscription = newSubscription(theTopicUrl, Constants.CT_FHIR_JSON_NEW); - subscription.getMeta().addProfile(SubscriptionConstants.SUBSCRIPTION_TOPIC_PROFILE_URL); + private Subscription createTopicSubscription(String theTopicUrl) throws InterruptedException { + Subscription subscription = newTopicSubscription(theTopicUrl, Constants.CT_FHIR_JSON_NEW); return postSubscription(subscription); } - private void waitForRegisteredSubscriptionTopicCount(int theTarget) throws Exception { - await().until(() -> subscriptionTopicRegistryHasSize(theTarget)); - } - - private boolean subscriptionTopicRegistryHasSize(int theTarget) { - int size = mySubscriptionTopicRegistry.size(); - if (size == theTarget) { - return true; - } - mySubscriptionTopicLoader.doSyncResourcessForUnitTest(); - return mySubscriptionTopicRegistry.size() == theTarget; - } - - private SubscriptionTopic createEncounterSubscriptionTopic(Encounter.EncounterStatus theFrom, Encounter.EncounterStatus theCurrent, SubscriptionTopic.InteractionTrigger... theInteractionTriggers) { + private SubscriptionTopic createEncounterSubscriptionTopic(Encounter.EncounterStatus theFrom, Encounter.EncounterStatus theCurrent, SubscriptionTopic.InteractionTrigger... theInteractionTriggers) throws InterruptedException { SubscriptionTopic retval = new SubscriptionTopic(); retval.setUrl(SUBSCRIPTION_TOPIC_TEST_URL); retval.setStatus(Enumerations.PublicationStatus.ACTIVE); @@ -139,37 +74,18 @@ public class SubscriptionTopicR5Test extends BaseSubscriptionsR5Test { queryCriteria.setPrevious("Encounter?status=" + theFrom.toCode()); queryCriteria.setCurrent("Encounter?status=" + theCurrent.toCode()); queryCriteria.setRequireBoth(true); - queryCriteria.setRequireBoth(true); - mySubscriptionTopicDao.create(retval, mySrd); + super.createResource(retval, false); return retval; } - private Encounter sendEncounterWithStatus(Encounter.EncounterStatus theStatus) { + private Encounter sendEncounterWithStatus(Encounter.EncounterStatus theStatus, boolean theExpectDelivery) throws InterruptedException { Encounter encounter = new Encounter(); encounter.setStatus(theStatus); - IIdType id = myEncounterDao.create(encounter, mySrd).getId(); + IIdType id = createResource(encounter, theExpectDelivery); encounter.setId(id); return encounter; } - static class TestSystemProvider { - AtomicInteger myCount = new AtomicInteger(0); - Bundle myLastInput; - @Transaction - public Bundle transaction(@TransactionParam Bundle theInput) { - myCount.incrementAndGet(); - myLastInput = theInput; - return theInput; - } - - public int getCount() { - return myCount.get(); - } - - public Bundle getLastInput() { - return myLastInput; - } - } } diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR5IT.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR5IT.java new file mode 100644 index 00000000000..c860c12961c --- /dev/null +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR5IT.java @@ -0,0 +1,884 @@ +package ca.uhn.fhir.jpa.subscription.resthook; + +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; +import ca.uhn.fhir.jpa.subscription.BaseSubscriptionsR5Test; +import ca.uhn.fhir.jpa.test.util.StoppableSubscriptionDeliveringRestHookSubscriber; +import ca.uhn.fhir.rest.api.CacheControlDirective; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.util.BundleUtil; +import ca.uhn.fhir.util.HapiExtensions; +import org.hamcrest.MatcherAssert; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r5.model.BooleanType; +import org.hl7.fhir.r5.model.Bundle; +import org.hl7.fhir.r5.model.CodeableConcept; +import org.hl7.fhir.r5.model.Coding; +import org.hl7.fhir.r5.model.Enumerations; +import org.hl7.fhir.r5.model.IdType; +import org.hl7.fhir.r5.model.Observation; +import org.hl7.fhir.r5.model.Patient; +import org.hl7.fhir.r5.model.SearchParameter; +import org.hl7.fhir.r5.model.Subscription; +import org.hl7.fhir.r5.model.SubscriptionStatus; +import org.hl7.fhir.r5.model.SubscriptionTopic; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.matchesPattern; +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.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test the rest-hook subscriptions + */ +public class RestHookTestR5IT extends BaseSubscriptionsR5Test { + private static final Logger ourLog = LoggerFactory.getLogger(RestHookTestR5IT.class); + public static final String OBS_CODE = "1000000050"; + public static final String OBS_CODE2 = OBS_CODE + "111"; + private static final String CUSTOM_URL = "http://custom.topic.url"; + + @Autowired + StoppableSubscriptionDeliveringRestHookSubscriber myStoppableSubscriptionDeliveringRestHookSubscriber; + + @AfterEach + public void cleanupStoppableSubscriptionDeliveringRestHookSubscriber() { + ourLog.info("@AfterEach"); + myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(null); + myStoppableSubscriptionDeliveringRestHookSubscriber.unPause(); + } + + @Test + public void testRestHookSubscriptionApplicationFhirJson() throws Exception { + createObservationSubscriptionTopic(OBS_CODE); + createObservationSubscriptionTopic(OBS_CODE2); + waitForRegisteredSubscriptionTopicCount(2); + + // WIP STR5 will likely require matching TopicSubscription + Subscription subscription1 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_XML_NEW); + + Subscription subscription = postSubscription(subscription1); + waitForActivatedSubscriptionCount(1); + + Observation sentObservation = sendObservationExpectDelivery(); + + // Should see 1 subscription notification + assertReceivedTransactionCount(1); + + Observation obs = assertBundleAndGetObservation(subscription, sentObservation); + assertEquals(Enumerations.ObservationStatus.FINAL, obs.getStatus()); + assertEquals(sentObservation.getIdElement(), obs.getIdElement()); + } + + @NotNull + private Observation sendObservationExpectDelivery() throws InterruptedException { + return sendObservation(OBS_CODE, "SNOMED-CT", true); + } + + @Test + public void testUpdatesHaveCorrectMetadata() throws Exception { + + createSubscriptionTopic(); + + Subscription subscription = createMatchingTopicSubscription(); + + /* + * Send version 1 + */ + + Observation sentObservation = sendObservationExpectDelivery(); + sentObservation = myObservationDao.read(sentObservation.getIdElement().toUnqualifiedVersionless()); + + // Should see 1 subscription notification + assertReceivedTransactionCount(1); + + Observation receivedObs = assertBundleAndGetObservation(subscription, sentObservation); + + Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType()); + Assertions.assertEquals("1", receivedObs.getIdElement().getVersionIdPart()); + Assertions.assertEquals("1", receivedObs.getMeta().getVersionId()); + Assertions.assertEquals(sentObservation.getMeta().getLastUpdatedElement().getValueAsString(), receivedObs.getMeta().getLastUpdatedElement().getValueAsString()); + Assertions.assertEquals(sentObservation.getMeta().getLastUpdatedElement().getValueAsString(), receivedObs.getMeta().getLastUpdatedElement().getValueAsString()); + Assertions.assertEquals("1", receivedObs.getIdentifierFirstRep().getValue()); + + /* + * Send version 2 + */ + + sentObservation.getIdentifierFirstRep().setSystem("foo").setValue("2"); + updateResource(sentObservation, true); + sentObservation = myObservationDao.read(sentObservation.getIdElement().toUnqualifiedVersionless()); + + // Should see a second subscription notification + assertReceivedTransactionCount(2); + + receivedObs = assertBundleAndGetObservation(subscription, sentObservation); + + Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType()); + Assertions.assertEquals("2", receivedObs.getIdElement().getVersionIdPart()); + Assertions.assertEquals("2", receivedObs.getMeta().getVersionId()); + Assertions.assertEquals(sentObservation.getMeta().getLastUpdatedElement().getValueAsString(), receivedObs.getMeta().getLastUpdatedElement().getValueAsString()); + Assertions.assertEquals(sentObservation.getMeta().getLastUpdatedElement().getValueAsString(), receivedObs.getMeta().getLastUpdatedElement().getValueAsString()); + Assertions.assertEquals("2", receivedObs.getIdentifierFirstRep().getValue()); + } + + @NotNull + private Subscription createMatchingTopicSubscription() throws Exception { + Subscription subscription = createTopicSubscription(OBS_CODE); + waitForActivatedSubscriptionCount(1); + return subscription; + } + + @Test + public void testPlaceholderReferencesInTransactionAreResolvedCorrectly() throws Exception { + createSubscriptionTopic(); + + Subscription subscription = createMatchingTopicSubscription(); + + // Create a transaction that should match + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + + Patient patient = new Patient(); + patient.setId(IdType.newRandomUuid()); + patient.getIdentifierFirstRep().setSystem("foo").setValue("AAA"); + bundle.addEntry().setResource(patient).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Patient"); + + Observation sentObservation = new Observation(); + sentObservation.getIdentifierFirstRep().setSystem("foo").setValue("1"); + sentObservation.getCode().addCoding().setCode(OBS_CODE).setSystem("SNOMED-CT"); + sentObservation.setStatus(Enumerations.ObservationStatus.FINAL); + sentObservation.getSubject().setReference(patient.getId()); + bundle.addEntry().setResource(sentObservation).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Observation"); + + // Send the transaction + sendTransaction(bundle, true); + + Observation receivedObs = assertBundleAndGetObservation(subscription, sentObservation); + + MatcherAssert.assertThat(receivedObs.getSubject().getReference(), matchesPattern("Patient/[0-9]+")); + } + + @Test + public void testUpdatesHaveCorrectMetadataUsingTransactions() throws Exception { + createSubscriptionTopic(); + + Subscription subscription = createMatchingTopicSubscription(); + + /* + * Send version 1 + */ + + Observation sentObservation = new Observation(); + sentObservation.getIdentifierFirstRep().setSystem("foo").setValue("1"); + sentObservation.getCode().addCoding().setCode(OBS_CODE).setSystem("SNOMED-CT"); + sentObservation.setStatus(Enumerations.ObservationStatus.FINAL); + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + bundle.addEntry().setResource(sentObservation).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Observation"); + // Send the transaction + Bundle responseBundle = sendTransaction(bundle, true); + assertReceivedTransactionCount(1); + + Observation receivedObs = assertBundleAndGetObservation(subscription, sentObservation); + + Observation obs = myObservationDao.read(new IdType(responseBundle.getEntry().get(0).getResponse().getLocation())); + + Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType()); + Assertions.assertEquals("1", receivedObs.getIdElement().getVersionIdPart()); + Assertions.assertEquals("1", receivedObs.getMeta().getVersionId()); + Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), receivedObs.getMeta().getLastUpdatedElement().getValueAsString()); + Assertions.assertEquals("1", receivedObs.getIdentifierFirstRep().getValue()); + + /* + * Send version 2 + */ + + sentObservation = new Observation(); + sentObservation.setId(obs.getId()); + sentObservation.getIdentifierFirstRep().setSystem("foo").setValue("2"); + sentObservation.getCode().addCoding().setCode(OBS_CODE).setSystem("SNOMED-CT"); + sentObservation.setStatus(Enumerations.ObservationStatus.FINAL); + bundle = new Bundle(); + bundle.setType(Bundle.BundleType.TRANSACTION); + bundle.addEntry().setResource(sentObservation).getRequest().setMethod(Bundle.HTTPVerb.PUT).setUrl(obs.getIdElement().toUnqualifiedVersionless().getValue()); + // Send the transaction + sendTransaction(bundle, true); + assertReceivedTransactionCount(2); + + receivedObs = assertBundleAndGetObservation(subscription, sentObservation); + obs = myObservationDao.read(obs.getIdElement().toUnqualifiedVersionless()); + + Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType()); + Assertions.assertEquals("2", receivedObs.getIdElement().getVersionIdPart()); + Assertions.assertEquals("2", receivedObs.getMeta().getVersionId()); + Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), receivedObs.getMeta().getLastUpdatedElement().getValueAsString()); + Assertions.assertEquals("2", receivedObs.getIdentifierFirstRep().getValue()); + } + + @Test + public void testRepeatedDeliveries() throws Exception { + createSubscriptionTopic(); + + createTopicSubscription(OBS_CODE); + waitForActivatedSubscriptionCount(1); + + mySubscriptionTopicsCheckedLatch.setExpectedCount(100); + mySubscriptionDeliveredLatch.setExpectedCount(100); + // WIP STR5 I don't know the answer to this, but should we be bunching these up into a single delivery? + for (int i = 0; i < 100; i++) { + Observation observation = new Observation(); + observation.getIdentifierFirstRep().setSystem("foo").setValue("ID" + i); + observation.getCode().addCoding().setCode(OBS_CODE).setSystem("SNOMED-CT"); + observation.setStatus(Enumerations.ObservationStatus.FINAL); + myObservationDao.create(observation, mySrd); + } + mySubscriptionTopicsCheckedLatch.awaitExpected(); + mySubscriptionDeliveredLatch.awaitExpected(); + } + + @Test + public void testActiveSubscriptionShouldntReActivate() throws Exception { + createSubscriptionTopic(); + + createTopicSubscription(); + waitForActivatedSubscriptionCount(1); + + for (int i = 0; i < 5; i++) { + int changes = this.mySubscriptionLoader.doSyncSubscriptionsForUnitTest(); + assertEquals(0, changes); + } + } + + @NotNull + private Subscription createTopicSubscription() throws InterruptedException { + // WIP STR5 will likely require matching TopicSubscription + Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_JSON_NEW); + + return postSubscription(subscription); + } + + private void createSubscriptionTopic() throws InterruptedException { + createObservationSubscriptionTopic(OBS_CODE); + waitForRegisteredSubscriptionTopicCount(1); + } + + @Test + public void testRestHookSubscriptionNoopUpdateDoesntTriggerNewDelivery() throws Exception { + createObservationSubscriptionTopic(OBS_CODE); + createObservationSubscriptionTopic(OBS_CODE2); + waitForRegisteredSubscriptionTopicCount(2); + + Subscription subscription = createTopicSubscription(); + waitForActivatedSubscriptionCount(1); + + Observation sentObservation = sendObservationExpectDelivery(); + + assertReceivedTransactionCount(1); + + Observation obs = assertBundleAndGetObservation(subscription, sentObservation); + + // Should see 1 subscription notification + Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType()); + + // Send an update with no changes + obs.setId(obs.getIdElement().toUnqualifiedVersionless()); + myObservationDao.update(obs, mySrd); + + // TODO KHS replace this sleep with a latch on http request processed + Thread.sleep(1000); + + // Should be no further deliveries + assertReceivedTransactionCount(1); + } + + @Test + public void testRestHookSubscriptionApplicationJsonDisableVersionIdInDelivery() throws Exception { + createSubscriptionTopic(); + + waitForActivatedSubscriptionCount(0); + Subscription subscription = createTopicSubscription(); + waitForActivatedSubscriptionCount(1); + + ourLog.info("** About to send observation"); + Observation sentObservation1 = sendObservationExpectDelivery(); + + assertReceivedTransactionCount(1); + + Observation obs = assertBundleAndGetObservation(subscription, sentObservation1); + + Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType()); + + IdType idElement = obs.getIdElement(); + assertEquals(sentObservation1.getIdElement().getIdPart(), idElement.getIdPart()); + // VersionId is present + assertEquals(sentObservation1.getIdElement().getVersionIdPart(), idElement.getVersionIdPart()); + + subscription + .addExtension(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS, new BooleanType("true")); + ourLog.info("** About to update subscription"); + + ourLog.info("** About to send another..."); + updateResource(subscription, false); + + ourLog.info("** About to send observation"); + Observation sentObservation2 = sendObservationExpectDelivery(); + + assertReceivedTransactionCount(2); + + Observation obs2 = assertBundleAndGetObservation(subscription, sentObservation2); + + Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType()); + + idElement =obs2.getIdElement(); + assertEquals(sentObservation2.getIdElement().getIdPart(), idElement.getIdPart()); + // Now VersionId is stripped + assertEquals(null, idElement.getVersionIdPart()); + } + + @Test + public void testRestHookSubscriptionDoesntGetLatestVersionByDefault() throws Exception { + createSubscriptionTopic(); + + Subscription subscription = createTopicSubscription(); + waitForActivatedSubscriptionCount(1); + + myStoppableSubscriptionDeliveringRestHookSubscriber.pause(); + final CountDownLatch countDownLatch = new CountDownLatch(1); + myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(countDownLatch); + + ourLog.info("** About to send observation"); + Observation sentObservation = sendObservation(OBS_CODE, "SNOMED-CT", false); + assertEquals("1", sentObservation.getIdElement().getVersionIdPart()); + assertNull(sentObservation.getNoteFirstRep().getText()); + + sentObservation.getNoteFirstRep().setText("changed"); + + DaoMethodOutcome methodOutcome = updateResource(sentObservation, false); + assertEquals("2", methodOutcome.getId().getVersionIdPart()); + assertEquals("changed", sentObservation.getNoteFirstRep().getText()); + + // Wait for our two delivery channel threads to be paused + assertTrue(countDownLatch.await(5L, TimeUnit.SECONDS)); + // Open the floodgates! + mySubscriptionDeliveredLatch.setExpectedCount(2); + myStoppableSubscriptionDeliveringRestHookSubscriber.unPause(); + mySubscriptionDeliveredLatch.awaitExpected(); + + assertReceivedTransactionCount(2); + + Observation observation1 = getReceivedObservations().stream() + .filter(t -> "1".equals(t.getIdElement().getVersionIdPart())) + .findFirst() + .orElseThrow(); + Observation observation2 = getReceivedObservations().stream() + .filter(t -> "2".equals(t.getIdElement().getVersionIdPart())) + .findFirst() + .orElseThrow(); + + assertEquals("1", observation1.getIdElement().getVersionIdPart()); + assertNull(observation1.getNoteFirstRep().getText()); + assertEquals("2", observation2.getIdElement().getVersionIdPart()); + assertEquals("changed", observation2.getNoteFirstRep().getText()); + } + + @Test + public void testRestHookSubscriptionGetsLatestVersionWithFlag() throws Exception { + createSubscriptionTopic(); + + Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_JSON_NEW); + subscription + .addExtension(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION, new BooleanType("true")); + postSubscription(subscription); + waitForActivatedSubscriptionCount(1); + + myStoppableSubscriptionDeliveringRestHookSubscriber.pause(); + final CountDownLatch countDownLatch = new CountDownLatch(1); + myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(countDownLatch); + + ourLog.info("** About to send observation"); + Observation sentObservation = sendObservation(OBS_CODE, "SNOMED-CT", false); + assertEquals("1", sentObservation.getIdElement().getVersionIdPart()); + assertNull(sentObservation.getNoteFirstRep().getText()); + + sentObservation.getNoteFirstRep().setText("changed"); + DaoMethodOutcome methodOutcome = updateResource(sentObservation, false); + assertEquals("2", methodOutcome.getId().getVersionIdPart()); + assertEquals("changed", sentObservation.getNoteFirstRep().getText()); + + // Wait for our two delivery channel threads to be paused + assertTrue(countDownLatch.await(5L, TimeUnit.SECONDS)); + // Open the floodgates! + mySubscriptionDeliveredLatch.setExpectedCount(2); + myStoppableSubscriptionDeliveringRestHookSubscriber.unPause(); + mySubscriptionDeliveredLatch.awaitExpected(); + + assertTrue(getReceivedObservations().stream().allMatch(t -> "2".equals(t.getIdElement().getVersionIdPart()))); + assertTrue(getReceivedObservations().stream().anyMatch(t -> "changed".equals(t.getNoteFirstRep().getText()))); + } + + @Test + public void testRestHookSubscriptionApplicationJson() throws Exception { + createObservationSubscriptionTopic(OBS_CODE); + createObservationSubscriptionTopic(OBS_CODE2); + waitForRegisteredSubscriptionTopicCount(2); + + Subscription subscription1 = createTopicSubscription(); + // WIP STR5 will likely require matching TopicSubscription + Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE2, Constants.CT_FHIR_JSON_NEW); + + Subscription subscription2 = postSubscription(subscription); + waitForActivatedSubscriptionCount(2); + + Observation sentObservation1 = sendObservationExpectDelivery(); + assertReceivedTransactionCount(1); + Observation receivedObs = assertBundleAndGetObservation(subscription1, sentObservation1); + assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType()); + + Assertions.assertEquals("1", receivedObs.getIdElement().getVersionIdPart()); + + Subscription subscriptionTemp = myClient.read(Subscription.class, subscription2.getId()); + assertNotNull(subscriptionTemp); + subscriptionTemp.setTopic(subscription1.getTopic()); + updateResource(subscriptionTemp, false); + + Observation observation2 = sendObservationExpectDelivery(); + + assertReceivedTransactionCount(3); + + deleteSubscription(subscription2); + + Observation observationTemp3 = sendObservationExpectDelivery(); + + // Should see only one subscription notification + assertReceivedTransactionCount(4); + + Observation observation3 = myClient.read(Observation.class, observationTemp3.getId()); + CodeableConcept codeableConcept = new CodeableConcept(); + observation3.setCode(codeableConcept); + Coding coding = codeableConcept.addCoding(); + coding.setCode(OBS_CODE + "111"); + coding.setSystem("SNOMED-CT"); + updateResource(observation3, false); + + // Should see no subscription notification + assertReceivedTransactionCount(4); + + Observation observation3a = myClient.read(Observation.class, observationTemp3.getId()); + + CodeableConcept codeableConcept1 = new CodeableConcept(); + observation3a.setCode(codeableConcept1); + Coding coding1 = codeableConcept1.addCoding(); + coding1.setCode(OBS_CODE); + coding1.setSystem("SNOMED-CT"); + updateResource(observation3a, true); + + // Should see only one subscription notification + assertReceivedTransactionCount(5); + + assertFalse(subscription1.getId().equals(subscription2.getId())); + assertFalse(sentObservation1.getId().isEmpty()); + assertFalse(observation2.getId().isEmpty()); + } + + private void deleteSubscription(Subscription subscription2) throws InterruptedException { + mySubscriptionTopicsCheckedLatch.setExpectedCount(1); + myClient.delete().resourceById(new IdType("Subscription/" + subscription2.getId())).execute(); + mySubscriptionTopicsCheckedLatch.awaitExpected(); + } + + private void assertReceivedTransactionCount(int theExpected) { + if (getSystemProviderCount() != theExpected) { + String list = getReceivedObservations().stream() + .map(t -> t.getIdElement().toUnqualifiedVersionless().getValue() + " " + t.getCode().getCodingFirstRep().getCode()) + .collect(Collectors.joining(", ")); + throw new AssertionError("Expected " + theExpected + " transactions, have " + getSystemProviderCount() + ": " + list); + } + } + + @Test + public void testRestHookSubscriptionApplicationJsonDatabase() throws Exception { + // Same test as above, but now run it using database matching + myStorageSettings.setEnableInMemorySubscriptionMatching(false); + testRestHookSubscriptionApplicationJson(); + } + + @Nonnull + private Subscription createTopicSubscription(String theTopicUrlSuffix) throws InterruptedException { + // WIP STR5 will likely require matching TopicSubscription + Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + theTopicUrlSuffix, Constants.CT_FHIR_JSON_NEW); + + return postSubscription(subscription); + } + + @Test + public void testSubscriptionTriggerViaSubscription() throws Exception { + createSubscriptionTopic(); + + // WIP STR5 will likely require matching TopicSubscription + Subscription subscription1 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_XML_NEW); + + Subscription subscription = postSubscription(subscription1); + waitForActivatedSubscriptionCount(1); + + ourLog.info("** About to send observation"); + + Observation sentObservation = new Observation(); + sentObservation.addIdentifier().setSystem("foo").setValue("bar1"); + sentObservation.setId(IdType.newRandomUuid().getValue()); + CodeableConcept codeableConcept = new CodeableConcept() + .addCoding(new Coding().setCode(OBS_CODE).setSystem("SNOMED-CT")); + sentObservation.setCode(codeableConcept); + sentObservation.setStatus(Enumerations.ObservationStatus.FINAL); + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("foo").setValue("bar2"); + patient.setId(IdType.newRandomUuid().getValue()); + patient.setActive(true); + sentObservation.getSubject().setReference(patient.getId()); + + Bundle requestBundle = new Bundle(); + requestBundle.setType(Bundle.BundleType.TRANSACTION); + requestBundle.addEntry() + .setResource(sentObservation) + .setFullUrl(sentObservation.getId()) + .getRequest() + .setUrl("Observation?identifier=foo|bar1") + .setMethod(Bundle.HTTPVerb.PUT); + requestBundle.addEntry() + .setResource(patient) + .setFullUrl(patient.getId()) + .getRequest() + .setUrl("Patient?identifier=foo|bar2") + .setMethod(Bundle.HTTPVerb.PUT); + + sendTransaction(requestBundle, true); + + // Should see 1 subscription notification + assertReceivedTransactionCount(1); + Observation receivedObs = assertBundleAndGetObservation(subscription, sentObservation); + assertEquals(Constants.CT_FHIR_XML_NEW, getLastSystemProviderContentType()); + + ourLog.debug("Observation content: {}", myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(receivedObs)); + } + + @Test + public void testUpdateSubscriptionToMatchLater() throws Exception { + SubscriptionTopic subscriptionTopic = createObservationSubscriptionTopic(OBS_CODE2); + waitForRegisteredSubscriptionTopicCount(1); + + ourLog.info("** About to create non-matching subscription"); + + // WIP STR5 will likely require matching TopicSubscription + Subscription subscription1 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE2, Constants.CT_FHIR_XML_NEW); + + Subscription subscription = postSubscription(subscription1); + waitForActivatedSubscriptionCount(1); + + ourLog.info("** About to send observation that wont match"); + + Observation observation1 = sendObservation(OBS_CODE, "SNOMED-CT", false); + assertReceivedTransactionCount(0); + + ourLog.info("** About to update subscription topic"); + SubscriptionTopic subscriptionTopicTemp = myClient.read(SubscriptionTopic.class, subscriptionTopic.getId()); + assertNotNull(subscriptionTopicTemp); + setSubscriptionTopicCriteria(subscriptionTopicTemp, "Observation?code=SNOMED-CT|" + OBS_CODE); + updateResource(subscriptionTopicTemp, false); + + ourLog.info("** About to send Observation 2"); + Observation observation2 = sendObservationExpectDelivery(); + + // Should see a subscription notification this time + assertReceivedTransactionCount(1); + + deleteSubscription(subscription); + + Observation observationTemp3 = sendObservation(OBS_CODE, "SNOMED-CT", false); + + // No more matches + assertReceivedTransactionCount(1); + } + + private static void setSubscriptionTopicCriteria(SubscriptionTopic subscriptionTopicTemp, String theCriteria) { + subscriptionTopicTemp.getResourceTriggerFirstRep().getQueryCriteria().setCurrent(theCriteria); + } + + @Test + public void testRestHookSubscriptionApplicationXmlJson() throws Exception { + createObservationSubscriptionTopic(OBS_CODE); + createObservationSubscriptionTopic(OBS_CODE2); + waitForRegisteredSubscriptionTopicCount(2); + + // WIP STR5 will likely require matching TopicSubscription + Subscription subscription3 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_XML_NEW); + + Subscription subscription1 = postSubscription(subscription3); + // WIP STR5 will likely require matching TopicSubscription + Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE2, Constants.CT_FHIR_XML_NEW); + + Subscription subscription2 = postSubscription(subscription); + waitForActivatedSubscriptionCount(2); + + Observation observation1 = sendObservationExpectDelivery(); + + // Should see 1 subscription notification + assertReceivedTransactionCount(1); + assertEquals(Constants.CT_FHIR_XML_NEW, getLastSystemProviderContentType()); + } + + @Test + public void testRestHookTopicSubscriptionInvalidTopic() throws Exception { + try { + createTopicSubscription(OBS_CODE); + fail(); + } catch (UnprocessableEntityException e) { + assertEquals("HTTP 422 Unprocessable Entity: " + Msg.code(2322) + "No SubscriptionTopic exists with topic: " + SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, e.getMessage()); + } + } + + @Test + public void testRestHookSubscriptionTopicInvalidCriteria() throws Exception { + try { + createSubscriptionTopicWithCriteria("Observation?codeeeee=SNOMED-CT"); + } catch (UnprocessableEntityException e) { + assertEquals("HTTP 422 Unprocessable Entity: " + Msg.code(2339) + "Invalid SubscriptionTopic criteria 'Observation?codeeeee=SNOMED-CT' in SubscriptionTopic.resourceTrigger.queryCriteria.current: HAPI-0488: Failed to parse match URL[Observation?codeeeee=SNOMED-CT] - Resource type Observation does not have a parameter with name: codeeeee", e.getMessage()); + } + } + + @Nonnull + private SubscriptionTopic createSubscriptionTopicWithCriteria(String theCriteria) throws InterruptedException { + SubscriptionTopic subscriptionTopic = buildSubscriptionTopic(CUSTOM_URL); + setSubscriptionTopicCriteria(subscriptionTopic, theCriteria); + return createSubscriptionTopic(subscriptionTopic); + } + + @Test + public void testSubscriptionWithHeaders() throws Exception { + createSubscriptionTopic(); + + // Add some headers, and we'll also turn back to requested status for fun + Subscription subscription = createTopicSubscription(); + waitForActivatedSubscriptionCount(1); + + subscription.addHeader("X-Foo: FOO"); + subscription.addHeader("X-Bar: BAR"); + updateResource(subscription, false); + + Observation sentObservation = sendObservationExpectDelivery(); + + // Should see 1 subscription notification + assertReceivedTransactionCount(1); + Observation receivedObservation = assertBundleAndGetObservation(subscription, sentObservation); + Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, getLastSystemProviderContentType()); + + assertThat(getLastSystemProviderHeaders(), hasItem("X-Foo: FOO")); + assertThat(getLastSystemProviderHeaders(), hasItem("X-Bar: BAR")); + } + + @Test + public void testDisableSubscription() throws Exception { + createSubscriptionTopic(); + + Subscription subscription = createTopicSubscription(); + waitForActivatedSubscriptionCount(1); + + Observation sentObservation = sendObservationExpectDelivery(); + + // Should see 1 subscription notification + assertReceivedTransactionCount(1); + Observation receivedObservation = assertBundleAndGetObservation(subscription, sentObservation); + + // Disable + subscription.setStatus(Enumerations.SubscriptionStatusCodes.OFF); + updateResource(subscription, false); + + // Send another observation + sendObservation(OBS_CODE, "SNOMED-CT", false); + + // Should see no new delivery + assertReceivedTransactionCount(1); + } + + @Test + public void testInvalidProvenanceParam() { + assertThrows(UnprocessableEntityException.class, () -> { + String criteriabad = "Provenance?foo=http://hl7.org/fhir/v3/DocumentCompletion%7CAU"; + createSubscriptionTopicWithCriteria(criteriabad); + }); + } + + @Test + public void testInvalidProcedureRequestParam() { + assertThrows(UnprocessableEntityException.class, () -> { + String criteriabad = "ProcedureRequest?intent=instance-order&category=Laboratory"; + createSubscriptionTopicWithCriteria(criteriabad); + }); + } + + @Test + public void testInvalidBodySiteParam() { + assertThrows(UnprocessableEntityException.class, () -> { + String criteriabad = "BodySite?accessType=Catheter"; + createSubscriptionTopicWithCriteria(criteriabad); + }); + } + + @Test + public void testGoodSubscriptionPersists() throws Exception { + createSubscriptionTopic(); + + assertEquals(0, subscriptionCount()); + Subscription subscription = createTopicSubscription(); + waitForActivatedSubscriptionCount(1); + assertEquals(1, subscriptionCount()); + } + + private int subscriptionCount() { + IBaseBundle found = myClient.search().forResource(Subscription.class).cacheControl(new CacheControlDirective().setNoCache(true)).execute(); + return toUnqualifiedVersionlessIdValues(found).size(); + } + + @Test + public void testSubscriptionTopicWithNoStatusIsRejected() throws InterruptedException { + SubscriptionTopic subscriptionTopic = buildSubscriptionTopic(OBS_CODE); + subscriptionTopic.setStatus(null); + + try { + createSubscriptionTopic(subscriptionTopic); + fail(); + } catch (UnprocessableEntityException e) { + assertThat(e.getMessage(), containsString("Can not process submitted SubscriptionTopic - SubscriptionTopic.status must be populated on this server")); + } + } + + + @Test + public void testBadSubscriptionTopicDoesntPersist() throws InterruptedException { + assertEquals(0, subscriptionCount()); + String criteriaBad = "BodySite?accessType=Catheter"; + try { + createSubscriptionTopicWithCriteria(criteriaBad); + } catch (UnprocessableEntityException e) { + ourLog.info("Expected exception", e); + } + assertEquals(0, subscriptionCount()); + } + + @Test + public void testCustomSearchParam() throws Exception { + String criteria = "Observation?accessType=Catheter,PD%20Catheter"; + + SearchParameter sp = new SearchParameter(); + sp.addBase("Observation"); + sp.setCode("accessType"); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setExpression("Observation.extension('Observation#accessType')"); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + mySearchParameterDao.create(sp); + mySearchParamRegistry.forceRefresh(); + createSubscriptionTopicWithCriteria(criteria); + waitForRegisteredSubscriptionTopicCount(1); + + Subscription subscription = createTopicSubscription(CUSTOM_URL); + waitForActivatedSubscriptionCount(1); + + { + Observation observation = new Observation(); + observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("Catheter")); + createResource(observation, true); + assertReceivedTransactionCount(1); + assertBundleAndGetObservation(subscription, observation); + } + { + Observation observation = new Observation(); + observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("PD Catheter")); + createResource(observation, true); + assertReceivedTransactionCount(2); + assertBundleAndGetObservation(subscription, observation); + } + { + Observation observation = new Observation(); + createResource(observation, false); + assertReceivedTransactionCount(2); + } + { + Observation observation = new Observation(); + observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("XXX")); + createResource(observation, false); + assertReceivedTransactionCount(2); + } + } + + private Observation assertBundleAndGetObservation(Subscription subscription, Observation sentObservation) { + Bundle receivedBundle = getLastSystemProviderBundle(); + List resources = BundleUtil.toListOfResources(myFhirCtx, receivedBundle); + assertEquals(2, resources.size()); + + SubscriptionStatus ss = (SubscriptionStatus) resources.get(0); + validateSubscriptionStatus(subscription, sentObservation, ss); + + return (Observation) resources.get(1); + } + + private SubscriptionTopic createObservationSubscriptionTopic(String theCode) throws InterruptedException { + SubscriptionTopic subscriptionTopic = buildSubscriptionTopic(theCode); + return createSubscriptionTopic(subscriptionTopic); +} + + @Nonnull + private static SubscriptionTopic buildSubscriptionTopic(String theCode) { + SubscriptionTopic retval = new SubscriptionTopic(); + retval.setUrl(SUBSCRIPTION_TOPIC_TEST_URL+ theCode); + retval.setStatus(Enumerations.PublicationStatus.ACTIVE); + SubscriptionTopic.SubscriptionTopicResourceTriggerComponent trigger = retval.addResourceTrigger(); + trigger.setResource("Observation"); + trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.CREATE); + trigger.addSupportedInteraction(SubscriptionTopic.InteractionTrigger.UPDATE); + SubscriptionTopic.SubscriptionTopicResourceTriggerQueryCriteriaComponent queryCriteria = trigger.getQueryCriteria(); + queryCriteria.setCurrent("Observation?code=SNOMED-CT|" + theCode); + queryCriteria.setRequireBoth(false); + return retval; + } + + + private Observation sendObservation(String theCode, String theSystem, boolean theExpectDelivery) throws InterruptedException { + Observation observation = new Observation(); + CodeableConcept codeableConcept = new CodeableConcept(); + observation.setCode(codeableConcept); + observation.getIdentifierFirstRep().setSystem("foo").setValue("1"); + Coding coding = codeableConcept.addCoding(); + coding.setCode(theCode); + coding.setSystem(theSystem); + + observation.setStatus(Enumerations.ObservationStatus.FINAL); + + IIdType id = createResource(observation, theExpectDelivery); + observation.setId(id); + + return observation; + } + +} diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR5Test.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR5Test.java deleted file mode 100644 index 6c9b9f501bc..00000000000 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR5Test.java +++ /dev/null @@ -1,991 +0,0 @@ -package ca.uhn.fhir.jpa.subscription.resthook; - -import ca.uhn.fhir.i18n.Msg; -import ca.uhn.fhir.jpa.subscription.BaseSubscriptionsR5Test; -import ca.uhn.fhir.jpa.test.util.StoppableSubscriptionDeliveringRestHookSubscriber; -import ca.uhn.fhir.rest.api.CacheControlDirective; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; -import ca.uhn.fhir.util.HapiExtensions; -import org.hamcrest.MatcherAssert; -import org.hl7.fhir.instance.model.api.IBaseBundle; -import org.hl7.fhir.r5.model.BooleanType; -import org.hl7.fhir.r5.model.Bundle; -import org.hl7.fhir.r5.model.CodeableConcept; -import org.hl7.fhir.r5.model.Coding; -import org.hl7.fhir.r5.model.Enumerations; -import org.hl7.fhir.r5.model.IdType; -import org.hl7.fhir.r5.model.Observation; -import org.hl7.fhir.r5.model.Patient; -import org.hl7.fhir.r5.model.SearchParameter; -import org.hl7.fhir.r5.model.Subscription; -import org.hl7.fhir.r5.model.SubscriptionTopic; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.matchesPattern; -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.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -/** - * Test the rest-hook subscriptions - */ -public class RestHookTestR5Test extends BaseSubscriptionsR5Test { - private static final Logger ourLog = LoggerFactory.getLogger(RestHookTestR5Test.class); - - @Autowired - StoppableSubscriptionDeliveringRestHookSubscriber myStoppableSubscriptionDeliveringRestHookSubscriber; - - @AfterEach - public void cleanupStoppableSubscriptionDeliveringRestHookSubscriber() { - ourLog.info("@AfterEach"); - myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(null); - myStoppableSubscriptionDeliveringRestHookSubscriber.unPause(); - } - - @Test - public void testRestHookSubscriptionApplicationFhirJson() throws Exception { - String payload = "application/fhir+json"; - - String code = "1000000050"; - String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - - createSubscription(criteria1, payload); - createSubscription(criteria2, payload); - waitForActivatedSubscriptionCount(2); - - sendObservation(code, "SNOMED-CT"); - - // Should see 1 subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0)); - } - - @Test - public void testUpdatesHaveCorrectMetadata() throws Exception { - String payload = "application/fhir+json"; - - String code = "1000000050"; - String criteria1 = "Observation?"; - - createSubscription(criteria1, payload); - waitForActivatedSubscriptionCount(1); - - /* - * Send version 1 - */ - - Observation obs = sendObservation(code, "SNOMED-CT"); - obs = myObservationDao.read(obs.getIdElement().toUnqualifiedVersionless()); - - // Should see 1 subscription notification - waitForQueueToDrain(); - int idx = 0; - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(idx)); - Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdElement().getVersionIdPart()); - Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getVersionId()); - Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getLastUpdatedElement().getValueAsString()); - Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getLastUpdatedElement().getValueAsString()); - Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdentifierFirstRep().getValue()); - - /* - * Send version 2 - */ - - obs.getIdentifierFirstRep().setSystem("foo").setValue("2"); - myObservationDao.update(obs); - obs = myObservationDao.read(obs.getIdElement().toUnqualifiedVersionless()); - - // Should see 1 subscription notification - waitForQueueToDrain(); - idx++; - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations); - Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(idx)); - Assertions.assertEquals("2", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdElement().getVersionIdPart()); - Assertions.assertEquals("2", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getVersionId()); - Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getLastUpdatedElement().getValueAsString()); - Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getLastUpdatedElement().getValueAsString()); - Assertions.assertEquals("2", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdentifierFirstRep().getValue()); - } - - @Test - public void testPlaceholderReferencesInTransactionAreResolvedCorrectly() throws Exception { - - String payload = "application/fhir+json"; - String code = "1000000050"; - String criteria1 = "Observation?"; - createSubscription(criteria1, payload); - waitForActivatedSubscriptionCount(1); - - // Create a transaction that should match - Bundle bundle = new Bundle(); - bundle.setType(Bundle.BundleType.TRANSACTION); - - Patient patient = new Patient(); - patient.setId(IdType.newRandomUuid()); - patient.getIdentifierFirstRep().setSystem("foo").setValue("AAA"); - bundle.addEntry().setResource(patient).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Patient"); - - Observation observation = new Observation(); - observation.getIdentifierFirstRep().setSystem("foo").setValue("1"); - observation.getCode().addCoding().setCode(code).setSystem("SNOMED-CT"); - observation.setStatus(Enumerations.ObservationStatus.FINAL); - observation.getSubject().setReference(patient.getId()); - bundle.addEntry().setResource(observation).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Observation"); - - // Send the transaction - mySystemDao.transaction(null, bundle); - - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - - MatcherAssert.assertThat(BaseSubscriptionsR5Test.ourUpdatedObservations.get(0).getSubject().getReference(), matchesPattern("Patient/[0-9]+")); - } - - @Test - public void testUpdatesHaveCorrectMetadataUsingTransactions() throws Exception { - String payload = "application/fhir+json"; - - String code = "1000000050"; - String criteria1 = "Observation?"; - - createSubscription(criteria1, payload); - waitForActivatedSubscriptionCount(1); - - /* - * Send version 1 - */ - - Observation observation = new Observation(); - observation.getIdentifierFirstRep().setSystem("foo").setValue("1"); - observation.getCode().addCoding().setCode(code).setSystem("SNOMED-CT"); - observation.setStatus(Enumerations.ObservationStatus.FINAL); - Bundle bundle = new Bundle(); - bundle.setType(Bundle.BundleType.TRANSACTION); - bundle.addEntry().setResource(observation).getRequest().setMethod(Bundle.HTTPVerb.POST).setUrl("Observation"); - Bundle responseBundle = mySystemDao.transaction(null, bundle); - - Observation obs = myObservationDao.read(new IdType(responseBundle.getEntry().get(0).getResponse().getLocation())); - - // Should see 1 subscription notification - waitForQueueToDrain(); - int idx = 0; - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(idx)); - Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdElement().getVersionIdPart()); - Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getVersionId()); - Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getLastUpdatedElement().getValueAsString()); - Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdentifierFirstRep().getValue()); - - /* - * Send version 2 - */ - - observation = new Observation(); - observation.setId(obs.getId()); - observation.getIdentifierFirstRep().setSystem("foo").setValue("2"); - observation.getCode().addCoding().setCode(code).setSystem("SNOMED-CT"); - observation.setStatus(Enumerations.ObservationStatus.FINAL); - bundle = new Bundle(); - bundle.setType(Bundle.BundleType.TRANSACTION); - bundle.addEntry().setResource(observation).getRequest().setMethod(Bundle.HTTPVerb.PUT).setUrl(obs.getIdElement().toUnqualifiedVersionless().getValue()); - mySystemDao.transaction(null, bundle); - obs = myObservationDao.read(obs.getIdElement().toUnqualifiedVersionless()); - - // Should see 1 subscription notification - waitForQueueToDrain(); - idx++; - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations); - Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(idx)); - Assertions.assertEquals("2", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdElement().getVersionIdPart()); - Assertions.assertEquals("2", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getVersionId()); - Assertions.assertEquals(obs.getMeta().getLastUpdatedElement().getValueAsString(), BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getMeta().getLastUpdatedElement().getValueAsString()); - Assertions.assertEquals("2", BaseSubscriptionsR5Test.ourUpdatedObservations.get(idx).getIdentifierFirstRep().getValue()); - } - - @Test - public void testRepeatedDeliveries() throws Exception { - String payload = "application/fhir+json"; - - String code = "1000000050"; - String criteria1 = "Observation?"; - - createSubscription(criteria1, payload); - waitForActivatedSubscriptionCount(1); - - for (int i = 0; i < 100; i++) { - Observation observation = new Observation(); - observation.getIdentifierFirstRep().setSystem("foo").setValue("ID" + i); - observation.getCode().addCoding().setCode(code).setSystem("SNOMED-CT"); - observation.setStatus(Enumerations.ObservationStatus.FINAL); - myObservationDao.create(observation); - } - - waitForSize(100, BaseSubscriptionsR5Test.ourUpdatedObservations); - } - - @Test - public void testActiveSubscriptionShouldntReActivate() throws Exception { - String criteria = "Observation?code=111111111&_format=xml"; - String payload = "application/fhir+json"; - createSubscription(criteria, payload); - - waitForActivatedSubscriptionCount(1); - for (int i = 0; i < 5; i++) { - int changes = this.mySubscriptionLoader.doSyncSubscriptionsForUnitTest(); - assertEquals(0, changes); - } - } - - @Test - public void testRestHookSubscriptionNoopUpdateDoesntTriggerNewDelivery() throws Exception { - String payload = "application/fhir+json"; - - String code = "1000000050"; - String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - - createSubscription(criteria1, payload); - createSubscription(criteria2, payload); - waitForActivatedSubscriptionCount(2); - - Observation obs = sendObservation(code, "SNOMED-CT"); - - // Should see 1 subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0)); - - // Send an update with no changes - obs.setId(obs.getIdElement().toUnqualifiedVersionless()); - myClient.update().resource(obs).execute(); - - // Should be no further deliveries - Thread.sleep(1000); - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - - - } - - @Test - public void testRestHookSubscriptionApplicationJsonDisableVersionIdInDelivery() throws Exception { - String payload = "application/json"; - - String code = "1000000050"; - String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - - waitForActivatedSubscriptionCount(0); - Subscription subscription1 = createSubscription(criteria1, payload); - waitForActivatedSubscriptionCount(1); - - - ourLog.info("** About to send observation"); - Observation observation1 = sendObservation(code, "SNOMED-CT"); - - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0)); - - IdType idElement = BaseSubscriptionsR5Test.ourUpdatedObservations.get(0).getIdElement(); - assertEquals(observation1.getIdElement().getIdPart(), idElement.getIdPart()); - // VersionId is present - assertEquals(observation1.getIdElement().getVersionIdPart(), idElement.getVersionIdPart()); - - subscription1 - .addExtension(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_STRIP_VERSION_IDS, new BooleanType("true")); - ourLog.info("** About to update subscription"); - - int modCount = myCountingInterceptor.getSentCount("Subscription"); - ourLog.info("** About to send another..."); - myClient.update().resource(subscription1).execute(); - waitForSize(modCount + 2, () -> myCountingInterceptor.getSentCount("Subscription"), () -> myCountingInterceptor.toString()); - - ourLog.info("** About to send observation"); - Observation observation2 = sendObservation(code, "SNOMED-CT"); - - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations); - Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(1)); - - idElement = BaseSubscriptionsR5Test.ourUpdatedObservations.get(1).getIdElement(); - assertEquals(observation2.getIdElement().getIdPart(), idElement.getIdPart()); - // Now VersionId is stripped - assertEquals(null, idElement.getVersionIdPart()); - } - - @Test - public void testRestHookSubscriptionDoesntGetLatestVersionByDefault() throws Exception { - String payload = "application/json"; - - String code = "1000000050"; - String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - - waitForActivatedSubscriptionCount(0); - createSubscription(criteria1, payload); - waitForActivatedSubscriptionCount(1); - - myStoppableSubscriptionDeliveringRestHookSubscriber.pause(); - final CountDownLatch countDownLatch = new CountDownLatch(1); - myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(countDownLatch); - - ourLog.info("** About to send observation"); - Observation observation = sendObservation(code, "SNOMED-CT"); - assertEquals("1", observation.getIdElement().getVersionIdPart()); - assertNull(observation.getNoteFirstRep().getText()); - - observation.getNoteFirstRep().setText("changed"); - MethodOutcome methodOutcome = myClient.update().resource(observation).execute(); - assertEquals("2", methodOutcome.getId().getVersionIdPart()); - assertEquals("changed", observation.getNoteFirstRep().getText()); - - // Wait for our two delivery channel threads to be paused - assertTrue(countDownLatch.await(5L, TimeUnit.SECONDS)); - // Open the floodgates! - myStoppableSubscriptionDeliveringRestHookSubscriber.unPause(); - - - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations); - - Observation observation1 = BaseSubscriptionsR5Test.ourUpdatedObservations.stream().filter(t->t.getIdElement().getVersionIdPart().equals("1")).findFirst().orElseThrow(()->new IllegalStateException()); - Observation observation2 = BaseSubscriptionsR5Test.ourUpdatedObservations.stream().filter(t->t.getIdElement().getVersionIdPart().equals("2")).findFirst().orElseThrow(()->new IllegalStateException()); - - assertEquals("1", observation1.getIdElement().getVersionIdPart()); - assertNull(observation1.getNoteFirstRep().getText()); - assertEquals("2", observation2.getIdElement().getVersionIdPart()); - assertEquals("changed", observation2.getNoteFirstRep().getText()); - } - - @Test - public void testRestHookSubscriptionGetsLatestVersionWithFlag() throws Exception { - String payload = "application/json"; - - String code = "1000000050"; - String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - - waitForActivatedSubscriptionCount(0); - - Subscription subscription = newSubscription(criteria1, payload); - subscription - .addExtension(HapiExtensions.EXT_SUBSCRIPTION_RESTHOOK_DELIVER_LATEST_VERSION, new BooleanType("true")); - myClient.create().resource(subscription).execute(); - - waitForActivatedSubscriptionCount(1); - - myStoppableSubscriptionDeliveringRestHookSubscriber.pause(); - final CountDownLatch countDownLatch = new CountDownLatch(1); - myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(countDownLatch); - - ourLog.info("** About to send observation"); - Observation observation = sendObservation(code, "SNOMED-CT"); - assertEquals("1", observation.getIdElement().getVersionIdPart()); - assertNull(observation.getNoteFirstRep().getText()); - - observation.getNoteFirstRep().setText("changed"); - MethodOutcome methodOutcome = myClient.update().resource(observation).execute(); - assertEquals("2", methodOutcome.getId().getVersionIdPart()); - assertEquals("changed", observation.getNoteFirstRep().getText()); - - // Wait for our two delivery channel threads to be paused - assertTrue(countDownLatch.await(5L, TimeUnit.SECONDS)); - // Open the floodgates! - myStoppableSubscriptionDeliveringRestHookSubscriber.unPause(); - - - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations); - - Observation observation1 = BaseSubscriptionsR5Test.ourUpdatedObservations.get(0); - Observation observation2 = BaseSubscriptionsR5Test.ourUpdatedObservations.get(1); - - assertEquals("2", observation1.getIdElement().getVersionIdPart()); - assertEquals("changed", observation1.getNoteFirstRep().getText()); - assertEquals("2", observation2.getIdElement().getVersionIdPart()); - assertEquals("changed", observation2.getNoteFirstRep().getText()); - } - - @Test - public void testRestHookSubscriptionApplicationJson() throws Exception { - String payload = "application/json"; - - String code = "1000000050"; - String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - - Subscription subscription1 = createSubscription(criteria1, payload); - Subscription subscription2 = createSubscription(criteria2, payload); - waitForActivatedSubscriptionCount(2); - - Observation observation1 = sendObservation(code, "SNOMED-CT"); - - // Should see 1 subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0)); - - Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(0).getIdElement().getVersionIdPart()); - - Subscription subscriptionTemp = myClient.read(Subscription.class, subscription2.getId()); - assertNotNull(subscriptionTemp); - - SubscriptionTopic topic = (SubscriptionTopic) subscriptionTemp.getContained().get(0); - topic.getResourceTriggerFirstRep().getQueryCriteria().setCurrent(criteria1); - - myClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute(); - waitForQueueToDrain(); - - Observation observation2 = sendObservation(code, "SNOMED-CT"); - waitForQueueToDrain(); - - // Should see two subscription notifications - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(3, BaseSubscriptionsR5Test.ourUpdatedObservations); - - myClient.delete().resourceById(new IdType("Subscription/" + subscription2.getId())).execute(); - waitForQueueToDrain(); - - Observation observationTemp3 = sendObservation(code, "SNOMED-CT"); - waitForQueueToDrain(); - - // Should see only one subscription notification - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(4, BaseSubscriptionsR5Test.ourUpdatedObservations); - - Observation observation3 = myClient.read(Observation.class, observationTemp3.getId()); - CodeableConcept codeableConcept = new CodeableConcept(); - observation3.setCode(codeableConcept); - Coding coding = codeableConcept.addCoding(); - coding.setCode(code + "111"); - coding.setSystem("SNOMED-CT"); - myClient.update().resource(observation3).withId(observation3.getIdElement()).execute(); - - // Should see no subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(4, BaseSubscriptionsR5Test.ourUpdatedObservations); - - Observation observation3a = myClient.read(Observation.class, observationTemp3.getId()); - - CodeableConcept codeableConcept1 = new CodeableConcept(); - observation3a.setCode(codeableConcept1); - Coding coding1 = codeableConcept1.addCoding(); - coding1.setCode(code); - coding1.setSystem("SNOMED-CT"); - myClient.update().resource(observation3a).withId(observation3a.getIdElement()).execute(); - - // Should see only one subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(5, BaseSubscriptionsR5Test.ourUpdatedObservations); - - assertFalse(subscription1.getId().equals(subscription2.getId())); - assertFalse(observation1.getId().isEmpty()); - assertFalse(observation2.getId().isEmpty()); - } - - @Test - public void testRestHookSubscriptionApplicationJsonDatabase() throws Exception { - // Same test as above, but now run it using database matching - myStorageSettings.setEnableInMemorySubscriptionMatching(false); - String payload = "application/json"; - - String code = "1000000050"; - String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - - Subscription subscription1 = createSubscription(criteria1, payload); - Subscription subscription2 = createSubscription(criteria2, payload); - waitForActivatedSubscriptionCount(2); - - Observation observation1 = sendObservation(code, "SNOMED-CT"); - - // Should see 1 subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0)); - - Assertions.assertEquals("1", BaseSubscriptionsR5Test.ourUpdatedObservations.get(0).getIdElement().getVersionIdPart()); - - Subscription subscriptionTemp = myClient.read(Subscription.class, subscription2.getId()); - assertNotNull(subscriptionTemp); - - SubscriptionTopic topic = (SubscriptionTopic) subscriptionTemp.getContained().get(0); - topic.getResourceTriggerFirstRep().getQueryCriteria().setCurrent(criteria1); - - myClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute(); - waitForQueueToDrain(); - - Observation observation2 = sendObservation(code, "SNOMED-CT"); - waitForQueueToDrain(); - - // Should see two subscription notifications - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(3, BaseSubscriptionsR5Test.ourUpdatedObservations); - - myClient.delete().resourceById(new IdType("Subscription/" + subscription2.getId())).execute(); - waitForQueueToDrain(); - - Observation observationTemp3 = sendObservation(code, "SNOMED-CT"); - waitForQueueToDrain(); - - // Should see only one subscription notification - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(4, BaseSubscriptionsR5Test.ourUpdatedObservations); - - Observation observation3 = myClient.read(Observation.class, observationTemp3.getId()); - CodeableConcept codeableConcept = new CodeableConcept(); - observation3.setCode(codeableConcept); - Coding coding = codeableConcept.addCoding(); - coding.setCode(code + "111"); - coding.setSystem("SNOMED-CT"); - myClient.update().resource(observation3).withId(observation3.getIdElement()).execute(); - - // Should see no subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(4, BaseSubscriptionsR5Test.ourUpdatedObservations); - - Observation observation3a = myClient.read(Observation.class, observationTemp3.getId()); - - CodeableConcept codeableConcept1 = new CodeableConcept(); - observation3a.setCode(codeableConcept1); - Coding coding1 = codeableConcept1.addCoding(); - coding1.setCode(code); - coding1.setSystem("SNOMED-CT"); - myClient.update().resource(observation3a).withId(observation3a.getIdElement()).execute(); - - // Should see only one subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(5, BaseSubscriptionsR5Test.ourUpdatedObservations); - - assertFalse(subscription1.getId().equals(subscription2.getId())); - assertFalse(observation1.getId().isEmpty()); - assertFalse(observation2.getId().isEmpty()); - } - - @Test - public void testRestHookSubscriptionApplicationXml() throws Exception { - String payload = "application/xml"; - - String code = "1000000050"; - String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - - Subscription subscription1 = createSubscription(criteria1, payload); - Subscription subscription2 = createSubscription(criteria2, payload); - waitForActivatedSubscriptionCount(2); - - ourLog.info("** About to send observation"); - Observation observation1 = sendObservation(code, "SNOMED-CT"); - - // Should see 1 subscription notification - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourContentTypes); - Assertions.assertEquals(Constants.CT_FHIR_XML_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0)); - - Subscription subscriptionTemp = myClient.read(Subscription.class, subscription2.getId()); - assertNotNull(subscriptionTemp); - SubscriptionTopic topic = (SubscriptionTopic) subscriptionTemp.getContained().get(0); - topic.getResourceTriggerFirstRep().getQueryCriteria().setCurrent(criteria1); - - myClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute(); - waitForQueueToDrain(); - - Observation observation2 = sendObservation(code, "SNOMED-CT"); - waitForQueueToDrain(); - - // Should see two subscription notifications - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(3, BaseSubscriptionsR5Test.ourUpdatedObservations); - - myClient.delete().resourceById(new IdType("Subscription/" + subscription2.getId())).execute(); - - Observation observationTemp3 = sendObservation(code, "SNOMED-CT"); - - // Should see only one subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(4, BaseSubscriptionsR5Test.ourUpdatedObservations); - - Observation observation3 = myClient.read(Observation.class, observationTemp3.getId()); - CodeableConcept codeableConcept = new CodeableConcept(); - observation3.setCode(codeableConcept); - Coding coding = codeableConcept.addCoding(); - coding.setCode(code + "111"); - coding.setSystem("SNOMED-CT"); - myClient.update().resource(observation3).withId(observation3.getIdElement()).execute(); - - // Should see no subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(4, BaseSubscriptionsR5Test.ourUpdatedObservations); - - Observation observation3a = myClient.read(Observation.class, observationTemp3.getId()); - - CodeableConcept codeableConcept1 = new CodeableConcept(); - observation3a.setCode(codeableConcept1); - Coding coding1 = codeableConcept1.addCoding(); - coding1.setCode(code); - coding1.setSystem("SNOMED-CT"); - myClient.update().resource(observation3a).withId(observation3a.getIdElement()).execute(); - - // Should see only one subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(5, BaseSubscriptionsR5Test.ourUpdatedObservations); - - assertFalse(subscription1.getId().equals(subscription2.getId())); - assertFalse(observation1.getId().isEmpty()); - assertFalse(observation2.getId().isEmpty()); - } - - @Test - public void testSubscriptionTriggerViaSubscription() throws Exception { - String payload = "application/xml"; - - String code = "1000000050"; - String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - - createSubscription(criteria1, payload); - waitForActivatedSubscriptionCount(1); - - ourLog.info("** About to send observation"); - - Observation observation = new Observation(); - observation.addIdentifier().setSystem("foo").setValue("bar1"); - observation.setId(IdType.newRandomUuid().getValue()); - CodeableConcept codeableConcept = new CodeableConcept() - .addCoding(new Coding().setCode(code).setSystem("SNOMED-CT")); - observation.setCode(codeableConcept); - observation.setStatus(Enumerations.ObservationStatus.FINAL); - - Patient patient = new Patient(); - patient.addIdentifier().setSystem("foo").setValue("bar2"); - patient.setId(IdType.newRandomUuid().getValue()); - patient.setActive(true); - observation.getSubject().setReference(patient.getId()); - - Bundle requestBundle = new Bundle(); - requestBundle.setType(Bundle.BundleType.TRANSACTION); - requestBundle.addEntry() - .setResource(observation) - .setFullUrl(observation.getId()) - .getRequest() - .setUrl("Observation?identifier=foo|bar1") - .setMethod(Bundle.HTTPVerb.PUT); - requestBundle.addEntry() - .setResource(patient) - .setFullUrl(patient.getId()) - .getRequest() - .setUrl("Patient?identifier=foo|bar2") - .setMethod(Bundle.HTTPVerb.PUT); - myClient.transaction().withBundle(requestBundle).execute(); - - // Should see 1 subscription notification - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - Assertions.assertEquals(Constants.CT_FHIR_XML_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0)); - - Observation obs = BaseSubscriptionsR5Test.ourUpdatedObservations.get(0); - ourLog.debug("Observation content: {}", myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(obs)); - } - - @Test - public void testUpdateSubscriptionToMatchLater() throws Exception { - String payload = "application/xml"; - - String code = "1000000050"; - String criteriaBad = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - - ourLog.info("** About to create non-matching subscription"); - - Subscription subscription2 = createSubscription(criteriaBad, payload); - - ourLog.info("** About to send observation that wont match"); - - Observation observation1 = sendObservation(code, "SNOMED-CT"); - - // Criteria didn't match, shouldn't see any updates - waitForQueueToDrain(); - Thread.sleep(1000); - Assertions.assertEquals(0, BaseSubscriptionsR5Test.ourUpdatedObservations.size()); - - Subscription subscriptionTemp = myClient.read().resource(Subscription.class).withId(subscription2.getId()).execute(); - assertNotNull(subscriptionTemp); - String criteriaGood = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - - SubscriptionTopic topic = (SubscriptionTopic) subscriptionTemp.getContained().get(0); - topic.getResourceTriggerFirstRep().getQueryCriteria().setCurrent(criteriaGood); - - ourLog.info("** About to update subscription"); - myClient.update().resource(subscriptionTemp).withId(subscriptionTemp.getIdElement()).execute(); - waitForQueueToDrain(); - - ourLog.info("** About to send Observation 2"); - Observation observation2 = sendObservation(code, "SNOMED-CT"); - waitForQueueToDrain(); - - // Should see a subscription notification this time - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - - myClient.delete().resourceById(new IdType("Subscription/" + subscription2.getId())).execute(); - - Observation observationTemp3 = sendObservation(code, "SNOMED-CT"); - - // No more matches - Thread.sleep(1000); - Assertions.assertEquals(1, BaseSubscriptionsR5Test.ourUpdatedObservations.size()); - } - - @Test - public void testRestHookSubscriptionApplicationXmlJson() throws Exception { - String payload = "application/fhir+xml"; - - String code = "1000000050"; - String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - String criteria2 = "Observation?code=SNOMED-CT|" + code + "111&_format=xml"; - - Subscription subscription1 = createSubscription(criteria1, payload); - Subscription subscription2 = createSubscription(criteria2, payload); - waitForActivatedSubscriptionCount(2); - - Observation observation1 = sendObservation(code, "SNOMED-CT"); - - // Should see 1 subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - Assertions.assertEquals(Constants.CT_FHIR_XML_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0)); - } - - @Test - public void testRestHookSubscriptionInvalidCriteria() throws Exception { - String payload = "application/xml"; - - String criteria1 = "Observation?codeeeee=SNOMED-CT"; - - try { - createSubscription(criteria1, payload); - fail(); - } catch (UnprocessableEntityException e) { - assertEquals("HTTP 422 Unprocessable Entity: " + Msg.code(9) + "Invalid subscription criteria submitted: Observation?codeeeee=SNOMED-CT " + Msg.code(488) + "Failed to parse match URL[Observation?codeeeee=SNOMED-CT] - Resource type Observation does not have a parameter with name: codeeeee", e.getMessage()); - } - } - - @Test - public void testSubscriptionWithHeaders() throws Exception { - String payload = "application/fhir+json"; - - String code = "1000000050"; - String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - - // Add some headers, and we'll also turn back to requested status for fun - Subscription subscription = createSubscription(criteria1, payload); - waitForActivatedSubscriptionCount(1); - - subscription.addHeader("X-Foo: FOO"); - subscription.addHeader("X-Bar: BAR"); - subscription.setStatus(Enumerations.SubscriptionStatusCodes.REQUESTED); - myClient.update().resource(subscription).execute(); - waitForQueueToDrain(); - - sendObservation(code, "SNOMED-CT"); - - // Should see 1 subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - Assertions.assertEquals(Constants.CT_FHIR_JSON_NEW, BaseSubscriptionsR5Test.ourContentTypes.get(0)); - assertThat(BaseSubscriptionsR5Test.ourHeaders, hasItem("X-Foo: FOO")); - assertThat(BaseSubscriptionsR5Test.ourHeaders, hasItem("X-Bar: BAR")); - } - - @Test - public void testDisableSubscription() throws Exception { - String payload = "application/fhir+json"; - - String code = "1000000050"; - String criteria1 = "Observation?code=SNOMED-CT|" + code + "&_format=xml"; - - Subscription subscription = createSubscription(criteria1, payload); - waitForActivatedSubscriptionCount(1); - - sendObservation(code, "SNOMED-CT"); - - // Should see 1 subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - - // Disable - subscription.setStatus(Enumerations.SubscriptionStatusCodes.OFF); - myClient.update().resource(subscription).execute(); - waitForQueueToDrain(); - - // Send another object - sendObservation(code, "SNOMED-CT"); - - // Should see 1 subscription notification - waitForQueueToDrain(); - waitForSize(0, BaseSubscriptionsR5Test.ourCreatedObservations); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - - } - - @Test - public void testInvalidProvenanceParam() { - assertThrows(UnprocessableEntityException.class, () -> { - String payload = "application/fhir+json"; - String criteriabad = "Provenance?foo=http://hl7.org/fhir/v3/DocumentCompletion%7CAU"; - Subscription subscription = newSubscription(criteriabad, payload); - myClient.create().resource(subscription).execute(); - }); - } - - @Test - public void testInvalidProcedureRequestParam() { - assertThrows(UnprocessableEntityException.class, () -> { - String payload = "application/fhir+json"; - String criteriabad = "ProcedureRequest?intent=instance-order&category=Laboratory"; - Subscription subscription = newSubscription(criteriabad, payload); - myClient.create().resource(subscription).execute(); - }); - } - - @Test - public void testInvalidBodySiteParam() { - assertThrows(UnprocessableEntityException.class, () -> { - String payload = "application/fhir+json"; - String criteriabad = "BodySite?accessType=Catheter"; - Subscription subscription = newSubscription(criteriabad, payload); - myClient.create().resource(subscription).execute(); - }); - } - - @Test - public void testGoodSubscriptionPersists() { - assertEquals(0, subscriptionCount()); - String payload = "application/fhir+json"; - String criteriaGood = "Patient?gender=male"; - Subscription subscription = newSubscription(criteriaGood, payload); - myClient.create().resource(subscription).execute(); - assertEquals(1, subscriptionCount()); - } - - private int subscriptionCount() { - IBaseBundle found = myClient.search().forResource(Subscription.class).cacheControl(new CacheControlDirective().setNoCache(true)).execute(); - return toUnqualifiedVersionlessIdValues(found).size(); - } - - @Test - public void testSubscriptionWithNoStatusIsRejected() { - Subscription subscription = newSubscription("Observation?", "application/json"); - subscription.setStatus(null); - - try { - myClient.create().resource(subscription).execute(); - fail(); - } catch (UnprocessableEntityException e) { - assertThat(e.getMessage(), containsString("Can not process submitted Subscription - Subscription.status must be populated on this server")); - } - } - - - @Test - public void testBadSubscriptionDoesntPersist() { - assertEquals(0, subscriptionCount()); - String payload = "application/fhir+json"; - String criteriaBad = "BodySite?accessType=Catheter"; - Subscription subscription = newSubscription(criteriaBad, payload); - try { - myClient.create().resource(subscription).execute(); - } catch (UnprocessableEntityException e) { - ourLog.info("Expected exception", e); - } - assertEquals(0, subscriptionCount()); - } - - @Test - public void testCustomSearchParam() throws Exception { - String criteria = "Observation?accessType=Catheter,PD%20Catheter"; - - SearchParameter sp = new SearchParameter(); - sp.addBase("Observation"); - sp.setCode("accessType"); - sp.setType(Enumerations.SearchParamType.TOKEN); - sp.setExpression("Observation.extension('Observation#accessType')"); - sp.setStatus(Enumerations.PublicationStatus.ACTIVE); - mySearchParameterDao.create(sp); - mySearchParamRegistry.forceRefresh(); - createSubscription(criteria, "application/json"); - waitForActivatedSubscriptionCount(1); - - { - Observation bodySite = new Observation(); - bodySite.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("Catheter")); - MethodOutcome methodOutcome = myClient.create().resource(bodySite).execute(); - assertEquals(true, methodOutcome.getCreated()); - waitForQueueToDrain(); - waitForSize(1, BaseSubscriptionsR5Test.ourUpdatedObservations); - } - { - Observation observation = new Observation(); - observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("PD Catheter")); - MethodOutcome methodOutcome = myClient.create().resource(observation).execute(); - assertEquals(true, methodOutcome.getCreated()); - waitForQueueToDrain(); - waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations); - } - { - Observation observation = new Observation(); - MethodOutcome methodOutcome = myClient.create().resource(observation).execute(); - assertEquals(true, methodOutcome.getCreated()); - waitForQueueToDrain(); - waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations); - } - { - Observation observation = new Observation(); - observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("XXX")); - MethodOutcome methodOutcome = myClient.create().resource(observation).execute(); - assertEquals(true, methodOutcome.getCreated()); - waitForQueueToDrain(); - waitForSize(2, BaseSubscriptionsR5Test.ourUpdatedObservations); - } - - } - - -} diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceModifiedMessage.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceModifiedMessage.java index 45d2915df29..2f7356ab343 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceModifiedMessage.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceModifiedMessage.java @@ -34,6 +34,7 @@ import org.apache.commons.lang3.builder.ToStringBuilder; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.List; @@ -50,6 +51,8 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im protected RequestPartitionId myPartitionId; @JsonIgnore protected transient IBaseResource myPayloadDecoded; + @JsonIgnore + protected transient String myPayloadType; /** * Constructor @@ -126,6 +129,7 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im return retVal; } + @Nullable public IBaseResource getNewPayload(FhirContext theCtx) { if (myPayloadDecoded == null && isNotBlank(myPayload)) { myPayloadDecoded = theCtx.newJsonParser().parseResource(myPayload); @@ -133,6 +137,7 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im return myPayloadDecoded; } + @Nullable public IBaseResource getPayload(FhirContext theCtx) { IBaseResource retVal = myPayloadDecoded; if (retVal == null && isNotBlank(myPayload)) { @@ -143,6 +148,7 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im return retVal; } + @Nonnull public String getPayloadString() { if (this.myPayload != null) { return this.myPayload; @@ -228,5 +234,27 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im return StringUtils.defaultString(super.getMessageKey(), myPayloadId); } + public boolean hasPayloadType(FhirContext theFhirContext, @Nonnull String theResourceName) { + if (myPayloadType == null) { + myPayloadType = getPayloadType(theFhirContext); + } + return theResourceName.equals(myPayloadType); + } + + @Nullable + public String getPayloadType(FhirContext theFhirContext) { + String retval = null; + IIdType payloadId = getPayloadId(theFhirContext); + if (payloadId != null) { + retval = payloadId.getResourceType(); + } + if (isBlank(retval)) { + IBaseResource payload = getNewPayload(theFhirContext); + if (payload != null) { + retval = theFhirContext.getResourceType(payload); + } + } + return retval; + } } 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 245b4f5518e..5d1f0eb2cc0 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 @@ -26,6 +26,7 @@ import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType; +import ca.uhn.fhir.jpa.subscription.model.CanonicalTopicSubscriptionFilter; import ca.uhn.fhir.model.api.BasePrimitive; import ca.uhn.fhir.model.api.ExtensionDt; import ca.uhn.fhir.model.dstu2.resource.Subscription; @@ -327,9 +328,6 @@ public class SubscriptionCanonicalizer { retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode())); } setPartitionIdOnReturnValue(theSubscription, retVal); - retVal.setChannelType(getChannelType(subscription)); - retVal.setCriteriaString(getCriteria(theSubscription)); - retVal.setEndpointUrl(subscription.getChannel().getEndpoint()); retVal.setHeaders(subscription.getChannel().getHeader()); retVal.setChannelExtensions(extractExtension(subscription)); retVal.setIdElement(subscription.getIdElement()); @@ -344,6 +342,18 @@ public class SubscriptionCanonicalizer { } } + if (retVal.isTopicSubscription()) { + retVal.getTopicSubscription().setTopic(getCriteria(theSubscription)); + retVal.getTopicSubscription().setContent(org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE); + retVal.setEndpointUrl(subscription.getChannel().getEndpoint()); + retVal.setChannelType(getChannelType(subscription)); + // WIP STR5 set other topic subscription fields + } else { + retVal.setCriteriaString(getCriteria(theSubscription)); + retVal.setEndpointUrl(subscription.getChannel().getEndpoint()); + retVal.setChannelType(getChannelType(subscription)); + } + if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { String from; String subjectTemplate; @@ -389,18 +399,10 @@ public class SubscriptionCanonicalizer { private CanonicalSubscription canonicalizeR5(IBaseResource theSubscription) { org.hl7.fhir.r5.model.Subscription subscription = (org.hl7.fhir.r5.model.Subscription) theSubscription; - // WIP STR5 now that we have SubscriptionTopic, rewrite this so that all R5 subscriptions are SubscriptionTopic - // subscriptions. This will require major rework of RestHookTestR5Test - CanonicalSubscription retVal = new CanonicalSubscription(); - Enumerations.SubscriptionStatusCodes status = subscription.getStatus(); - if (status != null) { - retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode())); - } + + setPartitionIdOnReturnValue(theSubscription, retVal); - retVal.setChannelType(getChannelType(subscription)); - retVal.setCriteriaString(getCriteria(theSubscription)); - retVal.setEndpointUrl(subscription.getEndpoint()); retVal.setHeaders(subscription.getHeader()); retVal.setChannelExtensions(extractExtension(subscription)); retVal.setIdElement(subscription.getIdElement()); @@ -408,14 +410,40 @@ public class SubscriptionCanonicalizer { retVal.setPayloadSearchCriteria(getExtensionString(subscription, HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA)); retVal.setTags(extractTags(subscription)); - - List profiles = subscription.getMeta().getProfile(); - for (org.hl7.fhir.r5.model.CanonicalType next : profiles) { - if (SubscriptionConstants.SUBSCRIPTION_TOPIC_PROFILE_URL.equals(next.getValueAsString())) { - retVal.setTopicSubscription(true); + List topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics"); + if (topicExts.size() > 0) { + IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive(); + if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) { + throw new PreconditionFailedException(Msg.code(2325) + "Topic reference must be an EventDefinition"); } } + // All R5 subscriptions are topic subscriptions + retVal.setTopicSubscription(true); + + Enumerations.SubscriptionStatusCodes status = subscription.getStatus(); + if (status != null) { + // WIP STR5 do all the codes map? + retVal.setStatus(org.hl7.fhir.r4.model.Subscription.SubscriptionStatus.fromCode(status.toCode())); + } + retVal.getTopicSubscription().setContent(subscription.getContent()); + retVal.setEndpointUrl(subscription.getEndpoint()); + retVal.getTopicSubscription().setTopic(subscription.getTopic()); + retVal.setChannelType(getChannelType(subscription)); + + subscription.getFilterBy().forEach(filter -> { + retVal.getTopicSubscription().addFilter(convertFilter(filter)); + }); + + retVal.getTopicSubscription().setHeartbeatPeriod(subscription.getHeartbeatPeriod()); + retVal.getTopicSubscription().setMaxCount(subscription.getMaxCount()); + + setR5FlagsBasedOnChannelType(subscription, retVal); + + return retVal; + } + + private void setR5FlagsBasedOnChannelType(org.hl7.fhir.r5.model.Subscription subscription, CanonicalSubscription retVal) { if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) { String from; String subjectTemplate; @@ -441,15 +469,16 @@ public class SubscriptionCanonicalizer { retVal.getRestHookDetails().setStripVersionId(Boolean.parseBoolean(stripVersionIds)); retVal.getRestHookDetails().setDeliverLatestVersion(Boolean.parseBoolean(deliverLatestVersion)); } + } - List topicExts = subscription.getExtensionsByUrl("http://hl7.org/fhir/subscription/topics"); - if (topicExts.size() > 0) { - IBaseReference ref = (IBaseReference) topicExts.get(0).getValueAsPrimitive(); - if (!"EventDefinition".equals(ref.getReferenceElement().getResourceType())) { - throw new PreconditionFailedException(Msg.code(2325) + "Topic reference must be an EventDefinition"); - } - } - + private CanonicalTopicSubscriptionFilter convertFilter(org.hl7.fhir.r5.model.Subscription.SubscriptionFilterByComponent theFilter) { + CanonicalTopicSubscriptionFilter retVal = new CanonicalTopicSubscriptionFilter(); + retVal.setResourceType(theFilter.getResourceType()); + retVal.setFilterParameter(theFilter.getFilterParameter()); + retVal.setModifier(theFilter.getModifier()); + // WIP STR5 add this once it's available +// retVal.setComparator(theFilter.getComparator()); + retVal.setValue(theFilter.getValue()); return retVal; } @@ -540,15 +569,6 @@ public class SubscriptionCanonicalizer { retVal = ((org.hl7.fhir.r4b.model.Subscription) theSubscription).getCriteria(); break; case R5: - org.hl7.fhir.r5.model.Subscription subscription = (org.hl7.fhir.r5.model.Subscription) theSubscription; - String topicElement = subscription.getTopicElement().getValue(); - org.hl7.fhir.r5.model.SubscriptionTopic topic = (org.hl7.fhir.r5.model.SubscriptionTopic) subscription.getContained().stream().filter(t -> ("#" + t.getId()).equals(topicElement) || (t.getId()).equals(topicElement)).findFirst().orElse(null); - if (topic == null) { - ourLog.warn("Missing contained subscription topic in R5 subscription"); - return null; - } - retVal = topic.getResourceTriggerFirstRep().getQueryCriteria().getCurrent(); - break; default: throw new IllegalStateException(Msg.code(2327) + "Subscription criteria is not supported for FHIR version: " + myFhirContext.getVersion().getVersion()); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/CanonicalSubscription.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/CanonicalSubscription.java index 4b4d2f9d360..1065f4b05ce 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/CanonicalSubscription.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/CanonicalSubscription.java @@ -60,6 +60,7 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso @JsonProperty("status") private Subscription.SubscriptionStatus myStatus; @JsonProperty("triggerDefinition") + @Deprecated private CanonicalEventDefinition myTrigger; @JsonProperty("emailDetails") private EmailDetails myEmailDetails; @@ -77,8 +78,12 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso private boolean myCrossPartitionEnabled; @JsonProperty("sendDeleteMessages") private boolean mySendDeleteMessages; + @JsonProperty("isTopicSubscription") private boolean myIsTopicSubscription; + @JsonProperty("myTopicSubscription") + private CanonicalTopicSubscription myTopicSubscription; + /** * Constructor */ @@ -94,10 +99,7 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso myPayloadSearchCriteria = thePayloadSearchCriteria; } - /** - * For now we're using the R4 TriggerDefinition, but this - * may change in the future when things stabilize - */ + @Deprecated public void addTrigger(CanonicalEventDefinition theTrigger) { myTrigger = theTrigger; } @@ -251,10 +253,7 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso this.myCrossPartitionEnabled = myCrossPartitionEnabled; } - /** - * For now we're using the R4 triggerdefinition, but this - * may change in the future when things stabilize - */ + @Deprecated public CanonicalEventDefinition getTrigger() { return myTrigger; } @@ -291,6 +290,7 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso b.append(myChannelExtensions, that.myChannelExtensions); b.append(mySendDeleteMessages, that.mySendDeleteMessages); b.append(myPayloadSearchCriteria, that.myPayloadSearchCriteria); + b.append(myTopicSubscription, that.myTopicSubscription); return b.isEquals(); } @@ -361,6 +361,49 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso return myIsTopicSubscription; } + // PayloadString is called ContentType in R5 + public String getContentType() { + assert isTopicSubscription(); + return getPayloadString(); + } + + public CanonicalTopicSubscription getTopicSubscription() { + assert isTopicSubscription(); + if (myTopicSubscription == null) { + myTopicSubscription = new CanonicalTopicSubscription(); + } + return myTopicSubscription; + } + + public void setTopicSubscription(CanonicalTopicSubscription theTopicSubscription) { + myTopicSubscription = theTopicSubscription; + } + + public org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent getContent() { + assert isTopicSubscription(); + return myTopicSubscription.getContent(); + } + + public String getTopic() { + assert isTopicSubscription(); + return myTopicSubscription.getTopic(); + } + + public List getFilters() { + assert isTopicSubscription(); + return myTopicSubscription.getFilters(); + } + + public int getHeartbeatPeriod() { + assert isTopicSubscription(); + return myTopicSubscription.getHeartbeatPeriod(); + } + + public int getMaxCount() { + assert isTopicSubscription(); + return myTopicSubscription.getMaxCount(); + } + public static class EmailDetails implements IModelJson { @JsonProperty("from") @@ -469,11 +512,13 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso } + @Deprecated public static class CanonicalEventDefinition implements IModelJson { /** * Constructor */ + @Deprecated public CanonicalEventDefinition() { // nothing yet } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/CanonicalTopicSubscription.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/CanonicalTopicSubscription.java new file mode 100644 index 00000000000..bacb9de785a --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/CanonicalTopicSubscription.java @@ -0,0 +1,135 @@ +/*- + * #%L + * HAPI FHIR Storage api + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.subscription.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.hl7.fhir.r5.model.Subscription; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CanonicalTopicSubscription { + @JsonProperty("topic") + private String myTopic; + + @JsonProperty("filters") + private List myFilters; + + @JsonProperty("parameters") + private Map myParameters; + + @JsonProperty("heartbeatPeriod") + private Integer myHeartbeatPeriod; + + @JsonProperty("timeout") + private Integer myTimeout; + + @JsonProperty("content") + private Subscription.SubscriptionPayloadContent myContent; + + @JsonProperty("maxCount") + private Integer myMaxCount; + + public String getTopic() { + return myTopic; + } + + public void setTopic(String theTopic) { + myTopic = theTopic; + } + + public List getFilters() { + if (myFilters == null) { + myFilters = new ArrayList<>(); + } + return myFilters; + } + + public void addFilter(CanonicalTopicSubscriptionFilter theFilter) { + getFilters().add(theFilter); + } + + public void setFilters(List theFilters) { + myFilters = theFilters; + } + + public Map getParameters() { + if (myParameters == null) { + myParameters = new HashMap<>(); + } + return myParameters; + } + + public void setParameters(Map theParameters) { + myParameters = theParameters; + } + + public Integer getHeartbeatPeriod() { + return myHeartbeatPeriod; + } + + public void setHeartbeatPeriod(Integer theHeartbeatPeriod) { + myHeartbeatPeriod = theHeartbeatPeriod; + } + + public Integer getTimeout() { + return myTimeout; + } + + public void setTimeout(Integer theTimeout) { + myTimeout = theTimeout; + } + + public Integer getMaxCount() { + return myMaxCount; + } + + public void setMaxCount(Integer theMaxCount) { + myMaxCount = theMaxCount; + } + + public Subscription.SubscriptionPayloadContent getContent() { + return myContent; + } + + public void setContent(Subscription.SubscriptionPayloadContent theContent) { + myContent = theContent; + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + + if (theO == null || getClass() != theO.getClass()) return false; + + CanonicalTopicSubscription that = (CanonicalTopicSubscription) theO; + + return new EqualsBuilder().append(myTopic, that.myTopic).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37).append(myTopic).toHashCode(); + } +} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/CanonicalTopicSubscriptionFilter.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/CanonicalTopicSubscriptionFilter.java new file mode 100644 index 00000000000..fc4d76fbae1 --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/CanonicalTopicSubscriptionFilter.java @@ -0,0 +1,82 @@ +/*- + * #%L + * HAPI FHIR Storage api + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.subscription.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.hl7.fhir.r5.model.Enumerations; +import org.hl7.fhir.r5.model.SearchParameter; + +public class CanonicalTopicSubscriptionFilter { + @JsonProperty("resourceType") + String myResourceType; + + @JsonProperty("filterParameter") + String myFilterParameter; + + + @JsonProperty("comparator") + SearchParameter.SearchComparator myComparator; + + @JsonProperty("modifier") + Enumerations.SubscriptionSearchModifier myModifier; + + @JsonProperty("value") + String myValue; + + public String getResourceType() { + return myResourceType; + } + + public void setResourceType(String theResourceType) { + myResourceType = theResourceType; + } + + public String getFilterParameter() { + return myFilterParameter; + } + + public void setFilterParameter(String theFilterParameter) { + myFilterParameter = theFilterParameter; + } + + public SearchParameter.SearchComparator getComparator() { + return myComparator; + } + + public void setComparator(SearchParameter.SearchComparator theComparator) { + myComparator = theComparator; + } + + public Enumerations.SubscriptionSearchModifier getModifier() { + return myModifier; + } + + public void setModifier(Enumerations.SubscriptionSearchModifier theModifier) { + myModifier = theModifier; + } + + public String getValue() { + return myValue; + } + + public void setValue(String theValue) { + myValue = theValue; + } +} 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 eefeaa782b2..ed22155f87b 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 @@ -2,20 +2,29 @@ package ca.uhn.fhir.jpa.subscription.match.registry; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; +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 org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Subscription; +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 static ca.uhn.fhir.rest.api.Constants.CT_FHIR_JSON_NEW; import static ca.uhn.fhir.util.HapiExtensions.EX_SEND_DELETE_MESSAGES; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class SubscriptionCanonicalizerTest { + private static final String TEST_TOPIC = "http://test.topic"; FhirContext r4Context = FhirContext.forR4(); private final SubscriptionCanonicalizer testedSC = new SubscriptionCanonicalizer(r4Context); @@ -64,4 +73,62 @@ class SubscriptionCanonicalizerTest { CanonicalSubscription canonicalize = dstu2Canonicalizer.canonicalize(dstu2Sub); assertTrue(canonicalize.getSendDeleteMessages()); } + + @Test + public void testR5() { + // setup + SubscriptionCanonicalizer r5Canonicalizer = new SubscriptionCanonicalizer(FhirContext.forR5()); + 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.setEndpoint("http://foo"); + subscription.setTopic(TEST_TOPIC); + Coding channelType = new Coding().setSystem("http://terminology.hl7.org/CodeSystem/subscription-channel-type").setCode("rest-hook"); + subscription.setChannelType(channelType); + subscription.addFilterBy(buildFilter("Observation", "param1", "value1")); + subscription.addFilterBy(buildFilter("CarePlan", "param2", "value2")); + subscription.setHeartbeatPeriod(123); + subscription.setMaxCount(456); + + // execute + CanonicalSubscription canonicalize = r5Canonicalizer.canonicalize(subscription); + + // verify + assertEquals(Subscription.SubscriptionStatus.ACTIVE, canonicalize.getStatus()); + assertEquals(CT_FHIR_JSON_NEW, canonicalize.getContentType()); + assertEquals(org.hl7.fhir.r5.model.Subscription.SubscriptionPayloadContent.FULLRESOURCE, canonicalize.getContent()); + assertEquals("http://foo", canonicalize.getEndpointUrl()); + assertEquals(TEST_TOPIC, canonicalize.getTopic()); + assertEquals(CanonicalSubscriptionChannelType.RESTHOOK, canonicalize.getChannelType()); + assertThat(canonicalize.getFilters(), hasSize(2)); + + CanonicalTopicSubscriptionFilter filter1 = canonicalize.getFilters().get(0); + assertEquals("Observation", filter1.getResourceType()); + assertEquals("param1", filter1.getFilterParameter()); + // WIP STR5 assert comparator once core libs are updated + assertEquals(Enumerations.SubscriptionSearchModifier.EQUAL, filter1.getModifier()); + assertEquals("value1", filter1.getValue()); + + CanonicalTopicSubscriptionFilter filter2 = canonicalize.getFilters().get(1); + assertEquals("CarePlan", filter2.getResourceType()); + assertEquals("param2", filter2.getFilterParameter()); + // WIP STR5 assert comparator once core libs are updated + assertEquals(Enumerations.SubscriptionSearchModifier.EQUAL, filter1.getModifier()); + assertEquals("value2", filter2.getValue()); + assertEquals(123, canonicalize.getHeartbeatPeriod()); + assertEquals(456, canonicalize.getMaxCount()); + } + + @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(); + filter.setResourceType(theResourceType); + filter.setFilterParameter(theParam); + filter.setModifier(Enumerations.SubscriptionSearchModifier.EQUAL); + // WIP STR5 add comparator once core libs are updated + filter.setValue(theValue); + return filter; + } } diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/concurrency/PointcutLatch.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/concurrency/PointcutLatch.java index 386205b2a16..931f3257506 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/concurrency/PointcutLatch.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/test/concurrency/PointcutLatch.java @@ -43,6 +43,7 @@ import java.util.stream.Collectors; // This class is primarily used for testing. public class PointcutLatch implements IAnonymousInterceptor, IPointcutLatch { private static final Logger ourLog = LoggerFactory.getLogger(PointcutLatch.class); + private static final int DEFAULT_TIMEOUT_SECONDS = 10; private static final FhirObjectPrinter ourFhirObjectToStringMapper = new FhirObjectPrinter();