Allow search criteria as subscription delivery mechanism (#1951)
* Terser should create correct Enumeration on create * Start work * Work on subscriptions * Work on seed bundles * Bundle transmission * Add changelog * Test fix * Fix LGTM warning
This commit is contained in:
parent
cd69a140fc
commit
fa4bbe3685
|
@ -9,6 +9,7 @@ import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||||
import ca.uhn.fhir.model.primitive.IdDt;
|
import ca.uhn.fhir.model.primitive.IdDt;
|
||||||
import ca.uhn.fhir.model.primitive.IntegerDt;
|
import ca.uhn.fhir.model.primitive.IntegerDt;
|
||||||
import ca.uhn.fhir.rest.annotation.IdParam;
|
import ca.uhn.fhir.rest.annotation.IdParam;
|
||||||
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
import ca.uhn.fhir.rest.api.QualifiedParamList;
|
import ca.uhn.fhir.rest.api.QualifiedParamList;
|
||||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||||
import ca.uhn.fhir.rest.param.binder.QueryParameterAndBinder;
|
import ca.uhn.fhir.rest.param.binder.QueryParameterAndBinder;
|
||||||
|
@ -386,4 +387,10 @@ public class ParameterUtil {
|
||||||
return b.toString();
|
return b.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the value is :iterate or :recurse (the former name of :iterate) for an _include parameter
|
||||||
|
*/
|
||||||
|
public static boolean isIncludeIterate(String theQualifier) {
|
||||||
|
return Constants.PARAM_INCLUDE_QUALIFIER_RECURSE.equals(theQualifier) || Constants.PARAM_INCLUDE_QUALIFIER_ITERATE.equals(theQualifier);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
package ca.uhn.fhir.util;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
|
||||||
|
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBase;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||||
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
|
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||||
|
import org.thymeleaf.util.Validate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class can be used to build a Bundle resource to be used as a FHIR transaction.
|
||||||
|
*
|
||||||
|
* This is not yet complete, and doesn't support all FHIR features. <b>USE WITH CAUTION</b> as the API
|
||||||
|
* may change.
|
||||||
|
*
|
||||||
|
* @since 5.1.0
|
||||||
|
*/
|
||||||
|
public class TransactionBuilder {
|
||||||
|
|
||||||
|
private final FhirContext myContext;
|
||||||
|
private final IBaseBundle myBundle;
|
||||||
|
private final RuntimeResourceDefinition myBundleDef;
|
||||||
|
private final BaseRuntimeChildDefinition myEntryChild;
|
||||||
|
private final BaseRuntimeElementDefinition<?> myEntryDef;
|
||||||
|
private final BaseRuntimeChildDefinition myEntryResourceChild;
|
||||||
|
private final BaseRuntimeChildDefinition myEntryFullUrlChild;
|
||||||
|
private final BaseRuntimeChildDefinition myEntryRequestChild;
|
||||||
|
private final BaseRuntimeElementDefinition<?> myEntryRequestDef;
|
||||||
|
private final BaseRuntimeChildDefinition myEntryRequestUrlChild;
|
||||||
|
private final BaseRuntimeChildDefinition myEntryRequestMethodChild;
|
||||||
|
private final BaseRuntimeElementDefinition<?> myEntryRequestMethodDef;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public TransactionBuilder(FhirContext theContext) {
|
||||||
|
myContext = theContext;
|
||||||
|
|
||||||
|
myBundleDef = myContext.getResourceDefinition("Bundle");
|
||||||
|
myBundle = (IBaseBundle) myBundleDef.newInstance();
|
||||||
|
|
||||||
|
BaseRuntimeChildDefinition typeChild = myBundleDef.getChildByName("type");
|
||||||
|
IPrimitiveType<?> type = (IPrimitiveType<?>) typeChild.getChildByName("type").newInstance(typeChild.getInstanceConstructorArguments());
|
||||||
|
type.setValueAsString("transaction");
|
||||||
|
typeChild.getMutator().setValue(myBundle, type);
|
||||||
|
|
||||||
|
myEntryChild = myBundleDef.getChildByName("entry");
|
||||||
|
myEntryDef = myEntryChild.getChildByName("entry");
|
||||||
|
|
||||||
|
myEntryResourceChild = myEntryDef.getChildByName("resource");
|
||||||
|
myEntryFullUrlChild = myEntryDef.getChildByName("fullUrl");
|
||||||
|
|
||||||
|
myEntryRequestChild = myEntryDef.getChildByName("request");
|
||||||
|
myEntryRequestDef = myEntryRequestChild.getChildByName("request");
|
||||||
|
|
||||||
|
myEntryRequestUrlChild = myEntryRequestDef.getChildByName("url");
|
||||||
|
|
||||||
|
myEntryRequestMethodChild = myEntryRequestDef.getChildByName("method");
|
||||||
|
myEntryRequestMethodDef = myEntryRequestMethodChild.getChildByName("method");
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an entry containing an update (PUT) request
|
||||||
|
*
|
||||||
|
* @param theResource The resource to update
|
||||||
|
*/
|
||||||
|
public TransactionBuilder addUpdateEntry(IBaseResource theResource) {
|
||||||
|
Validate.notNull(theResource, "theResource must not be null");
|
||||||
|
Validate.notEmpty(theResource.getIdElement().getValue(), "theResource must have an ID");
|
||||||
|
|
||||||
|
IBase entry = myEntryDef.newInstance();
|
||||||
|
myEntryChild.getMutator().addValue(myBundle, entry);
|
||||||
|
|
||||||
|
// Bundle.entry.fullUrl
|
||||||
|
IPrimitiveType<?> fullUrl = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance();
|
||||||
|
fullUrl.setValueAsString(theResource.getIdElement().getValue());
|
||||||
|
myEntryFullUrlChild.getMutator().setValue(entry, fullUrl);
|
||||||
|
|
||||||
|
// Bundle.entry.resource
|
||||||
|
myEntryResourceChild.getMutator().setValue(entry, theResource);
|
||||||
|
|
||||||
|
// Bundle.entry.request
|
||||||
|
IBase request = myEntryRequestDef.newInstance();
|
||||||
|
myEntryRequestChild.getMutator().setValue(entry, request);
|
||||||
|
|
||||||
|
// Bundle.entry.request.url
|
||||||
|
IPrimitiveType<?> url = (IPrimitiveType<?>) myContext.getElementDefinition("uri").newInstance();
|
||||||
|
url.setValueAsString(theResource.getIdElement().toUnqualifiedVersionless().getValue());
|
||||||
|
myEntryRequestUrlChild.getMutator().setValue(request, url);
|
||||||
|
|
||||||
|
// Bundle.entry.request.url
|
||||||
|
IPrimitiveType<?> method = (IPrimitiveType<?>) myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments());
|
||||||
|
method.setValueAsString("PUT");
|
||||||
|
myEntryRequestMethodChild.getMutator().setValue(request, method);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public IBaseBundle getBundle() {
|
||||||
|
return myBundle;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
type: add
|
||||||
|
issue: 1951
|
||||||
|
title: "REST Hook subscriptions may now specify a search expression to be used to fetch a collection of
|
||||||
|
resources to deliver when a subscription matches."
|
|
@ -7,13 +7,15 @@ import ca.uhn.fhir.jpa.subscription.channel.impl.LinkedBlockingChannel;
|
||||||
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor;
|
import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor;
|
||||||
import ca.uhn.fhir.rest.annotation.Create;
|
import ca.uhn.fhir.rest.annotation.Create;
|
||||||
import ca.uhn.fhir.rest.annotation.ResourceParam;
|
import ca.uhn.fhir.rest.annotation.ResourceParam;
|
||||||
|
import ca.uhn.fhir.rest.annotation.Transaction;
|
||||||
|
import ca.uhn.fhir.rest.annotation.TransactionParam;
|
||||||
import ca.uhn.fhir.rest.annotation.Update;
|
import ca.uhn.fhir.rest.annotation.Update;
|
||||||
import ca.uhn.fhir.rest.api.Constants;
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
import ca.uhn.fhir.rest.api.MethodOutcome;
|
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||||
import ca.uhn.fhir.util.BundleUtil;
|
|
||||||
import ca.uhn.fhir.test.utilities.JettyUtil;
|
import ca.uhn.fhir.test.utilities.JettyUtil;
|
||||||
|
import ca.uhn.fhir.util.BundleUtil;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import net.ttddyy.dsproxy.QueryCount;
|
import net.ttddyy.dsproxy.QueryCount;
|
||||||
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
|
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
|
||||||
|
@ -22,9 +24,19 @@ import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||||
import org.eclipse.jetty.servlet.ServletHolder;
|
import org.eclipse.jetty.servlet.ServletHolder;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.hl7.fhir.r4.model.*;
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
import org.junit.*;
|
import org.hl7.fhir.r4.model.CodeableConcept;
|
||||||
|
import org.hl7.fhir.r4.model.Coding;
|
||||||
|
import org.hl7.fhir.r4.model.IdType;
|
||||||
|
import org.hl7.fhir.r4.model.Observation;
|
||||||
|
import org.hl7.fhir.r4.model.Subscription;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Ignore;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
@ -36,28 +48,23 @@ import java.util.List;
|
||||||
@Ignore
|
@Ignore
|
||||||
public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test {
|
public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test {
|
||||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseSubscriptionsR4Test.class);
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseSubscriptionsR4Test.class);
|
||||||
|
|
||||||
private static Server ourListenerServer;
|
|
||||||
protected static int ourListenerPort;
|
protected static int ourListenerPort;
|
||||||
protected static List<String> ourContentTypes = Collections.synchronizedList(new ArrayList<>());
|
protected static List<String> ourContentTypes = Collections.synchronizedList(new ArrayList<>());
|
||||||
protected static List<String> ourHeaders = Collections.synchronizedList(new ArrayList<>());
|
protected static List<String> ourHeaders = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
protected static List<Bundle> ourTransactions = Collections.synchronizedList(Lists.newArrayList());
|
||||||
|
protected static List<Observation> ourCreatedObservations = Collections.synchronizedList(Lists.newArrayList());
|
||||||
|
protected static List<Observation> ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList());
|
||||||
|
private static Server ourListenerServer;
|
||||||
private static SingleQueryCountHolder ourCountHolder;
|
private static SingleQueryCountHolder ourCountHolder;
|
||||||
|
private static String ourListenerServerBase;
|
||||||
@Autowired
|
|
||||||
private SingleQueryCountHolder myCountHolder;
|
|
||||||
@Autowired
|
@Autowired
|
||||||
protected SubscriptionTestUtil mySubscriptionTestUtil;
|
protected SubscriptionTestUtil mySubscriptionTestUtil;
|
||||||
@Autowired
|
@Autowired
|
||||||
protected SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor;
|
protected SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor;
|
||||||
|
|
||||||
protected CountingInterceptor myCountingInterceptor;
|
protected CountingInterceptor myCountingInterceptor;
|
||||||
|
|
||||||
protected static List<Observation> ourCreatedObservations = Collections.synchronizedList(Lists.newArrayList());
|
|
||||||
protected static List<Observation> ourUpdatedObservations = Collections.synchronizedList(Lists.newArrayList());
|
|
||||||
private static String ourListenerServerBase;
|
|
||||||
|
|
||||||
protected List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>());
|
protected List<IIdType> mySubscriptionIds = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
@Autowired
|
||||||
|
private SingleQueryCountHolder myCountHolder;
|
||||||
|
|
||||||
@After
|
@After
|
||||||
public void afterUnregisterRestHookListener() {
|
public void afterUnregisterRestHookListener() {
|
||||||
|
@ -87,6 +94,7 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test
|
||||||
public void beforeReset() throws Exception {
|
public void beforeReset() throws Exception {
|
||||||
ourCreatedObservations.clear();
|
ourCreatedObservations.clear();
|
||||||
ourUpdatedObservations.clear();
|
ourUpdatedObservations.clear();
|
||||||
|
ourTransactions.clear();
|
||||||
ourContentTypes.clear();
|
ourContentTypes.clear();
|
||||||
ourHeaders.clear();
|
ourHeaders.clear();
|
||||||
|
|
||||||
|
@ -162,8 +170,7 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class ObservationResourceProvider implements IResourceProvider {
|
||||||
public static class ObservationListener implements IResourceProvider {
|
|
||||||
|
|
||||||
@Create
|
@Create
|
||||||
public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) {
|
public MethodOutcome create(@ResourceParam Observation theObservation, HttpServletRequest theRequest) {
|
||||||
|
@ -202,6 +209,17 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class PlainProvider {
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
public Bundle transaction(@TransactionParam Bundle theInput) {
|
||||||
|
ourLog.info("Received transaction update");
|
||||||
|
ourTransactions.add(theInput);
|
||||||
|
return theInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@AfterClass
|
@AfterClass
|
||||||
public static void reportTotalSelects() {
|
public static void reportTotalSelects() {
|
||||||
ourLog.info("Total database select queries: {}", getQueryCount().getSelect());
|
ourLog.info("Total database select queries: {}", getQueryCount().getSelect());
|
||||||
|
@ -215,8 +233,8 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test
|
||||||
public static void startListenerServer() throws Exception {
|
public static void startListenerServer() throws Exception {
|
||||||
RestfulServer ourListenerRestServer = new RestfulServer(FhirContext.forR4());
|
RestfulServer ourListenerRestServer = new RestfulServer(FhirContext.forR4());
|
||||||
|
|
||||||
ObservationListener obsListener = new ObservationListener();
|
ourListenerRestServer.registerProvider(new ObservationResourceProvider());
|
||||||
ourListenerRestServer.setResourceProviders(obsListener);
|
ourListenerRestServer.registerProvider(new PlainProvider());
|
||||||
|
|
||||||
ourListenerServer = new Server(0);
|
ourListenerServer = new Server(0);
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,8 @@ public class SubscriptionValidatingInterceptorTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testValidate_RestHook_NoEndpoint() {
|
public void testValidate_RestHook_NoEndpoint() {
|
||||||
|
when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true);
|
||||||
|
|
||||||
Subscription subscription = new Subscription();
|
Subscription subscription = new Subscription();
|
||||||
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
|
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
|
||||||
subscription.setCriteria("Patient?identifier=foo");
|
subscription.setCriteria("Patient?identifier=foo");
|
||||||
|
@ -102,6 +104,8 @@ public class SubscriptionValidatingInterceptorTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testValidate_RestHook_NoType() {
|
public void testValidate_RestHook_NoType() {
|
||||||
|
when(myDaoRegistry.isResourceTypeSupported(eq("Patient"))).thenReturn(true);
|
||||||
|
|
||||||
Subscription subscription = new Subscription();
|
Subscription subscription = new Subscription();
|
||||||
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
|
subscription.setStatus(Subscription.SubscriptionStatus.ACTIVE);
|
||||||
subscription.setCriteria("Patient?identifier=foo");
|
subscription.setCriteria("Patient?identifier=foo");
|
||||||
|
|
|
@ -1013,4 +1013,39 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDeliverSearchResult() throws Exception {
|
||||||
|
{
|
||||||
|
Subscription subscription = newSubscription("Observation?", "application/json");
|
||||||
|
subscription.addExtension(JpaConstants.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_RESULT, new StringType("Observation?_id=${matched_resource_id}&_include=*"));
|
||||||
|
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(subscription));
|
||||||
|
MethodOutcome methodOutcome = ourClient.create().resource(subscription).execute();
|
||||||
|
mySubscriptionIds.add(methodOutcome.getId());
|
||||||
|
waitForActivatedSubscriptionCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Patient patient = new Patient();
|
||||||
|
patient.setActive(true);
|
||||||
|
IIdType patientId = ourClient.create().resource(patient).execute().getId();
|
||||||
|
|
||||||
|
Observation observation = new Observation();
|
||||||
|
observation.addExtension().setUrl("Observation#accessType").setValue(new Coding().setCode("Catheter"));
|
||||||
|
observation.getSubject().setReferenceElement(patientId.toUnqualifiedVersionless());
|
||||||
|
MethodOutcome methodOutcome = ourClient.create().resource(observation).execute();
|
||||||
|
assertEquals(true, methodOutcome.getCreated());
|
||||||
|
|
||||||
|
waitForQueueToDrain();
|
||||||
|
waitForSize(1, ourTransactions);
|
||||||
|
|
||||||
|
ourLog.info("Received transaction: {}", myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(ourTransactions.get(0)));
|
||||||
|
|
||||||
|
Bundle xact = ourTransactions.get(0);
|
||||||
|
assertEquals(2, xact.getEntry().size());
|
||||||
|
|
||||||
|
ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(ourTransactions.get(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -214,6 +214,10 @@ public class JpaConstants {
|
||||||
* Extension ID for external binary references
|
* Extension ID for external binary references
|
||||||
*/
|
*/
|
||||||
public static final String EXT_EXTERNALIZED_BINARY_ID = "http://hapifhir.io/fhir/StructureDefinition/externalized-binary-id";
|
public static final String EXT_EXTERNALIZED_BINARY_ID = "http://hapifhir.io/fhir/StructureDefinition/externalized-binary-id";
|
||||||
|
/**
|
||||||
|
* For subscription, deliver a bundle containing a search result instead of just a single resource
|
||||||
|
*/
|
||||||
|
public static final String EXT_SUBSCRIPTION_PAYLOAD_SEARCH_RESULT = "http://hapifhir.io/fhir/StructureDefinition/subscription-payload-search-result";
|
||||||
/**
|
/**
|
||||||
* Placed in system-generated extensions
|
* Placed in system-generated extensions
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -26,6 +26,7 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
|
||||||
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
|
||||||
import ca.uhn.fhir.model.api.IQueryParameterAnd;
|
import ca.uhn.fhir.model.api.IQueryParameterAnd;
|
||||||
import ca.uhn.fhir.model.api.IQueryParameterType;
|
import ca.uhn.fhir.model.api.IQueryParameterType;
|
||||||
|
import ca.uhn.fhir.model.api.Include;
|
||||||
import ca.uhn.fhir.rest.api.Constants;
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
import ca.uhn.fhir.rest.api.QualifiedParamList;
|
import ca.uhn.fhir.rest.api.QualifiedParamList;
|
||||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||||
|
@ -50,7 +51,7 @@ public class MatchUrlService {
|
||||||
@Autowired
|
@Autowired
|
||||||
private ISearchParamRegistry mySearchParamRegistry;
|
private ISearchParamRegistry mySearchParamRegistry;
|
||||||
|
|
||||||
public SearchParameterMap translateMatchUrl(String theMatchUrl, RuntimeResourceDefinition theResourceDefinition) {
|
public SearchParameterMap translateMatchUrl(String theMatchUrl, RuntimeResourceDefinition theResourceDefinition, Flag... theFlags) {
|
||||||
SearchParameterMap paramMap = new SearchParameterMap();
|
SearchParameterMap paramMap = new SearchParameterMap();
|
||||||
List<NameValuePair> parameters = translateMatchUrl(theMatchUrl);
|
List<NameValuePair> parameters = translateMatchUrl(theMatchUrl);
|
||||||
|
|
||||||
|
@ -79,6 +80,13 @@ public class MatchUrlService {
|
||||||
|
|
||||||
for (String nextParamName : nameToParamLists.keySet()) {
|
for (String nextParamName : nameToParamLists.keySet()) {
|
||||||
List<QualifiedParamList> paramList = nameToParamLists.get(nextParamName);
|
List<QualifiedParamList> paramList = nameToParamLists.get(nextParamName);
|
||||||
|
|
||||||
|
if (theFlags != null && theFlags.length > 0) {
|
||||||
|
for (Flag next : theFlags) {
|
||||||
|
next.process(nextParamName, paramList, paramMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Constants.PARAM_LASTUPDATED.equals(nextParamName)) {
|
if (Constants.PARAM_LASTUPDATED.equals(nextParamName)) {
|
||||||
if (paramList != null && paramList.size() > 0) {
|
if (paramList != null && paramList.size() > 0) {
|
||||||
if (paramList.size() > 2) {
|
if (paramList.size() > 2) {
|
||||||
|
@ -131,8 +139,8 @@ public class MatchUrlService {
|
||||||
return UrlUtil.translateMatchUrl(theMatchUrl);
|
return UrlUtil.translateMatchUrl(theMatchUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IQueryParameterAnd newInstanceAnd(String theParamType) {
|
private IQueryParameterAnd<?> newInstanceAnd(String theParamType) {
|
||||||
Class<? extends IQueryParameterAnd> clazz = ResourceMetaParams.RESOURCE_META_AND_PARAMS.get(theParamType);
|
Class<? extends IQueryParameterAnd<?>> clazz = ResourceMetaParams.RESOURCE_META_AND_PARAMS.get(theParamType);
|
||||||
return ReflectionUtil.newInstance(clazz);
|
return ReflectionUtil.newInstance(clazz);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,4 +148,44 @@ public class MatchUrlService {
|
||||||
Class<? extends IQueryParameterType> clazz = ResourceMetaParams.RESOURCE_META_PARAMS.get(theParamType);
|
Class<? extends IQueryParameterType> clazz = ResourceMetaParams.RESOURCE_META_PARAMS.get(theParamType);
|
||||||
return ReflectionUtil.newInstance(clazz);
|
return ReflectionUtil.newInstance(clazz);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract static class Flag {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
Flag() {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract void process(String theParamName, List<QualifiedParamList> theValues, SearchParameterMap theMapToPopulate);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the parser should process _include and _revinclude (by default these are not handled)
|
||||||
|
*/
|
||||||
|
public static Flag processIncludes() {
|
||||||
|
return new Flag() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void process(String theParamName, List<QualifiedParamList> theValues, SearchParameterMap theMapToPopulate) {
|
||||||
|
if (Constants.PARAM_INCLUDE.equals(theParamName)) {
|
||||||
|
for (QualifiedParamList nextQualifiedList : theValues) {
|
||||||
|
for (String nextValue : nextQualifiedList) {
|
||||||
|
theMapToPopulate.addInclude(new Include(nextValue, ParameterUtil.isIncludeIterate(nextQualifiedList.getQualifier())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (Constants.PARAM_REVINCLUDE.equals(theParamName)) {
|
||||||
|
for (QualifiedParamList nextQualifiedList : theValues) {
|
||||||
|
for (String nextValue : nextQualifiedList) {
|
||||||
|
theMapToPopulate.addInclude(new Include(nextValue, ParameterUtil.isIncludeIterate(nextQualifiedList.getQualifier())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,7 @@ public class SubscriptionDeliveringMessageSubscriber extends BaseSubscriptionDel
|
||||||
public void handleMessage(ResourceDeliveryMessage theMessage) throws MessagingException, URISyntaxException {
|
public void handleMessage(ResourceDeliveryMessage theMessage) throws MessagingException, URISyntaxException {
|
||||||
CanonicalSubscription subscription = theMessage.getSubscription();
|
CanonicalSubscription subscription = theMessage.getSubscription();
|
||||||
|
|
||||||
// Interceptor call: SUBSCRIPTION_BEFORE_REST_HOOK_DELIVERY
|
// Interceptor call: SUBSCRIPTION_BEFORE_MESSAGE_DELIVERY
|
||||||
HookParams params = new HookParams()
|
HookParams params = new HookParams()
|
||||||
.add(CanonicalSubscription.class, subscription)
|
.add(CanonicalSubscription.class, subscription)
|
||||||
.add(ResourceDeliveryMessage.class, theMessage);
|
.add(ResourceDeliveryMessage.class, theMessage);
|
||||||
|
@ -104,7 +104,7 @@ public class SubscriptionDeliveringMessageSubscriber extends BaseSubscriptionDel
|
||||||
|
|
||||||
deliverPayload(theMessage, subscription, channelProducer);
|
deliverPayload(theMessage, subscription, channelProducer);
|
||||||
|
|
||||||
// Interceptor call: SUBSCRIPTION_AFTER_REST_HOOK_DELIVERY
|
// Interceptor call: SUBSCRIPTION_AFTER_MESSAGE_DELIVERY
|
||||||
params = new HookParams()
|
params = new HookParams()
|
||||||
.add(CanonicalSubscription.class, subscription)
|
.add(CanonicalSubscription.class, subscription)
|
||||||
.add(ResourceDeliveryMessage.class, theMessage);
|
.add(ResourceDeliveryMessage.class, theMessage);
|
||||||
|
|
|
@ -25,11 +25,14 @@ import ca.uhn.fhir.interceptor.api.HookParams;
|
||||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||||
|
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||||
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
import ca.uhn.fhir.jpa.subscription.match.deliver.BaseSubscriptionDeliverySubscriber;
|
import ca.uhn.fhir.jpa.subscription.match.deliver.BaseSubscriptionDeliverySubscriber;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription;
|
||||||
import ca.uhn.fhir.jpa.subscription.model.ResourceDeliveryMessage;
|
import ca.uhn.fhir.jpa.subscription.model.ResourceDeliveryMessage;
|
||||||
import ca.uhn.fhir.rest.api.EncodingEnum;
|
import ca.uhn.fhir.rest.api.EncodingEnum;
|
||||||
import ca.uhn.fhir.rest.api.RequestTypeEnum;
|
import ca.uhn.fhir.rest.api.RequestTypeEnum;
|
||||||
|
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||||
import ca.uhn.fhir.rest.client.api.Header;
|
import ca.uhn.fhir.rest.client.api.Header;
|
||||||
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||||
import ca.uhn.fhir.rest.client.api.IHttpClient;
|
import ca.uhn.fhir.rest.client.api.IHttpClient;
|
||||||
|
@ -40,7 +43,9 @@ import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor;
|
||||||
import ca.uhn.fhir.rest.gclient.IClientExecutable;
|
import ca.uhn.fhir.rest.gclient.IClientExecutable;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||||
|
import ca.uhn.fhir.util.TransactionBuilder;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.commons.text.StringSubstitutor;
|
||||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -49,6 +54,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
import org.springframework.messaging.MessagingException;
|
import org.springframework.messaging.MessagingException;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -65,6 +71,9 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
|
||||||
@Autowired
|
@Autowired
|
||||||
private DaoRegistry myDaoRegistry;
|
private DaoRegistry myDaoRegistry;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MatchUrlService myMatchUrlService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*/
|
*/
|
||||||
|
@ -81,41 +90,23 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
|
||||||
|
|
||||||
protected void doDelivery(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription, EncodingEnum thePayloadType, IGenericClient theClient, IBaseResource thePayloadResource) {
|
protected void doDelivery(ResourceDeliveryMessage theMsg, CanonicalSubscription theSubscription, EncodingEnum thePayloadType, IGenericClient theClient, IBaseResource thePayloadResource) {
|
||||||
IClientExecutable<?, ?> operation;
|
IClientExecutable<?, ?> operation;
|
||||||
switch (theMsg.getOperationType()) {
|
|
||||||
case CREATE:
|
if (isNotBlank(theSubscription.getPayloadSearchResult())) {
|
||||||
case UPDATE:
|
operation = createDeliveryRequestTransaction(theSubscription, theClient, thePayloadResource);
|
||||||
if (thePayloadResource == null || thePayloadResource.isEmpty()) {
|
} else if (thePayloadType != null) {
|
||||||
if (thePayloadType != null) {
|
operation = createDeliveryRequestNormal(theMsg, theClient, thePayloadResource);
|
||||||
operation = theClient.create().resource(thePayloadResource);
|
|
||||||
} else {
|
} else {
|
||||||
sendNotification(theMsg);
|
sendNotification(theMsg);
|
||||||
return;
|
operation = null;
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (thePayloadType != null) {
|
|
||||||
operation = theClient.update().resource(thePayloadResource);
|
|
||||||
} else {
|
|
||||||
sendNotification(theMsg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case DELETE:
|
|
||||||
operation = theClient.delete().resourceById(theMsg.getPayloadId(myFhirContext));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
ourLog.warn("Ignoring delivery message of type: {}", theMsg.getOperationType());
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (operation != null) {
|
||||||
|
|
||||||
if (thePayloadType != null) {
|
if (thePayloadType != null) {
|
||||||
operation.encoded(thePayloadType);
|
operation.encoded(thePayloadType);
|
||||||
}
|
}
|
||||||
|
|
||||||
String payloadId = null;
|
String payloadId = thePayloadResource.getIdElement().toUnqualified().getValue();
|
||||||
if (thePayloadResource != null) {
|
|
||||||
payloadId = thePayloadResource.getIdElement().toUnqualified().getValue();
|
|
||||||
}
|
|
||||||
ourLog.info("Delivering {} rest-hook payload {} for {}", theMsg.getOperationType(), payloadId, theSubscription.getIdElement(myFhirContext).toUnqualifiedVersionless().getValue());
|
ourLog.info("Delivering {} rest-hook payload {} for {}", theMsg.getOperationType(), payloadId, theSubscription.getIdElement(myFhirContext).toUnqualifiedVersionless().getValue());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -125,11 +116,56 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
|
||||||
ourLog.error("Exception: ", e);
|
ourLog.error("Exception: ", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private IClientExecutable<?, ?> createDeliveryRequestNormal(ResourceDeliveryMessage theMsg, IGenericClient theClient, IBaseResource thePayloadResource) {
|
||||||
|
IClientExecutable<?, ?> operation;
|
||||||
|
switch (theMsg.getOperationType()) {
|
||||||
|
case CREATE:
|
||||||
|
case UPDATE:
|
||||||
|
operation = theClient.update().resource(thePayloadResource);
|
||||||
|
break;
|
||||||
|
case DELETE:
|
||||||
|
operation = theClient.delete().resourceById(theMsg.getPayloadId(myFhirContext));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ourLog.warn("Ignoring delivery message of type: {}", theMsg.getOperationType());
|
||||||
|
operation = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IClientExecutable<?, ?> createDeliveryRequestTransaction(CanonicalSubscription theSubscription, IGenericClient theClient, IBaseResource thePayloadResource) {
|
||||||
|
IClientExecutable<?, ?> operation;
|
||||||
|
String resType = theSubscription.getPayloadSearchResult().substring(0, theSubscription.getPayloadSearchResult().indexOf('?'));
|
||||||
|
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resType);
|
||||||
|
RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(resType);
|
||||||
|
|
||||||
|
String payloadUrl = theSubscription.getPayloadSearchResult();
|
||||||
|
Map<String, String> valueMap = new HashMap<>(1);
|
||||||
|
valueMap.put("matched_resource_id", thePayloadResource.getIdElement().toUnqualifiedVersionless().getValue());
|
||||||
|
payloadUrl = new StringSubstitutor(valueMap).replace(payloadUrl);
|
||||||
|
SearchParameterMap payloadSearchMap = myMatchUrlService.translateMatchUrl(payloadUrl, resourceDefinition, MatchUrlService.processIncludes());
|
||||||
|
payloadSearchMap.setLoadSynchronous(true);
|
||||||
|
|
||||||
|
IBundleProvider searchResults = dao.search(payloadSearchMap);
|
||||||
|
|
||||||
|
TransactionBuilder builder = new TransactionBuilder(myFhirContext);
|
||||||
|
for (IBaseResource next : searchResults.getResources(0, searchResults.size())) {
|
||||||
|
builder.addUpdateEntry(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
operation = theClient.transaction().withBundle(builder.getBundle());
|
||||||
|
return operation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IBaseResource getResource(IIdType payloadId) throws ResourceGoneException {
|
public IBaseResource getResource(IIdType payloadId) throws ResourceGoneException {
|
||||||
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(payloadId.getResourceType());
|
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(payloadId.getResourceType());
|
||||||
IFhirResourceDao dao = myDaoRegistry.getResourceDao(resourceDef.getImplementingClass());
|
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceDef.getImplementingClass());
|
||||||
return dao.read(payloadId.toVersionless());
|
return dao.read(payloadId.toVersionless());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,8 +273,7 @@ public class SubscriptionDeliveringRestHookSubscriber extends BaseSubscriptionDe
|
||||||
// close connection in order to return a possible cached connection to the connection pool
|
// close connection in order to return a possible cached connection to the connection pool
|
||||||
response.close();
|
response.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
ourLog.error("Error trying to reach " + theMsg.getSubscription().getEndpointUrl());
|
ourLog.error("Error trying to reach {}: {}", theMsg.getSubscription().getEndpointUrl(), e.toString());
|
||||||
e.printStackTrace();
|
|
||||||
throw new ResourceNotFoundException(e.getMessage());
|
throw new ResourceNotFoundException(e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,6 +113,7 @@ public class SubscriptionCanonicalizer {
|
||||||
retVal.setChannelExtensions(extractExtension(subscription));
|
retVal.setChannelExtensions(extractExtension(subscription));
|
||||||
retVal.setIdElement(subscription.getIdElement());
|
retVal.setIdElement(subscription.getIdElement());
|
||||||
retVal.setPayloadString(subscription.getChannel().getPayload());
|
retVal.setPayloadString(subscription.getChannel().getPayload());
|
||||||
|
retVal.setPayloadSearchResult(getExtensionString(subscription, JpaConstants.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_RESULT));
|
||||||
|
|
||||||
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
|
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
|
||||||
String from;
|
String from;
|
||||||
|
@ -208,6 +209,7 @@ public class SubscriptionCanonicalizer {
|
||||||
retVal.setChannelExtensions(extractExtension(subscription));
|
retVal.setChannelExtensions(extractExtension(subscription));
|
||||||
retVal.setIdElement(subscription.getIdElement());
|
retVal.setIdElement(subscription.getIdElement());
|
||||||
retVal.setPayloadString(subscription.getChannel().getPayload());
|
retVal.setPayloadString(subscription.getChannel().getPayload());
|
||||||
|
retVal.setPayloadSearchResult(getExtensionString(subscription, JpaConstants.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_RESULT));
|
||||||
|
|
||||||
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
|
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
|
||||||
String from;
|
String from;
|
||||||
|
@ -261,6 +263,7 @@ public class SubscriptionCanonicalizer {
|
||||||
retVal.setChannelExtensions(extractExtension(subscription));
|
retVal.setChannelExtensions(extractExtension(subscription));
|
||||||
retVal.setIdElement(subscription.getIdElement());
|
retVal.setIdElement(subscription.getIdElement());
|
||||||
retVal.setPayloadString(subscription.getContentType());
|
retVal.setPayloadString(subscription.getContentType());
|
||||||
|
retVal.setPayloadSearchResult(getExtensionString(subscription, JpaConstants.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_RESULT));
|
||||||
|
|
||||||
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
|
if (retVal.getChannelType() == CanonicalSubscriptionChannelType.EMAIL) {
|
||||||
String from;
|
String from;
|
||||||
|
|
|
@ -34,7 +34,11 @@ import org.hl7.fhir.r4.model.Subscription;
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||||
|
|
||||||
|
@ -64,6 +68,8 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
||||||
private RestHookDetails myRestHookDetails;
|
private RestHookDetails myRestHookDetails;
|
||||||
@JsonProperty("extensions")
|
@JsonProperty("extensions")
|
||||||
private Map<String, List<String>> myChannelExtensions;
|
private Map<String, List<String>> myChannelExtensions;
|
||||||
|
@JsonProperty("payloadSearchResult")
|
||||||
|
private String myPayloadSearchResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
|
@ -72,6 +78,14 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getPayloadSearchResult() {
|
||||||
|
return myPayloadSearchResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPayloadSearchResult(String thePayloadSearchResult) {
|
||||||
|
myPayloadSearchResult = thePayloadSearchResult;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For now we're using the R4 TriggerDefinition, but this
|
* For now we're using the R4 TriggerDefinition, but this
|
||||||
* may change in the future when things stabilize
|
* may change in the future when things stabilize
|
||||||
|
@ -80,7 +94,6 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
||||||
myTrigger = theTrigger;
|
myTrigger = theTrigger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public CanonicalSubscriptionChannelType getChannelType() {
|
public CanonicalSubscriptionChannelType getChannelType() {
|
||||||
return myChannelType;
|
return myChannelType;
|
||||||
}
|
}
|
||||||
|
@ -136,7 +149,7 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
||||||
public String getChannelExtension(String theUrl) {
|
public String getChannelExtension(String theUrl) {
|
||||||
String retVal = null;
|
String retVal = null;
|
||||||
List<String> strings = myChannelExtensions.get(theUrl);
|
List<String> strings = myChannelExtensions.get(theUrl);
|
||||||
if (strings != null && strings.isEmpty()==false) {
|
if (strings != null && strings.isEmpty() == false) {
|
||||||
retVal = strings.get(0);
|
retVal = strings.get(0);
|
||||||
}
|
}
|
||||||
return retVal;
|
return retVal;
|
||||||
|
@ -276,6 +289,23 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return new ToStringBuilder(this)
|
||||||
|
.append("myIdElement", myIdElement)
|
||||||
|
.append("myStatus", myStatus)
|
||||||
|
.append("myCriteriaString", myCriteriaString)
|
||||||
|
.append("myEndpointUrl", myEndpointUrl)
|
||||||
|
.append("myPayloadString", myPayloadString)
|
||||||
|
// .append("myHeaders", myHeaders)
|
||||||
|
.append("myChannelType", myChannelType)
|
||||||
|
// .append("myTrigger", myTrigger)
|
||||||
|
// .append("myEmailDetails", myEmailDetails)
|
||||||
|
// .append("myRestHookDetails", myRestHookDetails)
|
||||||
|
// .append("myChannelExtensions", myChannelExtensions)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
public static class EmailDetails implements IModelJson {
|
public static class EmailDetails implements IModelJson {
|
||||||
|
|
||||||
@JsonProperty("from")
|
@JsonProperty("from")
|
||||||
|
@ -394,21 +424,4 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return new ToStringBuilder(this)
|
|
||||||
.append("myIdElement", myIdElement)
|
|
||||||
.append("myStatus", myStatus)
|
|
||||||
.append("myCriteriaString", myCriteriaString)
|
|
||||||
.append("myEndpointUrl", myEndpointUrl)
|
|
||||||
.append("myPayloadString", myPayloadString)
|
|
||||||
// .append("myHeaders", myHeaders)
|
|
||||||
.append("myChannelType", myChannelType)
|
|
||||||
// .append("myTrigger", myTrigger)
|
|
||||||
// .append("myEmailDetails", myEmailDetails)
|
|
||||||
// .append("myRestHookDetails", myRestHookDetails)
|
|
||||||
// .append("myChannelExtensions", myChannelExtensions)
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import ca.uhn.fhir.interceptor.api.Hook;
|
||||||
import ca.uhn.fhir.interceptor.api.Interceptor;
|
import ca.uhn.fhir.interceptor.api.Interceptor;
|
||||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
import ca.uhn.fhir.interceptor.api.Pointcut;
|
||||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||||
|
import ca.uhn.fhir.jpa.model.util.JpaConstants;
|
||||||
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
|
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.matching.SubscriptionStrategyEvaluator;
|
||||||
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer;
|
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionCanonicalizer;
|
||||||
|
@ -96,32 +97,19 @@ public class SubscriptionValidatingInterceptor {
|
||||||
|
|
||||||
if (!finished) {
|
if (!finished) {
|
||||||
|
|
||||||
String query = subscription.getCriteriaString();
|
validateQuery(subscription.getCriteriaString(), "Subscription.criteria");
|
||||||
if (isBlank(query)) {
|
|
||||||
throw new UnprocessableEntityException("Subscription.criteria must be populated");
|
|
||||||
}
|
|
||||||
|
|
||||||
int sep = query.indexOf('?');
|
if (subscription.getPayloadSearchResult() != null) {
|
||||||
if (sep <= 1) {
|
validateQuery(subscription.getPayloadSearchResult(), "Subscription.extension(url='" + JpaConstants.EXT_SUBSCRIPTION_PAYLOAD_SEARCH_RESULT + "')");
|
||||||
throw new UnprocessableEntityException("Subscription.criteria must be in the form \"{Resource Type}?[params]\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
String resType = query.substring(0, sep);
|
|
||||||
if (resType.contains("/")) {
|
|
||||||
throw new UnprocessableEntityException("Subscription.criteria must be in the form \"{Resource Type}?[params]\"");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
validateChannelType(subscription);
|
validateChannelType(subscription);
|
||||||
|
|
||||||
if (!myDaoRegistry.isResourceTypeSupported(resType)) {
|
|
||||||
throw new UnprocessableEntityException("Subscription.criteria contains invalid/unsupported resource type: " + resType);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
SubscriptionMatchingStrategy strategy = mySubscriptionStrategyEvaluator.determineStrategy(query);
|
SubscriptionMatchingStrategy strategy = mySubscriptionStrategyEvaluator.determineStrategy(subscription.getCriteriaString());
|
||||||
mySubscriptionCanonicalizer.setMatchingStrategyTag(theSubscription, strategy);
|
mySubscriptionCanonicalizer.setMatchingStrategyTag(theSubscription, strategy);
|
||||||
} catch (InvalidRequestException | DataFormatException e) {
|
} catch (InvalidRequestException | DataFormatException e) {
|
||||||
throw new UnprocessableEntityException("Invalid subscription criteria submitted: " + query + " " + e.getMessage());
|
throw new UnprocessableEntityException("Invalid subscription criteria submitted: " + subscription.getCriteriaString() + " " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subscription.getChannelType() == null) {
|
if (subscription.getChannelType() == null) {
|
||||||
|
@ -129,6 +117,28 @@ public class SubscriptionValidatingInterceptor {
|
||||||
} else if (subscription.getChannelType() == CanonicalSubscriptionChannelType.MESSAGE) {
|
} else if (subscription.getChannelType() == CanonicalSubscriptionChannelType.MESSAGE) {
|
||||||
validateMessageSubscriptionEndpoint(subscription.getEndpointUrl());
|
validateMessageSubscriptionEndpoint(subscription.getEndpointUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateQuery(String theQuery, String theFieldName) {
|
||||||
|
if (isBlank(theQuery)) {
|
||||||
|
throw new UnprocessableEntityException(theFieldName + " must be populated");
|
||||||
|
}
|
||||||
|
|
||||||
|
int sep = theQuery.indexOf('?');
|
||||||
|
if (sep <= 1) {
|
||||||
|
throw new UnprocessableEntityException(theFieldName + " must be in the form \"{Resource Type}?[params]\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
String resType = theQuery.substring(0, sep);
|
||||||
|
if (resType.contains("/")) {
|
||||||
|
throw new UnprocessableEntityException(theFieldName + " must be in the form \"{Resource Type}?[params]\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!myDaoRegistry.isResourceTypeSupported(resType)) {
|
||||||
|
throw new UnprocessableEntityException(theFieldName + " contains invalid/unsupported resource type: " + resType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,14 +20,6 @@ package ca.uhn.fhir.rest.server.method;
|
||||||
* #L%
|
* #L%
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
|
|
||||||
import ca.uhn.fhir.context.ConfigurationException;
|
import ca.uhn.fhir.context.ConfigurationException;
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import ca.uhn.fhir.model.api.Include;
|
import ca.uhn.fhir.model.api.Include;
|
||||||
|
@ -35,9 +27,18 @@ import ca.uhn.fhir.rest.annotation.IncludeParam;
|
||||||
import ca.uhn.fhir.rest.api.Constants;
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
import ca.uhn.fhir.rest.api.QualifiedParamList;
|
import ca.uhn.fhir.rest.api.QualifiedParamList;
|
||||||
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
|
||||||
|
import ca.uhn.fhir.rest.param.ParameterUtil;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
class IncludeParameter extends BaseQueryParameter {
|
class IncludeParameter extends BaseQueryParameter {
|
||||||
|
|
||||||
private Set<String> myAllow;
|
private Set<String> myAllow;
|
||||||
|
@ -142,7 +143,7 @@ class IncludeParameter extends BaseQueryParameter {
|
||||||
}
|
}
|
||||||
|
|
||||||
String qualifier = nextParamList.getQualifier();
|
String qualifier = nextParamList.getQualifier();
|
||||||
boolean recurse = Constants.PARAM_INCLUDE_QUALIFIER_RECURSE.equals(qualifier) || Constants.PARAM_INCLUDE_QUALIFIER_ITERATE.equals(qualifier);
|
boolean iterate = ParameterUtil.isIncludeIterate(qualifier);
|
||||||
|
|
||||||
String value = nextParamList.get(0);
|
String value = nextParamList.get(0);
|
||||||
if (myAllow != null && !myAllow.isEmpty()) {
|
if (myAllow != null && !myAllow.isEmpty()) {
|
||||||
|
@ -157,10 +158,10 @@ class IncludeParameter extends BaseQueryParameter {
|
||||||
if (mySpecType == String.class) {
|
if (mySpecType == String.class) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
return new Include(value, recurse);
|
return new Include(value, iterate);
|
||||||
}
|
}
|
||||||
|
|
||||||
retValCollection.add(new Include(value, recurse));
|
retValCollection.add(new Include(value, iterate));
|
||||||
}
|
}
|
||||||
|
|
||||||
return retValCollection;
|
return retValCollection;
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package ca.uhn.fhir.util;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
|
import org.hl7.fhir.r4.model.Patient;
|
||||||
|
import org.hl7.fhir.r4.model.codesystems.HttpVerb;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
|
||||||
|
public class TransactionBuilderTest {
|
||||||
|
private static final Logger ourLog = LoggerFactory.getLogger(TransactionBuilderTest.class);
|
||||||
|
private FhirContext myFhirContext = FhirContext.forR4();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAddEntryUpdate() {
|
||||||
|
TransactionBuilder builder = new TransactionBuilder(myFhirContext);
|
||||||
|
|
||||||
|
Patient patient = new Patient();
|
||||||
|
patient.setId("http://foo/Patient/123");
|
||||||
|
patient.setActive(true);
|
||||||
|
builder.addUpdateEntry(patient);
|
||||||
|
|
||||||
|
Bundle bundle = (Bundle) builder.getBundle();
|
||||||
|
ourLog.info("Bundle:\n{}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle));
|
||||||
|
|
||||||
|
assertEquals(Bundle.BundleType.TRANSACTION, bundle.getType());
|
||||||
|
assertEquals(1, bundle.getEntry().size());
|
||||||
|
assertSame(patient, bundle.getEntry().get(0).getResource());
|
||||||
|
assertEquals("http://foo/Patient/123", bundle.getEntry().get(0).getFullUrl());
|
||||||
|
assertEquals("Patient/123", bundle.getEntry().get(0).getRequest().getUrl());
|
||||||
|
assertEquals(Bundle.HTTPVerb.PUT, bundle.getEntry().get(0).getRequest().getMethod());
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue