cds hooks allow arbitrary strings for extensions (#6026)

* changes to context booter and basecdsservicejson

* cleanup CdsHooksContextBooter

* javadocs

* change JsonNode to CdsHooksExtension

* failing test for service extensions...

* fix extension serialization

* fix extension serialization

* failing test for request extensions not getting parsed...

* try adding a custom deserializer for extension

* try adding a custom deserializer for extension

* try adding a custom deserializer for extension

* wire up CdsServiceRegistryImpl

* fix wiring object mapper

* merge master

* get hook in deserializer

* create CdsServiceRequestJsonDeserializer..

* spotless

* fix for cds service feedback for  CdsHooksControllerTest.java

* spotless

* cleanup...

* spotless...

* docs

* enable tests for CdsHooksContextBooterTest

* more cleanup...

* refactor CdsServiceRequestJsonDeserializer

* apply suggestion

* spotless....

* fix checkstyle...

* spotless

* split custom extension classes into its own package for tests

* add changelog

* Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6023-fix-allowing-arbitrary-json-for-cds-hooks-extensions.yaml

Co-authored-by: Ken Stevens <khstevens@gmail.com>

* Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_4_0/6023-fix-allowing-arbitrary-json-for-cds-hooks-extensions.yaml

Co-authored-by: Ken Stevens <khstevens@gmail.com>

* Update hapi-fhir-server-cds-hooks/src/main/java/ca/uhn/hapi/fhir/cdshooks/api/json/CdsHooksExtension.java

Co-authored-by: Ken Stevens <khstevens@gmail.com>

* fix comments

* rename

* fix the comment

* spotless

* version bump to 7.3.9-SNAPSHOT

---------

Co-authored-by: Ken Stevens <khstevens@gmail.com>
This commit is contained in:
Aditya Dave 2024-07-09 09:45:21 -04:00 committed by GitHub
parent 3ffb695b6b
commit ce0160e7f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
99 changed files with 838 additions and 241 deletions

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-bom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<packaging>pom</packaging>
<name>HAPI FHIR BOM</name>
@ -12,7 +12,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -0,0 +1,4 @@
---
type: change
issue: 6024
title: "Previously, CDS hook extensions needed to be encoded as strings. This has been changed so that extensions are now properly provided as inline JSON."

View File

@ -11,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -3,7 +3,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -3,7 +3,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -19,6 +19,8 @@
*/
package ca.uhn.hapi.fhir.cdshooks.api;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsHooksExtension;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -72,4 +74,6 @@ public @interface CdsService {
* An arbitrary string which will be used to store stringify JSON
*/
String extension() default "";
Class<? extends CdsHooksExtension> extensionClass() default CdsHooksExtension.class;
}

View File

@ -54,7 +54,7 @@ public interface ICdsServiceRegistry {
* @param theCdsServiceFeedbackJson the request
* @return the response
*/
String callFeedback(String theServiceId, CdsServiceFeedbackJson theCdsServiceFeedbackJson);
CdsServiceFeedbackJson callFeedback(String theServiceId, CdsServiceFeedbackJson theCdsServiceFeedbackJson);
/**
* Register a new CDS Service with the endpoint.
@ -86,4 +86,12 @@ public interface ICdsServiceRegistry {
* @param theServiceId the id of the service to be removed
*/
void unregisterService(String theServiceId, String theModuleId);
/**
* Get registered CDS service with service ID
* @param theServiceId the id of the service to be retrieved
* @return CdsServiceJson
* @throws IllegalArgumentException if a CDS service with provided serviceId is not found
*/
CdsServiceJson getCdsServiceJson(String theServiceId);
}

View File

@ -24,18 +24,29 @@ import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @see <a href=" https://cds-hooks.hl7.org/1.0/#extensions">For reading more about Extension support in CDS hooks</a>
* Example can be found <a href="https://build.fhir.org/ig/HL7/davinci-crd/deviations.html#configuration-options-extension">here</a>
*/
public abstract class BaseCdsServiceJson implements IModelJson {
@JsonProperty(value = "extension", required = true)
String myExtension;
@JsonProperty(value = "extension")
CdsHooksExtension myExtension;
public String getExtension() {
private Class<? extends CdsHooksExtension> myExtensionClass;
public CdsHooksExtension getExtension() {
return myExtension;
}
public BaseCdsServiceJson setExtension(String theExtension) {
public BaseCdsServiceJson setExtension(CdsHooksExtension theExtension) {
this.myExtension = theExtension;
return this;
}
public void setExtensionClass(Class<? extends CdsHooksExtension> theClass) {
this.myExtensionClass = theClass;
}
public Class<? extends CdsHooksExtension> getExtensionClass() {
return myExtensionClass;
}
}

View File

@ -0,0 +1,9 @@
package ca.uhn.hapi.fhir.cdshooks.api.json;
import ca.uhn.fhir.model.api.IModelJson;
/**
* Users can define CDS Hooks extensions by extending this class.
* Implementors can extend this class for defining their custom extensions.
*/
public class CdsHooksExtension implements IModelJson {}

View File

@ -98,13 +98,15 @@ public class CdsHooksConfig {
CdsPrefetchSvc theCdsPrefetchSvc,
@Qualifier(CDS_HOOKS_OBJECT_MAPPER_FACTORY) ObjectMapper theObjectMapper,
ICdsCrServiceFactory theCdsCrServiceFactory,
ICrDiscoveryServiceFactory theCrDiscoveryServiceFactory) {
ICrDiscoveryServiceFactory theCrDiscoveryServiceFactory,
FhirContext theFhirContext) {
return new CdsServiceRegistryImpl(
theCdsHooksContextBooter,
theCdsPrefetchSvc,
theObjectMapper,
theCdsCrServiceFactory,
theCrDiscoveryServiceFactory);
theCrDiscoveryServiceFactory,
theFhirContext);
}
@Bean

View File

@ -94,13 +94,12 @@ public class CdsHooksController {
path = "{cds_hook}/feedback",
method = {RequestMethod.POST},
consumes = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<String> cdsServiceFeedback(
public ResponseEntity<CdsServiceFeedbackJson> cdsServiceFeedback(
@PathVariable("cds_hook") String theCdsHook,
@RequestBody CdsServiceFeedbackJson theCdsServiceFeedbackJson) {
String json = myCdsServiceRegistry.callFeedback(theCdsHook, theCdsServiceFeedbackJson);
CdsServiceFeedbackJson response = myCdsServiceRegistry.callFeedback(theCdsHook, theCdsServiceFeedbackJson);
return ResponseEntity.status(200)
.contentType(MediaType.APPLICATION_JSON)
.body(json);
.body(response);
}
}

View File

@ -0,0 +1,98 @@
package ca.uhn.hapi.fhir.cdshooks.serializer;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.serializer.FhirResourceDeserializer;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsHooksExtension;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestContextJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
import ca.uhn.hapi.fhir.cdshooks.svc.CdsServiceRegistryImpl;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
public class CdsServiceRequestJsonDeserializer extends StdDeserializer<CdsServiceRequestJson> {
private final CdsServiceRegistryImpl myCdsServiceRegistry;
private final ObjectMapper myObjectMapper;
private final FhirContext myFhirContext;
private final IParser myParser;
public CdsServiceRequestJsonDeserializer(CdsServiceRegistryImpl theCdsServiceRegistry, FhirContext theFhirContext) {
super(CdsServiceRequestJson.class);
myCdsServiceRegistry = theCdsServiceRegistry;
myFhirContext = theFhirContext;
myParser = myFhirContext.newJsonParser().setPrettyPrint(true);
// We create a new ObjectMapper instead of using the one from the ApplicationContext to avoid an infinite loop
// during deserialization.
myObjectMapper = new ObjectMapper();
configureObjectMapper(myObjectMapper);
}
@Override
public CdsServiceRequestJson deserialize(JsonParser theJsonParser, DeserializationContext theDeserializationContext)
throws IOException {
final JsonNode cdsServiceRequestJsonNode = theJsonParser.getCodec().readTree(theJsonParser);
final JsonNode hookNode = cdsServiceRequestJsonNode.get("hook");
final JsonNode extensionNode = cdsServiceRequestJsonNode.get("extension");
final JsonNode requestContext = cdsServiceRequestJsonNode.get("context");
final CdsServiceRequestJson cdsServiceRequestJson =
myObjectMapper.treeToValue(cdsServiceRequestJsonNode, CdsServiceRequestJson.class);
if (extensionNode != null) {
CdsHooksExtension myRequestExtension = deserializeExtension(hookNode.textValue(), extensionNode.toString());
cdsServiceRequestJson.setExtension(myRequestExtension);
}
if (requestContext != null) {
LinkedHashMap<String, Object> map =
myObjectMapper.readValue(requestContext.toString(), LinkedHashMap.class);
cdsServiceRequestJson.setContext(deserializeRequestContext(map));
}
return cdsServiceRequestJson;
}
void configureObjectMapper(ObjectMapper theObjectMapper) {
SimpleModule module = new SimpleModule();
module.addDeserializer(IBaseResource.class, new FhirResourceDeserializer(myFhirContext));
theObjectMapper.registerModule(module);
// set this as we will need to ignore properties which are not defined by specific implementation.
theObjectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
CdsHooksExtension deserializeExtension(String theServiceId, String theExtension) throws JsonProcessingException {
final CdsServiceJson cdsServicesJson = myCdsServiceRegistry.getCdsServiceJson(theServiceId);
Class<? extends CdsHooksExtension> extensionClass = cdsServicesJson.getExtensionClass();
if (extensionClass == null) {
return null;
}
return myObjectMapper.readValue(theExtension, extensionClass);
}
CdsServiceRequestContextJson deserializeRequestContext(LinkedHashMap<String, Object> theMap)
throws JsonProcessingException {
final CdsServiceRequestContextJson cdsServiceRequestContextJson = new CdsServiceRequestContextJson();
for (Map.Entry<String, Object> entry : theMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// Convert LinkedHashMap entries to Resources
if (value instanceof LinkedHashMap) {
String json = myObjectMapper.writeValueAsString(value);
IBaseResource resource = myParser.parseResource(json);
cdsServiceRequestContextJson.put(key, resource);
} else {
cdsServiceRequestContextJson.put(key, value);
}
}
return cdsServiceRequestContextJson;
}
}

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.hapi.fhir.cdshooks.api.CdsService;
import ca.uhn.hapi.fhir.cdshooks.api.CdsServiceFeedback;
import ca.uhn.hapi.fhir.cdshooks.api.CdsServicePrefetch;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsHooksExtension;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -85,7 +86,8 @@ public class CdsHooksContextBooter {
cdsServiceJson.setHook(annotation.hook());
cdsServiceJson.setDescription(annotation.description());
cdsServiceJson.setTitle(annotation.title());
cdsServiceJson.setExtension(validateJson(annotation.extension()));
cdsServiceJson.setExtension(serializeExtensions(annotation.extension(), annotation.extensionClass()));
cdsServiceJson.setExtensionClass(annotation.extensionClass());
for (CdsServicePrefetch prefetch : annotation.prefetch()) {
cdsServiceJson.addPrefetch(prefetch.value(), prefetch.query());
cdsServiceJson.addSource(prefetch.value(), prefetch.source());
@ -104,14 +106,13 @@ public class CdsHooksContextBooter {
}
}
protected String validateJson(String theExtension) {
CdsHooksExtension serializeExtensions(String theExtension, Class<? extends CdsHooksExtension> theClass) {
if (StringUtils.isEmpty(theExtension)) {
return null;
}
try {
final ObjectMapper mapper = new ObjectMapper();
mapper.readTree(theExtension);
return theExtension;
return mapper.readValue(theExtension, theClass);
} catch (JsonProcessingException e) {
final String message = String.format("Invalid JSON: %s", e.getMessage());
ourLog.debug(message);

View File

@ -131,4 +131,11 @@ public class CdsServiceCache {
}
return result;
}
CdsServiceJson getCdsServiceJson(String theString) {
return myCdsServiceJson.getServices().stream()
.filter(x -> x.getId().equals(theString))
.findFirst()
.orElse(null);
}
}

View File

@ -20,21 +20,26 @@
package ca.uhn.hapi.fhir.cdshooks.svc;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsMethod;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceMethod;
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsHooksExtension;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceFeedbackJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServicesJson;
import ca.uhn.hapi.fhir.cdshooks.serializer.CdsServiceRequestJsonDeserializer;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.ICdsCrServiceFactory;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery.ICrDiscoveryServiceFactory;
import ca.uhn.hapi.fhir.cdshooks.svc.prefetch.CdsPrefetchSvc;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Nonnull;
import jakarta.annotation.PostConstruct;
import org.apache.commons.lang3.Validate;
@ -59,10 +64,18 @@ public class CdsServiceRegistryImpl implements ICdsServiceRegistry {
CdsPrefetchSvc theCdsPrefetchSvc,
ObjectMapper theObjectMapper,
ICdsCrServiceFactory theCdsCrServiceFactory,
ICrDiscoveryServiceFactory theCrDiscoveryServiceFactory) {
ICrDiscoveryServiceFactory theCrDiscoveryServiceFactory,
FhirContext theFhirContext) {
myCdsHooksContextBooter = theCdsHooksContextBooter;
myCdsPrefetchSvc = theCdsPrefetchSvc;
myObjectMapper = theObjectMapper;
// registering this deserializer here instead of
// CdsHooksObjectMapperFactory to avoid circular
// dependency
SimpleModule module = new SimpleModule();
module.addDeserializer(
CdsServiceRequestJson.class, new CdsServiceRequestJsonDeserializer(this, theFhirContext));
myObjectMapper.registerModule(module);
myCdsCrServiceFactory = theCdsCrServiceFactory;
myCrDiscoveryServiceFactory = theCrDiscoveryServiceFactory;
}
@ -82,61 +95,14 @@ public class CdsServiceRegistryImpl implements ICdsServiceRegistry {
ICdsServiceMethod serviceMethod = (ICdsServiceMethod) getCdsServiceMethodOrThrowException(theServiceId);
myCdsPrefetchSvc.augmentRequest(theCdsServiceRequestJson, serviceMethod);
Object response = serviceMethod.invoke(myObjectMapper, theCdsServiceRequestJson, theServiceId);
return encodeServiceResponse(theServiceId, response);
}
private CdsServiceResponseJson encodeServiceResponse(String theServiceId, Object result) {
String json;
if (result instanceof String) {
json = (String) result;
} else {
try {
json = myObjectMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
throw new ConfigurationException(
Msg.code(2389) + "Failed to json serialize Cds service response of type "
+ result.getClass().getName() + " when calling CDS Hook Service " + theServiceId,
e);
}
}
try {
return myObjectMapper.readValue(json, CdsServiceResponseJson.class);
} catch (JsonProcessingException e) {
throw new ConfigurationException(
Msg.code(2390) + "Failed to json deserialize Cds service response of type "
+ result.getClass().getName() + " when calling CDS Hook Service " + theServiceId
+ ". Json: " + json,
e);
}
}
@Nonnull
private ICdsMethod getCdsServiceMethodOrThrowException(String theId) {
ICdsMethod retval = myServiceCache.getServiceMethod(theId);
if (retval == null) {
throw new ResourceNotFoundException(
Msg.code(2391) + "No service with id " + theId + " is registered on this server");
}
return retval;
}
@Nonnull
private ICdsMethod getCdsFeedbackMethodOrThrowException(String theId) {
ICdsMethod retval = myServiceCache.getFeedbackMethod(theId);
if (retval == null) {
throw new ResourceNotFoundException(
Msg.code(2392) + "No feedback service with id " + theId + " is registered on this server");
}
return retval;
}
@Override
public String callFeedback(String theServiceId, CdsServiceFeedbackJson theCdsServiceFeedbackJson) {
public CdsServiceFeedbackJson callFeedback(String theServiceId, CdsServiceFeedbackJson theCdsServiceFeedbackJson) {
ICdsMethod feedbackMethod = getCdsFeedbackMethodOrThrowException(theServiceId);
Object response = feedbackMethod.invoke(myObjectMapper, theCdsServiceFeedbackJson, theServiceId);
return encodeFeedbackResponse(theServiceId, theCdsServiceFeedbackJson, response);
return encodeFeedbackResponse(theServiceId, response);
}
@Override
@ -146,6 +112,9 @@ public class CdsServiceRegistryImpl implements ICdsServiceRegistry {
CdsServiceJson theCdsServiceJson,
boolean theAllowAutoFhirClientPrefetch,
String theModuleId) {
if (theCdsServiceJson.getExtensionClass() == null) {
theCdsServiceJson.setExtensionClass(CdsHooksExtension.class);
}
myServiceCache.registerDynamicService(
theServiceId, theServiceFunction, theCdsServiceJson, theAllowAutoFhirClientPrefetch, theModuleId);
}
@ -171,25 +140,100 @@ public class CdsServiceRegistryImpl implements ICdsServiceRegistry {
}
}
private String encodeFeedbackResponse(
String theServiceId, CdsServiceFeedbackJson theCdsServiceFeedbackJson, Object response) {
if (response instanceof String) {
return (String) response;
@Override
public CdsServiceJson getCdsServiceJson(String theServiceId) {
CdsServiceJson cdsServiceJson = myServiceCache.getCdsServiceJson(theServiceId);
if (cdsServiceJson == null) {
throw new IllegalArgumentException(Msg.code(2536) + "No service with " + theServiceId + " is registered.");
}
return cdsServiceJson;
}
@Nonnull
private ICdsMethod getCdsServiceMethodOrThrowException(String theId) {
ICdsMethod retval = myServiceCache.getServiceMethod(theId);
if (retval == null) {
throw new ResourceNotFoundException(
Msg.code(2391) + "No service with id " + theId + " is registered on this server");
}
return retval;
}
@Nonnull
CdsServiceResponseJson encodeServiceResponse(String theServiceId, Object result) {
if (result instanceof String) {
return buildResponseFromString(theServiceId, result, (String) result);
} else {
try {
// Try to json encode the response
return myObjectMapper.writeValueAsString(response);
} catch (JsonProcessingException e) {
try {
ourLog.warn("Failed to deserialize response from {} feedback method", theServiceId, e);
// Just send back what we received
return myObjectMapper.writeValueAsString(theCdsServiceFeedbackJson);
} catch (JsonProcessingException f) {
ourLog.error("Failed to deserialize request parameter to {} feedback method", theServiceId, e);
// Okay then...
return "{}";
}
}
return buildResponseFromImplementation(theServiceId, result);
}
}
@Nonnull
private ICdsMethod getCdsFeedbackMethodOrThrowException(String theId) {
ICdsMethod retval = myServiceCache.getFeedbackMethod(theId);
if (retval == null) {
throw new ResourceNotFoundException(
Msg.code(2392) + "No feedback service with id " + theId + " is registered on this server");
}
return retval;
}
@Nonnull
CdsServiceFeedbackJson encodeFeedbackResponse(String theServiceId, Object theResponse) {
if (theResponse instanceof String) {
return buildFeedbackFromString(theServiceId, (String) theResponse);
} else {
return buildFeedbackFromImplementation(theServiceId, theResponse);
}
}
private CdsServiceResponseJson buildResponseFromImplementation(String theServiceId, Object theResult) {
try {
return (CdsServiceResponseJson) theResult;
} catch (ClassCastException e) {
throw new ConfigurationException(
Msg.code(2389)
+ "Failed to cast Cds service response to CdsServiceResponseJson when calling CDS Hook Service "
+ theServiceId + ". The type "
+ theResult.getClass().getName()
+ " cannot be casted to CdsServiceResponseJson",
e);
}
}
private CdsServiceResponseJson buildResponseFromString(String theServiceId, Object theResult, String theJson) {
try {
return myObjectMapper.readValue(theJson, CdsServiceResponseJson.class);
} catch (JsonProcessingException e) {
throw new ConfigurationException(
Msg.code(2390) + "Failed to json deserialize Cds service response of type "
+ theResult.getClass().getName() + " when calling CDS Hook Service " + theServiceId
+ ". Json: " + theJson,
e);
}
}
private CdsServiceFeedbackJson buildFeedbackFromImplementation(String theServiceId, Object theResponse) {
try {
return (CdsServiceFeedbackJson) theResponse;
} catch (ClassCastException e) {
throw new ClassCastException(
Msg.code(2537) + "Failed to cast feedback response CdsServiceFeedbackJson for service "
+ theServiceId + ". " + e.getMessage());
}
}
private CdsServiceFeedbackJson buildFeedbackFromString(String theServiceId, String theResponse) {
try {
return myObjectMapper.readValue(theResponse, CdsServiceFeedbackJson.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(Msg.code(2538) + "Failed to serialize json Cds Feedback response for service "
+ theServiceId + ". " + e.getMessage());
}
}
@VisibleForTesting
void setServiceCache(CdsServiceCache theCdsServiceCache) {
myServiceCache = theCdsServiceCache;
}
}

View File

@ -10,6 +10,7 @@ import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseCardJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
import ca.uhn.hapi.fhir.cdshooks.config.CdsHooksConfig;
import ca.uhn.hapi.fhir.cdshooks.config.TestCdsHooksConfig;
import ca.uhn.hapi.fhir.cdshooks.custom.extensions.model.RequestExtension;
import ca.uhn.hapi.fhir.cdshooks.svc.prefetch.CdsPrefetchFhirClientSvc;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -21,6 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
@ -30,6 +32,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.UUID;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@ -67,7 +70,9 @@ public class CdsHooksControllerTest {
@BeforeEach
public void before() {
myMockMvc = MockMvcBuilders.standaloneSetup(new CdsHooksController(myCdsHooksRegistry)).build();
myMockMvc = MockMvcBuilders.standaloneSetup(new CdsHooksController(myCdsHooksRegistry))
.setMessageConverters(new MappingJackson2HttpMessageConverter(myObjectMapper))
.build();
}
@Test
@ -82,25 +87,29 @@ public class CdsHooksControllerTest {
.andExpect(jsonPath("services[2].title").value(GreeterCdsService.TEST_HOOK_TITLE))
.andExpect(jsonPath("services[2].id").value(GreeterCdsService.TEST_HOOK_STRING_ID))
.andExpect(jsonPath("services[2].prefetch." + GreeterCdsService.TEST_HOOK_PREFETCH_USER_KEY).value(GreeterCdsService.TEST_HOOK_PREFETCH_USER_QUERY))
.andExpect(jsonPath("services[2].prefetch." + GreeterCdsService.TEST_HOOK_PREFETCH_PATIENT_KEY).value(GreeterCdsService.TEST_HOOK_PREFETCH_PATIENT_QUERY));
.andExpect(jsonPath("services[2].prefetch." + GreeterCdsService.TEST_HOOK_PREFETCH_PATIENT_KEY).value(GreeterCdsService.TEST_HOOK_PREFETCH_PATIENT_QUERY))
.andExpect(jsonPath("services[5].extension.example-client-conformance").value("http://hooks.example.org/fhir/102/Conformance/patientview"));
}
@Test
void testExampleFeedback() throws Exception {
CdsServiceFeedbackJson request = new CdsServiceFeedbackJson();
// setup
final CdsServiceFeedbackJson request = new CdsServiceFeedbackJson();
request.setCard(TEST_HOOK_INSTANCE);
request.setOutcome(CdsServiceFeebackOutcomeEnum.accepted);
request.setOutcomeTimestamp(OUTCOME_TIMESTAMP);
String requestBody = myObjectMapper.writeValueAsString(request);
myMockMvc
final String requestBody = myObjectMapper.writeValueAsString(request);
// execute
final MvcResult actual = myMockMvc
.perform(post(CdsHooksController.BASE + "/example-service/feedback").contentType(MediaType.APPLICATION_JSON).content(requestBody))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("message").value("Thank you for your feedback dated " + OUTCOME_TIMESTAMP + "!"))
;
.andReturn();
// validate
final CdsServiceFeedbackJson cdsServiceFeedbackJson = myObjectMapper.readValue(actual.getResponse().getContentAsString(), CdsServiceFeedbackJson.class);
assertThat(cdsServiceFeedbackJson.getAcceptedSuggestions()).hasSize(1);
assertThat(cdsServiceFeedbackJson).usingRecursiveComparison().ignoringFields("myAcceptedSuggestions").isEqualTo(request);
}
@Test
@ -127,8 +136,14 @@ public class CdsHooksControllerTest {
@Test
void testCallHelloUniverse() throws Exception {
RequestExtension requestExtension = new RequestExtension();
requestExtension.setConfigItem("request-config-item");
CdsServiceRequestJson request = new CdsServiceRequestJson();
request.setExtension(requestExtension);
request.setFhirServer(TEST_FHIR_SERVER);
request.setHook(HelloWorldService.TEST_HOOK_UNIVERSE_ID);
String requestBody = myObjectMapper.writeValueAsString(request);
@ -139,7 +154,7 @@ public class CdsHooksControllerTest {
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("cards[0].summary").value("Hello Universe!"))
.andExpect(jsonPath("cards[0].indicator").value("critical"))
;
.andExpect(jsonPath("cards[0].extension.example-config-item").value("request-config-item"));
}
@Test
@ -163,21 +178,24 @@ public class CdsHooksControllerTest {
@Test
void testHelloWorldFeedback() throws Exception {
CdsServiceFeedbackJson request = new CdsServiceFeedbackJson();
// setup
final CdsServiceFeedbackJson request = new CdsServiceFeedbackJson();
request.setCard(TEST_HOOK_INSTANCE);
request.setOutcome(CdsServiceFeebackOutcomeEnum.accepted);
request.setOutcomeTimestamp(OUTCOME_TIMESTAMP);
String requestBody = myObjectMapper.writeValueAsString(request);
TestServerAppCtx.ourHelloWorldService.setExpectedCount(1);
myMockMvc
// execute
MvcResult actual = myMockMvc
.perform(post(CdsHooksController.BASE + "/" + HelloWorldService.TEST_HOOK_WORLD_ID + "/feedback").contentType(MediaType.APPLICATION_JSON).content(requestBody))
.andDo(print())
.andExpect(status().is2xxSuccessful())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("message").value("Thank you for your feedback dated " + OUTCOME_TIMESTAMP + "!"))
;
.andReturn();
// validate
final CdsServiceFeedbackJson cdsServiceFeedbackJson = myObjectMapper.readValue(actual.getResponse().getContentAsString(), CdsServiceFeedbackJson.class);
assertThat(cdsServiceFeedbackJson.getAcceptedSuggestions()).hasSize(1);
assertThat(cdsServiceFeedbackJson).usingRecursiveComparison().ignoringFields("myAcceptedSuggestions").isEqualTo(request);
TestServerAppCtx.ourHelloWorldService.awaitExpected();
}

View File

@ -3,6 +3,7 @@ package ca.uhn.hapi.fhir.cdshooks.controller;
import ca.uhn.hapi.fhir.cdshooks.api.CdsService;
import ca.uhn.hapi.fhir.cdshooks.api.CdsServiceFeedback;
import ca.uhn.hapi.fhir.cdshooks.api.CdsServicePrefetch;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceAcceptedSuggestionJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceFeedbackJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceIndicatorEnum;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
@ -11,6 +12,9 @@ import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseCardSourceJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
import org.hl7.fhir.r4.model.Patient;
import java.util.List;
import java.util.UUID;
public class ExampleCdsService {
@CdsService(value = "example-service",
hook = "patient-view",
@ -33,7 +37,8 @@ public class ExampleCdsService {
}
@CdsServiceFeedback("example-service")
public String exampleServiceFeedback(CdsServiceFeedbackJson theFeedback) {
return "{\"message\": \"Thank you for your feedback dated " + theFeedback.getOutcomeTimestamp() + "!\"}";
public CdsServiceFeedbackJson feedback(CdsServiceFeedbackJson theFeedback) {
theFeedback.setAcceptedSuggestions(List.of(new CdsServiceAcceptedSuggestionJson().setId(UUID.randomUUID().toString())));
return theFeedback;
}
}

View File

@ -4,12 +4,22 @@ import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.hapi.fhir.cdshooks.api.CdsService;
import ca.uhn.hapi.fhir.cdshooks.api.CdsServiceFeedback;
import ca.uhn.hapi.fhir.cdshooks.api.CdsServicePrefetch;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceAcceptedSuggestionJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceFeedbackJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceIndicatorEnum;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseCardJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseCardSourceJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
import ca.uhn.hapi.fhir.cdshooks.custom.extensions.model.ExampleConfigExtension;
import ca.uhn.hapi.fhir.cdshooks.custom.extensions.model.RequestExtension;
import ca.uhn.hapi.fhir.cdshooks.custom.extensions.model.ResponseExtension;
import ca.uhn.test.concurrency.IPointcutLatch;
import ca.uhn.test.concurrency.PointcutLatch;
import java.util.Date;
import java.util.List;
import java.util.UUID;
public class HelloWorldService implements IPointcutLatch {
public static final String TEST_HOOK = "hello-world";
@ -20,6 +30,9 @@ public class HelloWorldService implements IPointcutLatch {
public static final String TEST_HOOK_PLAYBACK_ID = "hwid3";
public static final String TEST_HOOK_PREFETCH_PATIENT_KEY = "patient";
public static final String TEST_HOOK_PREFETCH_MEDS_KEY = "medications";
public static final String CDS_HOOKS_EXTENSION_PROPERTY_PRACTITIONER_SPECIALITY = "myextension-practitionerspecialty";
public static final String CDS_HOOKS_EXTENSION_PROPERTY_TIMESTAMP = "timestamp";
public static final String CDS_HOOKS_EXTENSION_VALUE_PRACTITIONER_SPECIALITY = "some-speciality";
private final PointcutLatch myPointcutLatch = new PointcutLatch("Hello World CDS-Hook");
@ -31,23 +44,26 @@ public class HelloWorldService implements IPointcutLatch {
@CdsServicePrefetch(value = TEST_HOOK_PREFETCH_PATIENT_KEY, query = "Patient/{{context.patientId}}"),
@CdsServicePrefetch(value = TEST_HOOK_PREFETCH_MEDS_KEY, query = "MedicationRequest?patient={{context.patientId}}")
})
public String helloWorld(String theJson) {
return "{\n" +
" \"cards\" : [ {\n" +
" \"summary\" : \"Hello World!\",\n" +
" \"indicator\" : \"warning\",\n" +
" \"source\" : {\n" +
" \"label\" : \"World Greeter\"\n" +
" },\n" +
" \"detail\" : \"This is a test. Do not be alarmed.\"\n" +
" } ]\n" +
"}";
public CdsServiceResponseJson helloWorld(String theJson) {
final CdsServiceResponseJson cdsServiceResponseJson = new CdsServiceResponseJson();
final CdsServiceResponseCardJson cdsServiceResponseCardJson = new CdsServiceResponseCardJson();
cdsServiceResponseCardJson.setSummary("Hello World!");
cdsServiceResponseCardJson.setIndicator(CdsServiceIndicatorEnum.WARNING);
cdsServiceResponseCardJson.setDetail("This is a test. Do not be alarmed.");
cdsServiceResponseCardJson.setSource(new CdsServiceResponseCardSourceJson().setLabel("World Greeter"));
final ResponseExtension extension = new ResponseExtension();
extension.setTimestamp(new Date());
extension.setPractitionerSpecialty(CDS_HOOKS_EXTENSION_VALUE_PRACTITIONER_SPECIALITY);
cdsServiceResponseCardJson.setExtension(extension);
cdsServiceResponseJson.addCard(cdsServiceResponseCardJson);
return cdsServiceResponseJson;
}
@CdsServiceFeedback(TEST_HOOK_WORLD_ID)
public String feedback(CdsServiceFeedbackJson theFeedback) {
public CdsServiceFeedbackJson feedback(CdsServiceFeedbackJson theFeedback) {
myPointcutLatch.call(theFeedback);
return "{\"message\": \"Thank you for your feedback dated " + theFeedback.getOutcomeTimestamp() + "!\"}";
theFeedback.setAcceptedSuggestions(List.of(new CdsServiceAcceptedSuggestionJson().setId(UUID.randomUUID().toString())));
return theFeedback;
}
@CdsService(value = TEST_HOOK_UNIVERSE_ID,
@ -57,18 +73,23 @@ public class HelloWorldService implements IPointcutLatch {
prefetch = {
@CdsServicePrefetch(value = TEST_HOOK_PREFETCH_PATIENT_KEY, query = "Patient/{{context.patientId}}"),
@CdsServicePrefetch(value = TEST_HOOK_PREFETCH_MEDS_KEY, query = "MedicationRequest?patient={{context.patientId}}")
})
public String helloUniverse(String theJson) {
return "{\n" +
" \"cards\" : [ {\n" +
" \"summary\" : \"Hello Universe!\",\n" +
" \"indicator\" : \"critical\",\n" +
" \"source\" : {\n" +
" \"label\" : \"World Greeter\"\n" +
" },\n" +
" \"detail\" : \"This is a test. Do not be alarmed.\"\n" +
" } ]\n" +
"}";
},
extension = """
{
"example-config-item": "example-value"
}
""",
extensionClass = RequestExtension.class)
public CdsServiceResponseJson helloUniverse(CdsServiceRequestJson theCdsServiceRequestJson) {
final CdsServiceResponseJson cdsServiceResponseJson = new CdsServiceResponseJson();
final CdsServiceResponseCardJson cdsServiceResponseCardJson = new CdsServiceResponseCardJson();
cdsServiceResponseCardJson.setSummary("Hello Universe!");
cdsServiceResponseCardJson.setIndicator(CdsServiceIndicatorEnum.CRITICAL);
cdsServiceResponseCardJson.setDetail("This is a test. Do not be alarmed.");
cdsServiceResponseCardJson.setSource(new CdsServiceResponseCardSourceJson().setLabel("World Greeter"));
cdsServiceResponseCardJson.setExtension(theCdsServiceRequestJson.getExtension());
cdsServiceResponseJson.addCard(cdsServiceResponseCardJson);
return cdsServiceResponseJson;
}
@CdsService(value = TEST_HOOK_PLAYBACK_ID,
@ -78,21 +99,24 @@ public class HelloWorldService implements IPointcutLatch {
prefetch = {
@CdsServicePrefetch(value = TEST_HOOK_PREFETCH_PATIENT_KEY, query = "Patient/{{context.patientId}}"),
@CdsServicePrefetch(value = TEST_HOOK_PREFETCH_MEDS_KEY, query = "MedicationRequest?patient={{context.patientId}}")
})
public String playback(CdsServiceRequestJson theCdsServiceRequestJson) {
return "{\n" +
" \"cards\" : [ {\n" +
" \"summary\" : \"FhirServer: " + theCdsServiceRequestJson.getFhirServer() +
},
extension = """
{
"example-client-conformance": "http://hooks.example.org/fhir/102/Conformance/patientview"
}
""",
extensionClass = ExampleConfigExtension.class)
public CdsServiceResponseJson playback(CdsServiceRequestJson theCdsServiceRequestJson) {
final CdsServiceResponseJson cdsServiceResponseJson = new CdsServiceResponseJson();
final CdsServiceResponseCardJson cdsServiceResponseCardJson = new CdsServiceResponseCardJson();
cdsServiceResponseCardJson.setSummary("FhirServer: " + theCdsServiceRequestJson.getFhirServer() +
" Hook: " + theCdsServiceRequestJson.getHook() +
" Hook Instance: " + theCdsServiceRequestJson.getHookInstance() +
"\",\n" +
" \"indicator\" : \"critical\",\n" +
" \"source\" : {\n" +
" \"label\" : \"World Greeter\"\n" +
" },\n" +
" \"detail\" : \"This is a test. Do not be alarmed.\"\n" +
" } ]\n" +
"}";
" Hook Instance: " + theCdsServiceRequestJson.getHookInstance());
cdsServiceResponseCardJson.setIndicator(CdsServiceIndicatorEnum.CRITICAL);
cdsServiceResponseCardJson.setDetail("This is a test. Do not be alarmed.");
cdsServiceResponseCardJson.setSource(new CdsServiceResponseCardSourceJson().setLabel("World Greeter"));
cdsServiceResponseJson.addCard(cdsServiceResponseCardJson);
return cdsServiceResponseJson;
}
@Override

View File

@ -0,0 +1,13 @@
package ca.uhn.hapi.fhir.cdshooks.custom.extensions.model;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsHooksExtension;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ExampleConfigExtension extends CdsHooksExtension {
@JsonProperty("example-client-conformance")
String myExampleClientConformance;
public String getExampleClientConformance() {
return myExampleClientConformance;
}
}

View File

@ -0,0 +1,13 @@
package ca.uhn.hapi.fhir.cdshooks.custom.extensions.model;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsHooksExtension;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ExampleExtension extends CdsHooksExtension {
@JsonProperty("example-property")
String myExampleProperty;
public String getExampleProperty() {
return myExampleProperty;
}
}

View File

@ -0,0 +1,18 @@
package ca.uhn.hapi.fhir.cdshooks.custom.extensions.model;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsHooksExtension;
import com.fasterxml.jackson.annotation.JsonProperty;
public class RequestExtension extends CdsHooksExtension {
@JsonProperty("example-config-item")
String myConfigItem;
public String getConfigItem() {
return myConfigItem;
}
public void setConfigItem(String theConfigItem) {
myConfigItem = theConfigItem;
}
}

View File

@ -0,0 +1,22 @@
package ca.uhn.hapi.fhir.cdshooks.custom.extensions.model;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsHooksExtension;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Date;
public class ResponseExtension extends CdsHooksExtension {
@JsonProperty(value = "timestamp", required = true)
private Date myDate;
@JsonProperty(value = "myextension-practitionerspecialty", required = true)
private String myPractitionerSpecialty;
public void setTimestamp(Date theDate) {
myDate = theDate;
}
public void setPractitionerSpecialty(String thePractitionerSpecialty) {
myPractitionerSpecialty = thePractitionerSpecialty;
}
}

View File

@ -0,0 +1,117 @@
package ca.uhn.hapi.fhir.cdshooks.serializer;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsHooksExtension;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestContextJson;
import ca.uhn.hapi.fhir.cdshooks.custom.extensions.model.ExampleExtension;
import ca.uhn.hapi.fhir.cdshooks.svc.CdsServiceRegistryImpl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.LinkedHashMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;
@ExtendWith(MockitoExtension.class)
class CdsServiceRequestJsonDeserializerTest {
@Mock
private CdsServiceRegistryImpl myCdsServiceRegistry;
private final FhirContext myFhirContext = FhirContext.forR4();
private CdsServiceRequestJsonDeserializer myFixture;
@BeforeEach()
void setup() {
myFixture = new CdsServiceRequestJsonDeserializer(myCdsServiceRegistry, myFhirContext);
}
@Test
void configureObjectMapper() {
// setup
ObjectMapper input = new ObjectMapper();
// execute
myFixture.configureObjectMapper(input);
// validate
assertThat(input.getRegisteredModuleIds()).hasSize(1);
}
@Test
void deserializeExtensionWhenClassFoundShouldDeserializeExtension() throws JsonProcessingException {
// setup
final String serviceId = "service-id";
final String extension = """
{
"example-property": "example-value"
}
""";
final CdsServiceJson cdsServiceJson = new CdsServiceJson();
cdsServiceJson.setId(serviceId);
cdsServiceJson.setExtensionClass(ExampleExtension.class);
doReturn(cdsServiceJson).when(myCdsServiceRegistry).getCdsServiceJson(serviceId);
// execute
final ExampleExtension actual = (ExampleExtension) myFixture.deserializeExtension(serviceId, extension);
// validate
assertThat(actual.getExampleProperty()).isEqualTo("example-value");
}
@Test
void deserializeExtensionWhenClassFoundButExtensionHasExtraPropertiesShouldIgnoreExtraProperties() throws JsonProcessingException {
// setup
final String serviceId = "service-id";
final String extension = """
{
"example-property": "example-value",
"example-extra-property": "example-extra-value"
}
""";
final CdsServiceJson cdsServiceJson = new CdsServiceJson();
cdsServiceJson.setId(serviceId);
cdsServiceJson.setExtensionClass(ExampleExtension.class);
doReturn(cdsServiceJson).when(myCdsServiceRegistry).getCdsServiceJson(serviceId);
// execute
final ExampleExtension actual = (ExampleExtension) myFixture.deserializeExtension(serviceId, extension);
// validate
assertThat(actual.getExampleProperty()).isEqualTo("example-value");
}
@Test
void deserializeExtensionWhenNotClassFoundShouldReturnNull() throws JsonProcessingException {
// setup
final String serviceId = "service-id";
final String extension = """
{
"example-property": "example-value"
}
""";
final CdsServiceJson cdsServiceJson = new CdsServiceJson();
cdsServiceJson.setId(serviceId);
doReturn(cdsServiceJson).when(myCdsServiceRegistry).getCdsServiceJson(serviceId);
// execute
final CdsHooksExtension actual = myFixture.deserializeExtension(serviceId, extension);
// validate
assertThat(actual).isNull();
}
@Test
void deserializeRequestContextShouldDeserializeValidContext() throws JsonProcessingException {
// setup
final String encounterId = "123";
final Patient patientContext = new Patient();
patientContext.setId("456");
final LinkedHashMap<String, Object> input = new LinkedHashMap<>();
input.put("encounterId", encounterId);
input.put("patient", patientContext);
// execute
final CdsServiceRequestContextJson actual = myFixture.deserializeRequestContext(input);
// validate
assertThat(actual.get("encounterId")).isEqualTo(encounterId);
assertThat(actual.get("patient")).usingRecursiveComparison().isEqualTo(patientContext);
}
}

View File

@ -1,12 +1,13 @@
package ca.uhn.hapi.fhir.cdshooks.svc;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsHooksExtension;
import ca.uhn.hapi.fhir.cdshooks.custom.extensions.model.ExampleExtension;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class CdsHooksContextBooterTest {
@ -16,35 +17,34 @@ class CdsHooksContextBooterTest {
void setUp() {
myFixture = new CdsHooksContextBooter();
}
@Test
void validateJsonReturnsNullWhenInputIsEmptyString() {
void serializeExtensionsReturnsNullWhenInputIsEmptyString() {
// execute
final String actual = myFixture.validateJson("");
final CdsHooksExtension actual = myFixture.serializeExtensions("", ExampleExtension.class);
// validate
assertNull(actual);
assertThat(actual).isNull();
}
@Test
void validateJsonThrowsExceptionWhenInputIsInvalid() {
void serializeExtensionsThrowsExceptionWhenInputIsInvalid() {
// setup
final String expected = "HAPI-2378: Invalid JSON: Unrecognized token 'abc': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')\n" +
" at [Source: REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled); line: 1, column: 4]";
// execute
final UnprocessableEntityException actual = assertThrows(UnprocessableEntityException.class, () -> myFixture.validateJson("abc"));
// validate
assertEquals(expected, actual.getMessage());
// execute & validate
assertThatThrownBy(
() -> myFixture.serializeExtensions("abc", ExampleExtension.class))
.isInstanceOf(UnprocessableEntityException.class)
.hasMessage(expected);
}
@Test
void validateJsonReturnsInputWhenInputIsValidJsonString() {
void serializeExtensionsReturnsInputWhenInputIsValidJsonString() {
// setup
final String expected = "{\n \"com.example.timestamp\": \"2017-11-27T22:13:25Z\",\n \"myextension-practitionerspecialty\" : \"gastroenterology\"\n }";
final String input = "{\n\"example-property\": \"some-value\" }";
// execute
final String actual = myFixture.validateJson(expected);
final ExampleExtension actual = (ExampleExtension) myFixture.serializeExtensions(input, ExampleExtension.class);
// validate
assertEquals(expected, actual);
assertThat(actual.getExampleProperty()).isEqualTo("some-value");
}
}

View File

@ -0,0 +1,179 @@
package ca.uhn.hapi.fhir.cdshooks.svc;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceFeedbackJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.ICdsCrServiceFactory;
import ca.uhn.hapi.fhir.cdshooks.svc.cr.discovery.ICrDiscoveryServiceFactory;
import ca.uhn.hapi.fhir.cdshooks.svc.prefetch.CdsPrefetchSvc;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.doReturn;
@ExtendWith(MockitoExtension.class)
class CdsServiceRegistryImplTest {
private static final String SERVICE_ID = "service-id";
@Mock
private CdsHooksContextBooter myCdsHooksContextBooter;
@Mock
private CdsPrefetchSvc myCdsPrefetchSvc;
@Mock
private ICdsCrServiceFactory myCdsCrServiceFactory;
@Mock
private ICrDiscoveryServiceFactory myCrDiscoveryServiceFactory;
@Mock
private CdsServiceCache myCdsServiceCache;
private final ObjectMapper myObjectMapper = new ObjectMapper();
private final FhirContext myFhirContext = FhirContext.forR4();
private CdsServiceRegistryImpl myFixture;
@BeforeEach()
void setup() {
myFixture = new CdsServiceRegistryImpl(myCdsHooksContextBooter, myCdsPrefetchSvc, myObjectMapper, myCdsCrServiceFactory, myCrDiscoveryServiceFactory, myFhirContext);
}
@Test
void encodeFeedbackResponseWhenResponseIsString() {
// setup
final String expectedCardText = "some-card";
final String input = """
{
"card": "some-card"
}
""";
// execute
final CdsServiceFeedbackJson actual = myFixture.encodeFeedbackResponse(SERVICE_ID, input);
// validate
assertThat(actual.getCard()).isEqualTo(expectedCardText);
}
@Test
void encodeFeedbackResponseWhenResponseIsCdsServiceFeedbackJson() {
// setup
final CdsServiceFeedbackJson expected = new CdsServiceFeedbackJson();
expected.setCard("some-card");
// execute
final CdsServiceFeedbackJson actual = myFixture.encodeFeedbackResponse(SERVICE_ID, expected);
// validate
assertThat(actual).isEqualTo(expected);
}
@Test
void encodeFeedbackResponseWhenResponseIsInvalidString() {
// setup
final String invalidString = "some-invalid-feedback";
// execute & validate
assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> {
myFixture.encodeFeedbackResponse(SERVICE_ID, invalidString);
}).withMessageContaining("HAPI-2538: Failed to serialize json Cds Feedback response for service service-id.");
}
@Test
void encodeFeedbackResponseWhenResponseIsInvalidObject() {
// setup
final CdsServiceResponseJson invalidObject = new CdsServiceResponseJson();
// execute & validate
assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> {
myFixture.encodeFeedbackResponse(SERVICE_ID, invalidObject);
}).withMessageContaining("HAPI-2537: Failed to cast feedback response CdsServiceFeedbackJson for service service-id.");
}
@Test
void encodeServiceResponseWhenResponseIsString() throws JsonProcessingException {
// setup
final String input = """
{
"cards": [
{
"summary": "some-summary",
"indicator": "info",
"source": {
"label": "some-label"
}
}
]
}
""";
// execute
final CdsServiceResponseJson actual = myFixture.encodeServiceResponse(SERVICE_ID, input);
// validate
assertThat(actual).usingRecursiveComparison().isEqualTo(myObjectMapper.readValue(input, CdsServiceResponseJson.class));
}
@Test
void encodeServiceResponseWhenResponseCdsServiceResponseJson() throws JsonProcessingException {
// setup
final String input = """
{
"cards": [
{
"summary": "some-summary",
"indicator": "info",
"source": {
"label": "some-label"
}
}
]
}
""";
// execute
final CdsServiceResponseJson actual = myFixture.encodeServiceResponse(SERVICE_ID, myObjectMapper.readValue(input, CdsServiceResponseJson.class));
// validate
assertThat(actual).usingRecursiveComparison().isEqualTo(actual);
}
@Test
void encodeServiceResponseWhenResponseIsInvalidString() {
// setup
final String invalidString = "some-string";
// execute & validate
assertThatExceptionOfType(ConfigurationException.class).isThrownBy(() -> {
myFixture.encodeServiceResponse(SERVICE_ID, invalidString);
}).withMessageContaining("Failed to json deserialize Cds service response of type java.lang.String when calling CDS Hook Service service-id.");
}
@Test
void encodeServiceResponseWhenResponseIsInvalidObject() {
// setup
final CdsServiceFeedbackJson invalidObject = new CdsServiceFeedbackJson();
// execute & validate
assertThatExceptionOfType(ConfigurationException.class).isThrownBy(() -> {
myFixture.encodeServiceResponse(SERVICE_ID, invalidObject);
}).withMessageContaining("Failed to cast Cds service response to CdsServiceResponseJson when calling CDS Hook Service service-id. The type ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceFeedbackJson cannot be casted to CdsServiceResponseJson");
}
@Test
void getCdsServiceJsonWhenServicePresent() {
// setup
final CdsServiceJson cdsService = new CdsServiceJson();
cdsService.setId(SERVICE_ID);
myFixture.setServiceCache(myCdsServiceCache);
doReturn(cdsService).when(myCdsServiceCache).getCdsServiceJson(SERVICE_ID);
// execute
final CdsServiceJson actual = myFixture.getCdsServiceJson(SERVICE_ID);
// validate
assertThat(actual).isNotNull();
}
@Test
void getCdsServiceJsonWhenServiceIsNotPresent() {
// setup
final String serviceId = "non-existent-serviceid";
myFixture.setServiceCache(myCdsServiceCache);
doReturn(null).when(myCdsServiceCache).getCdsServiceJson(serviceId);
// execute & validate
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> {
myFixture.getCdsServiceJson(serviceId);
}).withMessage("HAPI-2536: No service with " + serviceId + " is registered.");
}
}

View File

@ -24,6 +24,7 @@ import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository;
import java.io.IOException;
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.assertTrue;
@ -48,7 +49,7 @@ public class CdsCrServiceR4Test extends BaseCrTest {
requestDetails.setId(planDefinitionId);
final Parameters params = new CdsCrServiceR4(requestDetails, repository, myCdsConfigService).encodeParams(cdsServiceRequestJson);
assertTrue(params.getParameter().size() == 2);
assertEquals(2, params.getParameter().size());
assertTrue(params.getParameter("parameters").hasResource());
}
@ -62,9 +63,9 @@ public class CdsCrServiceR4Test extends BaseCrTest {
requestDetails.setId(planDefinitionId);
final CdsServiceResponseJson cdsServiceResponseJson = new CdsCrServiceR4(requestDetails, repository, myCdsConfigService).encodeResponse(responseBundle);
assertTrue(cdsServiceResponseJson.getCards().size() == 1);
assertTrue(!cdsServiceResponseJson.getCards().get(0).getSummary().isEmpty());
assertTrue(!cdsServiceResponseJson.getCards().get(0).getDetail().isEmpty());
assertEquals(1, cdsServiceResponseJson.getCards().size());
assertFalse(cdsServiceResponseJson.getCards().get(0).getSummary().isEmpty());
assertFalse(cdsServiceResponseJson.getCards().get(0).getDetail().isEmpty());
}
@Test
@ -78,7 +79,7 @@ public class CdsCrServiceR4Test extends BaseCrTest {
requestDetails.setId(planDefinitionId);
final CdsServiceResponseJson cdsServiceResponseJson = new CdsCrServiceR4(requestDetails, repository, myCdsConfigService).encodeResponse(responseBundle);
assertTrue(cdsServiceResponseJson.getServiceActions().size() == 1);
assertEquals(1, cdsServiceResponseJson.getServiceActions().size());
assertEquals(ActionType.CREATE, cdsServiceResponseJson.getServiceActions().get(0).getType());
assertNotNull(cdsServiceResponseJson.getServiceActions().get(0).getResource());
}

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -21,7 +21,7 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-caching-api</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
</dependency>
<dependency>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>hapi-deployable-pom</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -8,7 +8,7 @@
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<packaging>pom</packaging>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<name>HAPI-FHIR</name>
<description>An open-source implementation of the FHIR specification in Java.</description>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.3.8-SNAPSHOT</version>
<version>7.3.9-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>