Skip to content

Commit

Permalink
improve validation for R4 backport subscriptions in SubscriptionValid…
Browse files Browse the repository at this point in the history
…atingInterceptor (#5973)

* still need to genericise the rest of the SubscriptionValidatingInterceptorTest tests
also there are Msg.code FIXMEs

* cleanup
still need to genericise the rest of the SubscriptionValidatingInterceptorTest tests

* cleanup
still need to genericise the rest of the SubscriptionValidatingInterceptorTest tests

* done

* done

* javadoc

* changelog

* fix test

* fix test

* fix test

* fix test

* fix test
  • Loading branch information
fil512 committed May 29, 2024
1 parent 4ac65da commit ac3a5e2
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 70 deletions.
29 changes: 29 additions & 0 deletions hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,21 @@ public static void setField(
mergeFields(theTerser, theResource, childDefinition, theFromFieldValues, theToFieldValues);
}

/**
* Sets the provided field with the given values. This method will add to the collection of existing field values
* in case of multiple cardinality. Use {@link #clearField(FhirContext, IBaseResource, String)}
* to remove values before setting
*
* @param theFhirContext Context holding resource definition
* @param theFieldName Child field name of the resource to set
* @param theResource The resource to set the values on
* @param theValue The String value to set on the resource child field name. This value is converted to the appropriate primitive type before the value is set
*/
public static void setStringField(
FhirContext theFhirContext, String theFieldName, IBaseResource theResource, String theValue) {
setField(theFhirContext, theFieldName, theResource, theFhirContext.newPrimitiveString(theValue));
}

/**
* Sets the specified value at the FHIR path provided.
*
Expand Down Expand Up @@ -493,6 +508,20 @@ public static void setFieldByFhirPath(
setFieldByFhirPath(theFhirContext.newTerser(), theFhirPath, theResource, theValue);
}

/**
* Sets the specified String value at the FHIR path provided.
*
* @param theFhirContext Context holding resource definition
* @param theFhirPath The FHIR path to set the field at
* @param theResource The resource on which the value should be set
* @param theValue The String value to set. The string is converted to the appropriate primitive type before setting the field
*/
public static void setStringFieldByFhirPath(
FhirContext theFhirContext, String theFhirPath, IBaseResource theResource, String theValue) {
setFieldByFhirPath(
theFhirContext.newTerser(), theFhirPath, theResource, theFhirContext.newPrimitiveString(theValue));
}

/**
* Returns field values ant the specified FHIR path from the resource.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
type: fix
issue: 5973
title: "Previously, subscription criteria on R4 backport subscriptions were not validated by the
SubscriptionValidatingInterceptor. This has been corrected."
Original file line number Diff line number Diff line change
Expand Up @@ -46,25 +46,28 @@
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.subscription.SubscriptionConstants;
import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.SubscriptionUtil;
import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.Subscription;
import org.hl7.fhir.r5.model.SubscriptionTopic;
import org.springframework.beans.factory.annotation.Autowired;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import static org.apache.commons.lang3.StringUtils.isBlank;

@Interceptor
public class SubscriptionValidatingInterceptor {

@Autowired
private SubscriptionCanonicalizer mySubscriptionCanonicalizer;

@Autowired
private DaoRegistry myDaoRegistry;

Expand All @@ -74,6 +77,9 @@ public class SubscriptionValidatingInterceptor {
@Autowired
private SubscriptionStrategyEvaluator mySubscriptionStrategyEvaluator;

@Autowired
private SubscriptionCanonicalizer mySubscriptionCanonicalizer;

private FhirContext myFhirContext;

@Autowired
Expand Down Expand Up @@ -150,27 +156,13 @@ void validateSubmittedSubscription(

if (!finished) {

if (subscription.isTopicSubscription()) {
if (myFhirContext.getVersion().getVersion()
!= FhirVersionEnum
.R4) { // In R4 topic subscriptions exist without a corresponidng SubscriptionTopic
// resource
Optional<IBaseResource> oTopic = findSubscriptionTopicByUrl(subscription.getTopic());
if (!oTopic.isPresent()) {
throw new UnprocessableEntityException(
Msg.code(2322) + "No SubscriptionTopic exists with topic: " + subscription.getTopic());
}
}
} else {
validateQuery(subscription.getCriteriaString(), "Subscription.criteria");

if (subscription.getPayloadSearchCriteria() != null) {
validateQuery(
subscription.getPayloadSearchCriteria(),
"Subscription.extension(url='" + HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA
+ "')");
}
if (subscription.getPayloadSearchCriteria() != null) {
validateQuery(
subscription.getPayloadSearchCriteria(),
"Subscription.extension(url='" + HapiExtensions.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_CRITERIA
+ "')");
}
validateCriteria(theSubscription, subscription);

validateChannelType(subscription);

Expand All @@ -197,6 +189,52 @@ void validateSubmittedSubscription(
}
}

private void validateCriteria(IBaseResource theSubscription, CanonicalSubscription theCanonicalSubscription) {
if (theCanonicalSubscription.isTopicSubscription()) {
if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.R4) {
validateR4BackportSubscription((Subscription) theSubscription);
} else {
validateR5PlusTopicSubscription(theCanonicalSubscription);
}
} else {
validateQuery(theCanonicalSubscription.getCriteriaString(), "Subscription.criteria");
}
}

private void validateR5PlusTopicSubscription(CanonicalSubscription theCanonicalSubscription) {
Optional<IBaseResource> oTopic = findSubscriptionTopicByUrl(theCanonicalSubscription.getTopic());
if (!oTopic.isPresent()) {
throw new UnprocessableEntityException(
Msg.code(2322) + "No SubscriptionTopic exists with topic: " + theCanonicalSubscription.getTopic());
}
}

private void validateR4BackportSubscription(Subscription theSubscription) {
// This is an R4 backport topic subscription
// In R4, topic subscriptions exist without a corresponding SubscriptionTopic
Subscription r4Subscription = theSubscription;
List<String> filterUrls = new ArrayList<>();
List<Extension> filterUrlExtensions = r4Subscription
.getCriteriaElement()
.getExtensionsByUrl(SubscriptionConstants.SUBSCRIPTION_TOPIC_FILTER_URL);
filterUrlExtensions.forEach(filterUrlExtension -> {
StringType filterUrlElement = (StringType) filterUrlExtension.getValue();
if (filterUrlElement != null) {
filterUrls.add(filterUrlElement.getValue());
}
});
if (filterUrls.isEmpty()) {
// Trigger a "no criteria" validation exception
validateQuery(
null,
"Subscription.criteria.extension with url " + SubscriptionConstants.SUBSCRIPTION_TOPIC_FILTER_URL);
} else {
filterUrls.forEach(filterUrl -> validateQuery(
filterUrl,
"Subscription.criteria.extension with url " + SubscriptionConstants.SUBSCRIPTION_TOPIC_FILTER_URL));
}
}

protected void validatePermissions(
IBaseResource theSubscription,
CanonicalSubscription theCanonicalSubscription,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package ca.uhn.fhir.jpa.subscription.submit.interceptor;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
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;
import ca.uhn.fhir.subscription.SubscriptionConstants;
import ca.uhn.fhir.util.TerserUtil;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseEnumeration;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Version independent utility class for setting fields on subscriptions
*/

public final class SubscriptionUtil {
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionUtil.class);

private SubscriptionUtil() {}

public static void setStatus(FhirContext theFhirContext, IBaseResource theSubscription, String theStatus) {
IBaseEnumeration newValue = switch (theFhirContext.getVersion().getVersion()) {
case DSTU3 -> new org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatusEnumFactory().fromType(new org.hl7.fhir.dstu3.model.StringType(theStatus));
case R4 -> new org.hl7.fhir.r4.model.Subscription.SubscriptionStatusEnumFactory().fromType(new org.hl7.fhir.r4.model.StringType(theStatus));
case R4B -> new org.hl7.fhir.r4b.model.Enumerations.SubscriptionStatusEnumFactory().fromType(new org.hl7.fhir.r4b.model.StringType(theStatus));
case R5 -> new org.hl7.fhir.r5.model.Enumerations.SubscriptionStatusCodesEnumFactory().fromType(new org.hl7.fhir.r5.model.StringType(theStatus));
default -> null;
};
TerserUtil.setField(theFhirContext, "status", theSubscription, newValue);
}

public static void setCriteria(FhirContext theFhirContext, IBaseResource theSubscription, String theCriteria) {
SubscriptionCanonicalizer canonicalizer = new SubscriptionCanonicalizer(theFhirContext);
CanonicalSubscription canonicalSubscription = canonicalizer.canonicalize(theSubscription);
if (canonicalSubscription.isTopicSubscription()) {
if (theFhirContext.getVersion().getVersion() == FhirVersionEnum.R5) {
// Nothing to do on R5
return;
}
Subscription subscription = (Subscription)theSubscription;
subscription.getCriteriaElement().addExtension(SubscriptionConstants.SUBSCRIPTION_TOPIC_FILTER_URL, new StringType(theCriteria));
} else {
TerserUtil.setStringField(theFhirContext, "criteria", theSubscription, theCriteria);
}
}

public static void setChannelType(FhirContext theFhirContext, IBaseResource theSubscription, String theChannelType) {
FhirVersionEnum version = theFhirContext.getVersion().getVersion();
IBase newValue = switch (version) {
case DSTU3 -> new org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelTypeEnumFactory().fromType(new org.hl7.fhir.dstu3.model.StringType(theChannelType));
case R4 -> new org.hl7.fhir.r4.model.Subscription.SubscriptionChannelTypeEnumFactory().fromType(new org.hl7.fhir.r4.model.StringType(theChannelType));
case R4B -> new org.hl7.fhir.r4b.model.Subscription.SubscriptionChannelTypeEnumFactory().fromType(new org.hl7.fhir.r4b.model.StringType(theChannelType));
case R5 -> CanonicalSubscriptionChannelType.valueOf(theChannelType.toUpperCase()).toR5Coding();
default -> null;
};
String fhirPath = switch(version) {
case DSTU3 -> "channel.type";
case R4 -> "channel.type";
case R4B -> "channel.type";
case R5 -> "channelType";
default -> null;
};
TerserUtil.setFieldByFhirPath(theFhirContext, fhirPath, theSubscription, newValue);
}

public static void setEndpoint(FhirContext theFhirContext, IBaseResource theSubscription, String theEndpoint) {
FhirVersionEnum version = theFhirContext.getVersion().getVersion();
String fhirPath = switch(version) {
case DSTU3 -> "channel.endpoint";
case R4 -> "channel.endpoint";
case R4B -> "channel.endpoint";
case R5 -> "endpoint";
default -> null;
};
TerserUtil.setStringFieldByFhirPath(theFhirContext, fhirPath, theSubscription, theEndpoint);
}
}
Loading

0 comments on commit ac3a5e2

Please sign in to comment.