implement cds hooks (#5076)
* move json and interfaces to hapi * move serialization over * moar test move * move prefetch service * move service registry * moved everything we can. now just need to add a controller and controller test * controller done * IJ Warnings * Move test * licenses * mvn spotless:apply * checkstyle * spotless --------- Co-authored-by: Ken Stevens <ken@smilecdr.com>
This commit is contained in:
parent
43694f378c
commit
c88662205d
|
@ -0,0 +1,44 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Core Library
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.fhir.serializer;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.parser.IParser;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class FhirResourceDeserializer extends StdDeserializer<IBaseResource> {
|
||||
private final IParser myParser;
|
||||
|
||||
public FhirResourceDeserializer(FhirContext theFhirContext) {
|
||||
super(IBaseResource.class);
|
||||
myParser = theFhirContext.newJsonParser().setPrettyPrint(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBaseResource deserialize(JsonParser theJsonParser, DeserializationContext theContext) throws IOException {
|
||||
String json = theJsonParser.getCodec().readTree(theJsonParser).toString();
|
||||
return myParser.parseResource(json);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - Core Library
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.fhir.serializer;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.parser.IParser;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class FhirResourceSerializer extends StdSerializer<IBaseResource> {
|
||||
private final IParser myParser;
|
||||
|
||||
public FhirResourceSerializer(FhirContext theFhirContext) {
|
||||
super(IBaseResource.class);
|
||||
myParser = theFhirContext.newJsonParser().setPrettyPrint(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(IBaseResource theResource, JsonGenerator theJsonGenerator, SerializerProvider theProvider)
|
||||
throws IOException {
|
||||
String resourceJson = myParser.encodeResourceToString(theResource);
|
||||
theJsonGenerator.writeRawValue(resourceJson);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-deployable-pom</artifactId>
|
||||
<version>6.7.14-SNAPSHOT</version>
|
||||
|
||||
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>hapi-fhir-server-cds-hooks</artifactId>
|
||||
<packaging>bundle</packaging>
|
||||
|
||||
<name>HAPI FHIR - CDS Hooks</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-base</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-server</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-storage</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-beans</artifactId>
|
||||
<version>${spring_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
<version>${spring_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<version>${spring_version}</version>
|
||||
</dependency>
|
||||
<!-- test -->
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ca.uhn.hapi.fhir</groupId>
|
||||
<artifactId>hapi-fhir-jpaserver-test-utilities</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.reflections</groupId>
|
||||
<artifactId>reflections</artifactId>
|
||||
<version>${reflections_version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-site-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipDeploy>true</skipDeploy>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.felix</groupId>
|
||||
<artifactId>maven-bundle-plugin</artifactId>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<instructions>
|
||||
<_nouses>true</_nouses>
|
||||
<_removeheaders>Built-By, Include-Resource, Private-Package, Require-Capability</_removeheaders>
|
||||
</instructions>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,44 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api;
|
||||
|
||||
public enum CdsResolutionStrategyEnum {
|
||||
/**
|
||||
* There are no means to fetch missing prefetch elements. A precondition failure should be thrown.
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* Fhir Server details were provided with the request. Smile CDR will use them to fetch missing resources before
|
||||
* calling the CDS Service
|
||||
*/
|
||||
FHIR_CLIENT,
|
||||
|
||||
/**
|
||||
* The CDS Hooks service method will be responsible for fetching the missing resources using the FHIR Server
|
||||
* in the request
|
||||
*/
|
||||
SERVICE,
|
||||
|
||||
/**
|
||||
* Missing prefetch elements will be retrieved directly from the configured Storage Module
|
||||
*/
|
||||
DAO
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Marks a method as a CDS Hooks service. A method annotated with `@CdsService(value="example-service")` is accessed
|
||||
* at a path like `<a href="https://example.com/cds-services/example-service">Example Service</a>`
|
||||
*
|
||||
* @see <a href="https://cds-hooks.hl7.org/ballots/2020Sep/">Version 1.1 of the CDS Hooks Specification</a>
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CdsService {
|
||||
/**
|
||||
* The {id} portion of the URL to this service which is available at
|
||||
* {baseUrl}/cds-services/{id}
|
||||
*/
|
||||
String value();
|
||||
|
||||
/**
|
||||
* The hook this service should be invoked on
|
||||
*/
|
||||
String hook();
|
||||
|
||||
/**
|
||||
* The human-friendly name of this service.
|
||||
*/
|
||||
String title() default "";
|
||||
|
||||
/**
|
||||
* The description of this service.
|
||||
*/
|
||||
String description();
|
||||
|
||||
/**
|
||||
* An object containing key/value pairs of FHIR queries that this service is requesting that the CDS Client prefetch
|
||||
* and provide on each service call. The key is a string that describes the type of data being requested and the value
|
||||
* is a string representing the FHIR query.
|
||||
*/
|
||||
CdsServicePrefetch[] prefetch();
|
||||
|
||||
/**
|
||||
* @return true if Smile CDR CDS Hooks can populate missing prefetch elements using the Fhir Server specified in the
|
||||
* request before calling the method. If false, then it is expected that the service will use the FHIR Server details and retrieve missing
|
||||
* prefetch elements itself.
|
||||
*/
|
||||
boolean allowAutoFhirClientPrefetch() default false;
|
||||
|
||||
/**
|
||||
* An arbitrary string which will be used to store stringify JSON
|
||||
*/
|
||||
String extension() default "";
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Marks a method as a CDS Hooks feedback service. A method annotated with `@CdsServiceFeedback(value="my-service")` is
|
||||
* accessed at a path like `<a href="https://example.com/cds-services/example-service/feedback">Example Feedback Service</a>`.
|
||||
*
|
||||
* @see <a href="https://cds-hooks.hl7.org/ballots/2020Sep/">Version 1.1 of the CDS Hooks Specification</a>
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CdsServiceFeedback {
|
||||
/**
|
||||
* The {id} portion of the URL to this service which is available at
|
||||
* {baseUrl}/cds-services/{id}/feedback
|
||||
*/
|
||||
String value();
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* An object containing a key/value pair of FHIR queries that a Cds Service is requesting that the CDS Client prefetch
|
||||
* and provide on each service call.
|
||||
*/
|
||||
@Target(ElementType.ANNOTATION_TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CdsServicePrefetch {
|
||||
/**
|
||||
* The type of data being requested
|
||||
*/
|
||||
String value();
|
||||
|
||||
/**
|
||||
* The FHIR Query that can be used to request the required data
|
||||
*/
|
||||
String query();
|
||||
|
||||
/**
|
||||
* The strategy used for this query, defaults to the service-wide strategy
|
||||
*/
|
||||
CdsResolutionStrategyEnum source() default CdsResolutionStrategyEnum.NONE;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public interface ICdsConfigService {
|
||||
@Nonnull
|
||||
FhirContext getFhirContext();
|
||||
|
||||
@Nonnull
|
||||
ObjectMapper getObjectMapper();
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api;
|
||||
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
public interface ICdsHooksDaoAuthorizationSvc {
|
||||
void authorizePreShow(IBaseResource theResource);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public interface ICdsMethod {
|
||||
Object invoke(ObjectMapper theObjectMapper, IModelJson theJson, String theServiceId);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api;
|
||||
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
|
||||
|
||||
public interface ICdsServiceMethod extends ICdsMethod {
|
||||
CdsServiceJson getCdsServiceJson();
|
||||
|
||||
boolean isAllowAutoFhirClientPrefetch();
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api;
|
||||
|
||||
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 java.util.function.Function;
|
||||
|
||||
/**
|
||||
* This registry holds all CDS Hooks services registered with the server.
|
||||
*/
|
||||
public interface ICdsServiceRegistry {
|
||||
/**
|
||||
* This is the json returned by calling https://example.com/cds-services
|
||||
*
|
||||
* @return a list of CDS Hooks service descriptors
|
||||
*/
|
||||
CdsServicesJson getCdsServicesJson();
|
||||
|
||||
/**
|
||||
* This is the REST method available at https://example.com/cds-services/{theServiceId}
|
||||
*
|
||||
* @param theServiceId the id of the service to be called
|
||||
* @param theCdsServiceRequestJson the service request
|
||||
* @return the service response
|
||||
*/
|
||||
CdsServiceResponseJson callService(String theServiceId, CdsServiceRequestJson theCdsServiceRequestJson);
|
||||
|
||||
/**
|
||||
* This is the REST method available at https://example.com/cds-services/{theServiceId}/feedback
|
||||
*
|
||||
* @param theServiceId the id of the service that feedback is being sent for
|
||||
* @param theCdsServiceFeedbackJson the request
|
||||
* @return the response
|
||||
*/
|
||||
String callFeedback(String theServiceId, CdsServiceFeedbackJson theCdsServiceFeedbackJson);
|
||||
|
||||
/**
|
||||
* Register a new CDS Service with the endpoint.
|
||||
*
|
||||
* @param theServiceId the id of the service
|
||||
* @param theServiceFunction the function that will be called to invoke the service
|
||||
* @param theCdsServiceJson the service descriptor
|
||||
* @param theAllowAutoFhirClientPrefetch Whether to allow the server to automatically prefetch resources
|
||||
* @param theModuleId the moduleId where the service is registered
|
||||
*/
|
||||
void registerService(
|
||||
String theServiceId,
|
||||
Function<CdsServiceRequestJson, CdsServiceResponseJson> theServiceFunction,
|
||||
CdsServiceJson theCdsServiceJson,
|
||||
boolean theAllowAutoFhirClientPrefetch,
|
||||
String theModuleId);
|
||||
|
||||
/**
|
||||
* Remove registered CDS service with the service ID, only removes dynamically registered service
|
||||
*
|
||||
* @param theServiceId the id of the service to be removed
|
||||
*/
|
||||
void unregisterService(String theServiceId, String theModuleId);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
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>
|
||||
*/
|
||||
public abstract class BaseCdsServiceJson implements IModelJson {
|
||||
|
||||
@JsonProperty(value = "extension", required = true)
|
||||
String myExtension;
|
||||
|
||||
public String getExtension() {
|
||||
return myExtension;
|
||||
}
|
||||
|
||||
public BaseCdsServiceJson setExtension(String theExtension) {
|
||||
this.myExtension = theExtension;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Represents the list of cards accepted in CdsServiceFeedback
|
||||
*/
|
||||
public class CdsServiceAcceptedSuggestionJson implements IModelJson {
|
||||
@JsonProperty(value = "id", required = true)
|
||||
String myId;
|
||||
|
||||
public String getId() {
|
||||
return myId;
|
||||
}
|
||||
|
||||
public CdsServiceAcceptedSuggestionJson setId(String theId) {
|
||||
myId = theId;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
public enum CdsServiceFeebackOutcomeEnum {
|
||||
accepted,
|
||||
overridden
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a CDS Hooks Service Feedback Request
|
||||
*
|
||||
* @see <a href="https://cds-hooks.hl7.org/ballots/2020Sep/">Version 1.1 of the CDS Hooks Specification</a>
|
||||
*/
|
||||
public class CdsServiceFeedbackJson implements IModelJson {
|
||||
@JsonProperty(value = "card", required = true)
|
||||
String myCard;
|
||||
|
||||
@JsonProperty(value = "outcome", required = true)
|
||||
CdsServiceFeebackOutcomeEnum myOutcome;
|
||||
|
||||
@JsonProperty(value = "acceptedSuggestions")
|
||||
List<CdsServiceAcceptedSuggestionJson> myAcceptedSuggestions;
|
||||
|
||||
@JsonProperty(value = "overrideReason")
|
||||
CdsServiceOverrideReasonJson myOverrideReason;
|
||||
|
||||
@JsonProperty(value = "outcomeTimestamp", required = true)
|
||||
String myOutcomeTimestamp;
|
||||
|
||||
public String getCard() {
|
||||
return myCard;
|
||||
}
|
||||
|
||||
public CdsServiceFeedbackJson setCard(String theCard) {
|
||||
myCard = theCard;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CdsServiceFeebackOutcomeEnum getOutcome() {
|
||||
return myOutcome;
|
||||
}
|
||||
|
||||
public CdsServiceFeedbackJson setOutcome(CdsServiceFeebackOutcomeEnum theOutcome) {
|
||||
myOutcome = theOutcome;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<CdsServiceAcceptedSuggestionJson> getAcceptedSuggestions() {
|
||||
return myAcceptedSuggestions;
|
||||
}
|
||||
|
||||
public CdsServiceFeedbackJson setAcceptedSuggestions(
|
||||
List<CdsServiceAcceptedSuggestionJson> theAcceptedSuggestions) {
|
||||
myAcceptedSuggestions = theAcceptedSuggestions;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CdsServiceOverrideReasonJson getOverrideReason() {
|
||||
return myOverrideReason;
|
||||
}
|
||||
|
||||
public CdsServiceFeedbackJson setOverrideReason(CdsServiceOverrideReasonJson theOverrideReason) {
|
||||
myOverrideReason = theOverrideReason;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getOutcomeTimestamp() {
|
||||
return myOutcomeTimestamp;
|
||||
}
|
||||
|
||||
public CdsServiceFeedbackJson setOutcomeTimestamp(String theOutcomeTimestamp) {
|
||||
myOutcomeTimestamp = theOutcomeTimestamp;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public enum CdsServiceIndicatorEnum {
|
||||
@JsonProperty("info")
|
||||
INFO,
|
||||
|
||||
@JsonProperty("warning")
|
||||
WARNING,
|
||||
|
||||
@JsonProperty("critical")
|
||||
CRITICAL
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.CdsResolutionStrategyEnum;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents a CDS Hooks Service descriptor
|
||||
*
|
||||
* @see <a href="https://cds-hooks.hl7.org/ballots/2020Sep/">Version 1.1 of the CDS Hooks Specification</a>
|
||||
*/
|
||||
public class CdsServiceJson extends BaseCdsServiceJson implements IModelJson {
|
||||
public static final String HOOK = "hook";
|
||||
public static final String TITLE = "title";
|
||||
public static final String DESCRIPTION = "description";
|
||||
public static final String ID = "id";
|
||||
public static final String PREFETCH = "prefetch";
|
||||
|
||||
@JsonProperty(value = HOOK, required = true)
|
||||
String myHook;
|
||||
|
||||
@JsonProperty(value = TITLE)
|
||||
String myTitle;
|
||||
|
||||
@JsonProperty(value = DESCRIPTION, required = true)
|
||||
String myDescription;
|
||||
|
||||
@JsonProperty(value = ID, required = true)
|
||||
String myId;
|
||||
|
||||
@JsonProperty(PREFETCH)
|
||||
private Map<String, String> myPrefetch;
|
||||
|
||||
private Map<String, CdsResolutionStrategyEnum> mySource;
|
||||
|
||||
public String getHook() {
|
||||
return myHook;
|
||||
}
|
||||
|
||||
public CdsServiceJson setHook(String theHook) {
|
||||
myHook = theHook;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return myTitle;
|
||||
}
|
||||
|
||||
public CdsServiceJson setTitle(String theTitle) {
|
||||
myTitle = theTitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return myDescription;
|
||||
}
|
||||
|
||||
public CdsServiceJson setDescription(String theDescription) {
|
||||
myDescription = theDescription;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return myId;
|
||||
}
|
||||
|
||||
public CdsServiceJson setId(String theId) {
|
||||
myId = theId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void addPrefetch(String theKey, String theQuery) {
|
||||
if (myPrefetch == null) {
|
||||
myPrefetch = new LinkedHashMap<>();
|
||||
}
|
||||
myPrefetch.put(theKey, theQuery);
|
||||
}
|
||||
|
||||
public Map<String, String> getPrefetch() {
|
||||
if (myPrefetch == null) {
|
||||
myPrefetch = new LinkedHashMap<>();
|
||||
}
|
||||
return Collections.unmodifiableMap(myPrefetch);
|
||||
}
|
||||
|
||||
public void addSource(String theKey, CdsResolutionStrategyEnum theSource) {
|
||||
if (mySource == null) {
|
||||
mySource = new LinkedHashMap<>();
|
||||
}
|
||||
mySource.put(theKey, theSource);
|
||||
}
|
||||
|
||||
public Map<String, CdsResolutionStrategyEnum> getSource() {
|
||||
if (mySource == null) {
|
||||
mySource = new LinkedHashMap<>();
|
||||
}
|
||||
return Collections.unmodifiableMap(mySource);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Represents the reason a specific service suggestion was overridden
|
||||
*/
|
||||
public class CdsServiceOverrideReasonJson implements IModelJson {
|
||||
@JsonProperty("reason")
|
||||
CdsServiceResponseCodingJson myReason;
|
||||
|
||||
@JsonProperty("userComment")
|
||||
String myUserComment;
|
||||
|
||||
public CdsServiceResponseCodingJson getReason() {
|
||||
return myReason;
|
||||
}
|
||||
|
||||
public CdsServiceOverrideReasonJson setReason(CdsServiceResponseCodingJson theReason) {
|
||||
myReason = theReason;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUserComment() {
|
||||
return myUserComment;
|
||||
}
|
||||
|
||||
public CdsServiceOverrideReasonJson setUserComment(String theUserComment) {
|
||||
myUserComment = theUserComment;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* A structure holding an OAuth 2.0 bearer access token granting the CDS Service access to FHIR resource
|
||||
*/
|
||||
public class CdsServiceRequestAuthorizationJson extends BaseCdsServiceJson implements IModelJson {
|
||||
@JsonProperty(value = "access_token", required = true)
|
||||
String myAccessToken;
|
||||
|
||||
@JsonProperty(value = "token_type", required = true)
|
||||
String myTokenType;
|
||||
|
||||
@JsonProperty(value = "expires_in", required = true)
|
||||
Long myExpiresIn;
|
||||
|
||||
@JsonProperty(value = "scope", required = true)
|
||||
String myScope;
|
||||
|
||||
@JsonProperty(value = "subject", required = true)
|
||||
String mySubject;
|
||||
|
||||
public String getAccessToken() {
|
||||
return myAccessToken;
|
||||
}
|
||||
|
||||
public CdsServiceRequestAuthorizationJson setAccessToken(String theAccessToken) {
|
||||
myAccessToken = theAccessToken;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getTokenType() {
|
||||
return myTokenType;
|
||||
}
|
||||
|
||||
public CdsServiceRequestAuthorizationJson setTokenType(String theTokenType) {
|
||||
myTokenType = theTokenType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Long getExpiresIn() {
|
||||
return myExpiresIn;
|
||||
}
|
||||
|
||||
public CdsServiceRequestAuthorizationJson setExpiresIn(Long theExpiresIn) {
|
||||
myExpiresIn = theExpiresIn;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getScope() {
|
||||
return myScope;
|
||||
}
|
||||
|
||||
public CdsServiceRequestAuthorizationJson setScope(String theScope) {
|
||||
myScope = theScope;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return mySubject;
|
||||
}
|
||||
|
||||
public CdsServiceRequestAuthorizationJson setSubject(String theSubject) {
|
||||
mySubject = theSubject;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class CdsServiceRequestContextJson extends BaseCdsServiceJson implements IModelJson {
|
||||
private Map<String, Object> myMap;
|
||||
|
||||
public String getString(String theKey) {
|
||||
if (myMap == null) {
|
||||
return null;
|
||||
}
|
||||
return (String) myMap.get(theKey);
|
||||
}
|
||||
|
||||
public List<String> getArray(String theKey) {
|
||||
if (myMap == null) {
|
||||
return null;
|
||||
}
|
||||
return (List<String>) myMap.get(theKey);
|
||||
}
|
||||
|
||||
public IBaseResource getResource(String theKey) {
|
||||
if (myMap == null) {
|
||||
return null;
|
||||
}
|
||||
return (IBaseResource) myMap.get(theKey);
|
||||
}
|
||||
|
||||
public void put(String theKey, Object theValue) {
|
||||
if (myMap == null) {
|
||||
myMap = new LinkedHashMap<>();
|
||||
}
|
||||
myMap.put(theKey, theValue);
|
||||
}
|
||||
|
||||
public Set<String> getKeys() {
|
||||
return myMap.keySet();
|
||||
}
|
||||
|
||||
public Object get(String theKey) {
|
||||
return myMap.get(theKey);
|
||||
}
|
||||
|
||||
public boolean containsKey(String theKey) {
|
||||
return myMap.containsKey(theKey);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Represents a CDS Hooks Service Request
|
||||
*
|
||||
* @see <a href="https://cds-hooks.hl7.org/ballots/2020Sep/">Version 1.1 of the CDS Hooks Specification</a>
|
||||
*/
|
||||
public class CdsServiceRequestJson extends BaseCdsServiceJson implements IModelJson {
|
||||
@JsonProperty(value = "hook", required = true)
|
||||
String myHook;
|
||||
|
||||
@JsonProperty(value = "hookInstance", required = true)
|
||||
String myHookInstance;
|
||||
|
||||
@JsonProperty("fhirServer")
|
||||
String myFhirServer;
|
||||
|
||||
@JsonProperty("fhirAuthorization")
|
||||
CdsServiceRequestAuthorizationJson myServiceRequestAuthorizationJson;
|
||||
|
||||
@JsonProperty(value = "context", required = true)
|
||||
CdsServiceRequestContextJson myContext;
|
||||
|
||||
@JsonProperty("prefetch")
|
||||
Map<String, IBaseResource> myPrefetch;
|
||||
|
||||
public String getHookInstance() {
|
||||
return myHookInstance;
|
||||
}
|
||||
|
||||
public CdsServiceRequestJson setHookInstance(String theHookInstance) {
|
||||
myHookInstance = theHookInstance;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getFhirServer() {
|
||||
return myFhirServer;
|
||||
}
|
||||
|
||||
public CdsServiceRequestJson setFhirServer(String theFhirServer) {
|
||||
myFhirServer = theFhirServer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getHook() {
|
||||
return myHook;
|
||||
}
|
||||
|
||||
public CdsServiceRequestJson setHook(String theHook) {
|
||||
myHook = theHook;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CdsServiceRequestContextJson getContext() {
|
||||
if (myContext == null) {
|
||||
myContext = new CdsServiceRequestContextJson();
|
||||
}
|
||||
return myContext;
|
||||
}
|
||||
|
||||
public CdsServiceRequestJson setContext(CdsServiceRequestContextJson theContext) {
|
||||
myContext = theContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CdsServiceRequestAuthorizationJson getServiceRequestAuthorizationJson() {
|
||||
if (myServiceRequestAuthorizationJson == null) {
|
||||
myServiceRequestAuthorizationJson = new CdsServiceRequestAuthorizationJson();
|
||||
}
|
||||
return myServiceRequestAuthorizationJson;
|
||||
}
|
||||
|
||||
public CdsServiceRequestJson setServiceRequestAuthorizationJson(
|
||||
CdsServiceRequestAuthorizationJson theServiceRequestAuthorizationJson) {
|
||||
myServiceRequestAuthorizationJson = theServiceRequestAuthorizationJson;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void addPrefetch(String theKey, IBaseResource theResource) {
|
||||
if (myPrefetch == null) {
|
||||
myPrefetch = new LinkedHashMap<>();
|
||||
}
|
||||
myPrefetch.put(theKey, theResource);
|
||||
}
|
||||
|
||||
public IBaseResource getPrefetch(String theKey) {
|
||||
if (myPrefetch == null) {
|
||||
return null;
|
||||
}
|
||||
return myPrefetch.get(theKey);
|
||||
}
|
||||
|
||||
public void addContext(String theKey, Object theValue) {
|
||||
getContext().put(theKey, theValue);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Set<String> getPrefetchKeys() {
|
||||
if (myPrefetch == null) {
|
||||
return new HashSet<>();
|
||||
}
|
||||
return Collections.unmodifiableSet(myPrefetch.keySet());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a CDS Hooks Service Response Card
|
||||
*/
|
||||
public class CdsServiceResponseCardJson extends BaseCdsServiceJson implements IModelJson {
|
||||
@JsonProperty("uuid")
|
||||
String myUuid;
|
||||
|
||||
@JsonProperty(value = "summary", required = true)
|
||||
String mySummary;
|
||||
|
||||
@JsonProperty("detail")
|
||||
String myDetail;
|
||||
|
||||
@JsonProperty(value = "indicator", required = true)
|
||||
CdsServiceIndicatorEnum myIndicator;
|
||||
|
||||
@JsonProperty(value = "source", required = true)
|
||||
CdsServiceResponseCardSourceJson mySource;
|
||||
|
||||
@JsonProperty("suggestions")
|
||||
List<CdsServiceResponseSuggestionJson> mySuggestions;
|
||||
|
||||
@JsonProperty("selectionBehavior")
|
||||
String mySelectionBehaviour;
|
||||
|
||||
@JsonProperty("overrideReasons")
|
||||
List<CdsServiceResponseCodingJson> myOverrideReasons;
|
||||
|
||||
@JsonProperty("links")
|
||||
List<CdsServiceResponseLinkJson> myLinks;
|
||||
|
||||
public String getSummary() {
|
||||
return mySummary;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCardJson setSummary(String theSummary) {
|
||||
mySummary = theSummary;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CdsServiceIndicatorEnum getIndicator() {
|
||||
return myIndicator;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCardJson setIndicator(CdsServiceIndicatorEnum theIndicator) {
|
||||
myIndicator = theIndicator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCardSourceJson getSource() {
|
||||
return mySource;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCardJson setSource(CdsServiceResponseCardSourceJson theSource) {
|
||||
mySource = theSource;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDetail() {
|
||||
return myDetail;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCardJson setDetail(String theDetail) {
|
||||
myDetail = theDetail;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUuid() {
|
||||
return myUuid;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCardJson setUuid(String theUuid) {
|
||||
myUuid = theUuid;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<CdsServiceResponseSuggestionJson> getSuggestions() {
|
||||
return mySuggestions;
|
||||
}
|
||||
|
||||
public void addSuggestion(CdsServiceResponseSuggestionJson theSuggestion) {
|
||||
if (mySuggestions == null) {
|
||||
mySuggestions = new ArrayList<>();
|
||||
}
|
||||
mySuggestions.add(theSuggestion);
|
||||
}
|
||||
|
||||
public String getSelectionBehaviour() {
|
||||
return mySelectionBehaviour;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCardJson setSelectionBehaviour(String theSelectionBehaviour) {
|
||||
mySelectionBehaviour = theSelectionBehaviour;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<CdsServiceResponseCodingJson> getOverrideReasons() {
|
||||
return myOverrideReasons;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCardJson setOverrideReasons(List<CdsServiceResponseCodingJson> theOverrideReasons) {
|
||||
myOverrideReasons = theOverrideReasons;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<CdsServiceResponseLinkJson> getLinks() {
|
||||
return myLinks;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCardJson setLinks(List<CdsServiceResponseLinkJson> theLinks) {
|
||||
myLinks = theLinks;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Represents a CDS Hooks Service Response Card Source
|
||||
*/
|
||||
public class CdsServiceResponseCardSourceJson implements IModelJson {
|
||||
@JsonProperty(value = "label", required = true)
|
||||
String myLabel;
|
||||
|
||||
@JsonProperty("url")
|
||||
String myUrl;
|
||||
|
||||
@JsonProperty("icon")
|
||||
String myIcon;
|
||||
|
||||
@JsonProperty("topic")
|
||||
CdsServiceResponseCodingJson myTopic;
|
||||
|
||||
public String getLabel() {
|
||||
return myLabel;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCardSourceJson setLabel(String theLabel) {
|
||||
myLabel = theLabel;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return myUrl;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCardSourceJson setUrl(String theUrl) {
|
||||
myUrl = theUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
return myIcon;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCardSourceJson setIcon(String theIcon) {
|
||||
myIcon = theIcon;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCodingJson getTopic() {
|
||||
return myTopic;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCardSourceJson setTopic(CdsServiceResponseCodingJson theTopic) {
|
||||
myTopic = theTopic;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Coding using within CdsService responses
|
||||
*/
|
||||
public class CdsServiceResponseCodingJson implements IModelJson {
|
||||
@JsonProperty(value = "code", required = true)
|
||||
String myCode;
|
||||
|
||||
@JsonProperty("system")
|
||||
String mySystem;
|
||||
|
||||
@JsonProperty("display")
|
||||
String myDisplay;
|
||||
|
||||
public String getCode() {
|
||||
return myCode;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCodingJson setCode(String theCode) {
|
||||
myCode = theCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getSystem() {
|
||||
return mySystem;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCodingJson setSystem(String theSystem) {
|
||||
mySystem = theSystem;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDisplay() {
|
||||
return myDisplay;
|
||||
}
|
||||
|
||||
public CdsServiceResponseCodingJson setDisplay(String theDisplay) {
|
||||
myDisplay = theDisplay;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a CDS Hooks Service Response
|
||||
*
|
||||
* @see <a href="https://cds-hooks.hl7.org/ballots/2020Sep/">Version 1.1 of the CDS Hooks Specification</a>
|
||||
*/
|
||||
public class CdsServiceResponseJson implements IModelJson {
|
||||
@JsonProperty(value = "cards", required = true)
|
||||
private final List<CdsServiceResponseCardJson> myCards = new ArrayList<>();
|
||||
|
||||
@JsonProperty("systemActions")
|
||||
List<CdsServiceResponseSystemActionJson> myServiceActions;
|
||||
|
||||
public void addCard(CdsServiceResponseCardJson theCdsServiceResponseCardJson) {
|
||||
myCards.add(theCdsServiceResponseCardJson);
|
||||
}
|
||||
|
||||
public List<CdsServiceResponseCardJson> getCards() {
|
||||
return myCards;
|
||||
}
|
||||
|
||||
public void addServiceAction(CdsServiceResponseSystemActionJson theCdsServiceResponseSystemActionJson) {
|
||||
if (myServiceActions == null) {
|
||||
myServiceActions = new ArrayList<>();
|
||||
}
|
||||
myServiceActions.add(theCdsServiceResponseSystemActionJson);
|
||||
}
|
||||
|
||||
public List<CdsServiceResponseSystemActionJson> getServiceActions() {
|
||||
return myServiceActions;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Link used within a Cds Service Response
|
||||
*/
|
||||
public class CdsServiceResponseLinkJson implements IModelJson {
|
||||
@JsonProperty(value = "label", required = true)
|
||||
String myLabel;
|
||||
|
||||
@JsonProperty(value = "url", required = true)
|
||||
String myUrl;
|
||||
|
||||
@JsonProperty(value = "type", required = true)
|
||||
String myType;
|
||||
|
||||
@JsonProperty(value = "appContext")
|
||||
String myAppContext;
|
||||
|
||||
public String getLabel() {
|
||||
return myLabel;
|
||||
}
|
||||
|
||||
public CdsServiceResponseLinkJson setLabel(String theLabel) {
|
||||
myLabel = theLabel;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return myUrl;
|
||||
}
|
||||
|
||||
public CdsServiceResponseLinkJson setUrl(String theUrl) {
|
||||
myUrl = theUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return myType;
|
||||
}
|
||||
|
||||
public CdsServiceResponseLinkJson setType(String theType) {
|
||||
myType = theType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getAppContext() {
|
||||
return myAppContext;
|
||||
}
|
||||
|
||||
public CdsServiceResponseLinkJson setAppContext(String theAppContext) {
|
||||
myAppContext = theAppContext;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
/**
|
||||
* A Suggested Action
|
||||
*/
|
||||
public class CdsServiceResponseSuggestionActionJson extends BaseCdsServiceJson implements IModelJson {
|
||||
@JsonProperty(value = "type", required = true)
|
||||
String myType;
|
||||
|
||||
@JsonProperty("description")
|
||||
String myDescription;
|
||||
|
||||
@JsonProperty("resource")
|
||||
IBaseResource myResource;
|
||||
|
||||
public String getType() {
|
||||
return myType;
|
||||
}
|
||||
|
||||
public CdsServiceResponseSuggestionActionJson setType(String theType) {
|
||||
myType = theType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return myDescription;
|
||||
}
|
||||
|
||||
public CdsServiceResponseSuggestionActionJson setDescription(String theDescription) {
|
||||
myDescription = theDescription;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IBaseResource getResource() {
|
||||
return myResource;
|
||||
}
|
||||
|
||||
public CdsServiceResponseSuggestionActionJson setResource(IBaseResource theResource) {
|
||||
myResource = theResource;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Allows a service to suggest a set of changes in the context of the current activity
|
||||
*/
|
||||
public class CdsServiceResponseSuggestionJson implements IModelJson {
|
||||
@JsonProperty(value = "label", required = true)
|
||||
String myLabel;
|
||||
|
||||
@JsonProperty("uuid")
|
||||
String myUuid;
|
||||
|
||||
@JsonProperty("isRecommended")
|
||||
Boolean myRecommended;
|
||||
|
||||
@JsonProperty("actions")
|
||||
List<CdsServiceResponseSuggestionActionJson> myActions;
|
||||
|
||||
public String getLabel() {
|
||||
return myLabel;
|
||||
}
|
||||
|
||||
public CdsServiceResponseSuggestionJson setLabel(String theLabel) {
|
||||
myLabel = theLabel;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUuid() {
|
||||
return myUuid;
|
||||
}
|
||||
|
||||
public CdsServiceResponseSuggestionJson setUuid(String theUuid) {
|
||||
myUuid = theUuid;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Boolean getRecommended() {
|
||||
return myRecommended;
|
||||
}
|
||||
|
||||
public CdsServiceResponseSuggestionJson setRecommended(Boolean theRecommended) {
|
||||
myRecommended = theRecommended;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<CdsServiceResponseSuggestionActionJson> getActions() {
|
||||
return myActions;
|
||||
}
|
||||
|
||||
public void addAction(CdsServiceResponseSuggestionActionJson theAction) {
|
||||
if (myActions == null) {
|
||||
myActions = new ArrayList<>();
|
||||
}
|
||||
myActions.add(theAction);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
/**
|
||||
* Represents a CDS Service Response System Action
|
||||
*/
|
||||
public class CdsServiceResponseSystemActionJson extends BaseCdsServiceJson implements IModelJson {
|
||||
@JsonProperty(value = "type", required = true)
|
||||
String myType;
|
||||
|
||||
@JsonProperty(value = "description", required = true)
|
||||
String myDescription;
|
||||
|
||||
@JsonProperty(value = "resource")
|
||||
IBaseResource myResource;
|
||||
|
||||
public String getType() {
|
||||
return myType;
|
||||
}
|
||||
|
||||
public CdsServiceResponseSystemActionJson setType(String theType) {
|
||||
myType = theType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return myDescription;
|
||||
}
|
||||
|
||||
public CdsServiceResponseSystemActionJson setDescription(String theDescription) {
|
||||
myDescription = theDescription;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IBaseResource getResource() {
|
||||
return myResource;
|
||||
}
|
||||
|
||||
public CdsServiceResponseSystemActionJson setResource(IBaseResource theResource) {
|
||||
myResource = theResource;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a list of CDS Hooks Service descriptors
|
||||
*
|
||||
* @see <a href="https://cds-hooks.hl7.org/ballots/2020Sep/">Version 1.1 of the CDS Hooks Specification</a>
|
||||
*/
|
||||
public class CdsServicesJson implements IModelJson {
|
||||
@JsonProperty("services")
|
||||
private List<CdsServiceJson> myServices;
|
||||
|
||||
public CdsServicesJson addService(CdsServiceJson theCdsServiceJson) {
|
||||
if (myServices == null) {
|
||||
myServices = new ArrayList<>();
|
||||
}
|
||||
myServices.add(theCdsServiceJson);
|
||||
return this;
|
||||
}
|
||||
|
||||
public CdsServicesJson removeService(CdsServiceJson theCdsServiceJson) {
|
||||
if (myServices == null) {
|
||||
myServices = new ArrayList<>();
|
||||
}
|
||||
myServices.remove(theCdsServiceJson);
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<CdsServiceJson> getServices() {
|
||||
return myServices;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.config;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsConfigService;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsHooksDaoAuthorizationSvc;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry;
|
||||
import ca.uhn.hapi.fhir.cdshooks.module.CdsHooksObjectMapperFactory;
|
||||
import ca.uhn.hapi.fhir.cdshooks.svc.CdsConfigServiceImpl;
|
||||
import ca.uhn.hapi.fhir.cdshooks.svc.CdsHooksContextBooter;
|
||||
import ca.uhn.hapi.fhir.cdshooks.svc.CdsServiceRegistryImpl;
|
||||
import ca.uhn.hapi.fhir.cdshooks.svc.prefetch.CdsPrefetchDaoSvc;
|
||||
import ca.uhn.hapi.fhir.cdshooks.svc.prefetch.CdsPrefetchFhirClientSvc;
|
||||
import ca.uhn.hapi.fhir.cdshooks.svc.prefetch.CdsPrefetchSvc;
|
||||
import ca.uhn.hapi.fhir.cdshooks.svc.prefetch.CdsResolutionStrategySvc;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class CdsHooksConfig {
|
||||
|
||||
public static final String CDS_HOOKS_OBJECT_MAPPER_FACTORY = "cdsHooksObjectMapperFactory";
|
||||
|
||||
@Autowired(required = false)
|
||||
private DaoRegistry myDaoRegistry;
|
||||
|
||||
@Autowired(required = false)
|
||||
private MatchUrlService myMatchUrlService;
|
||||
|
||||
@Bean(name = CDS_HOOKS_OBJECT_MAPPER_FACTORY)
|
||||
public ObjectMapper objectMapper(FhirContext theFhirContext) {
|
||||
return new CdsHooksObjectMapperFactory(theFhirContext).newMapper();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ICdsServiceRegistry cdsServiceRegistry(
|
||||
CdsHooksContextBooter theCdsHooksContextBooter,
|
||||
CdsPrefetchSvc theCdsPrefetchSvc,
|
||||
@Qualifier(CDS_HOOKS_OBJECT_MAPPER_FACTORY) ObjectMapper theObjectMapper) {
|
||||
return new CdsServiceRegistryImpl(theCdsHooksContextBooter, theCdsPrefetchSvc, theObjectMapper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ICdsConfigService cdsConfigService(
|
||||
FhirContext theFhirContext, @Qualifier(CDS_HOOKS_OBJECT_MAPPER_FACTORY) ObjectMapper theObjectMapper) {
|
||||
return new CdsConfigServiceImpl(theFhirContext, theObjectMapper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
CdsPrefetchSvc cdsPrefetchSvc(
|
||||
CdsResolutionStrategySvc theCdsResolutionStrategySvc,
|
||||
CdsPrefetchDaoSvc theResourcePrefetchDao,
|
||||
CdsPrefetchFhirClientSvc theResourcePrefetchFhirClient,
|
||||
ICdsHooksDaoAuthorizationSvc theCdsHooksDaoAuthorizationSvc) {
|
||||
return new CdsPrefetchSvc(
|
||||
theCdsResolutionStrategySvc,
|
||||
theResourcePrefetchDao,
|
||||
theResourcePrefetchFhirClient,
|
||||
theCdsHooksDaoAuthorizationSvc);
|
||||
}
|
||||
|
||||
@Bean
|
||||
CdsPrefetchDaoSvc resourcePrefetchDao(DaoRegistry theDaoRegistry, FhirContext theFhirContext) {
|
||||
return new CdsPrefetchDaoSvc(theDaoRegistry, myMatchUrlService, theFhirContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
CdsPrefetchFhirClientSvc resourcePrefetchFhirClient(FhirContext theFhirContext) {
|
||||
return new CdsPrefetchFhirClientSvc(theFhirContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
CdsResolutionStrategySvc cdsResolutionStrategySvc() {
|
||||
return new CdsResolutionStrategySvc(myDaoRegistry);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.controller;
|
||||
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceFeedbackJson;
|
||||
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 org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = CdsHooksController.BASE)
|
||||
public class CdsHooksController {
|
||||
static final String BASE = "/cds-services";
|
||||
private final ICdsServiceRegistry myCdsServiceRegistry;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public CdsHooksController(ICdsServiceRegistry theCdsServiceRegistry) {
|
||||
super();
|
||||
myCdsServiceRegistry = theCdsServiceRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of CDS Hooks service descriptors.
|
||||
* This method is idempotent.
|
||||
*/
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@RequestMapping(
|
||||
path = "",
|
||||
method = {RequestMethod.GET},
|
||||
produces = {MediaType.APPLICATION_JSON_VALUE})
|
||||
public ResponseEntity<CdsServicesJson> cdsServices() {
|
||||
CdsServicesJson response = myCdsServiceRegistry.getCdsServicesJson();
|
||||
return ResponseEntity.status(200)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a specific CDS-Hook service
|
||||
*/
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@RequestMapping(
|
||||
path = "{cds_hook}",
|
||||
method = {RequestMethod.POST},
|
||||
consumes = {MediaType.APPLICATION_JSON_VALUE})
|
||||
public ResponseEntity<CdsServiceResponseJson> cdsServiceRequest(
|
||||
@PathVariable("cds_hook") String theCdsHook, @RequestBody CdsServiceRequestJson theCdsServiceRequestJson) {
|
||||
CdsServiceResponseJson response = myCdsServiceRegistry.callService(theCdsHook, theCdsServiceRequestJson);
|
||||
return ResponseEntity.status(200)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Here is the description of the CDS Hooks feedback method from the CDS Hooks specification:
|
||||
* <p>
|
||||
* Once a CDS Hooks service responds to a hook by returning a card, the service has no further interaction with the CDS client. The acceptance of a suggestion or rejection of a card is valuable information to enable a service to improve its behavior towards the goal of the end-user having a positive and meaningful experience with the CDS. A feedback endpoint enables suggestion tracking & analytics.
|
||||
* <p>
|
||||
* Upon receiving a card, a user may accept its suggestions, ignore it entirely, or dismiss it with or without an override reason. Note that while one or more suggestions can be accepted, an entire card is either ignored or overridden.
|
||||
* <p>
|
||||
* Typically, an end user may only accept (a suggestion), or override a card once; however, a card once ignored could later be acted upon. CDS Hooks does not specify the UI behavior of CDS clients, including the persistence of cards. CDS clients should faithfully report each of these distinct end-user interactions as feedback.
|
||||
*/
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@RequestMapping(
|
||||
path = "{cds_hook}/feedback",
|
||||
method = {RequestMethod.POST},
|
||||
consumes = {MediaType.APPLICATION_JSON_VALUE})
|
||||
public ResponseEntity<String> cdsServiceFeedback(
|
||||
@PathVariable("cds_hook") String theCdsHook,
|
||||
@RequestBody CdsServiceFeedbackJson theCdsServiceFeedbackJson) {
|
||||
String json = myCdsServiceRegistry.callFeedback(theCdsHook, theCdsServiceFeedbackJson);
|
||||
|
||||
return ResponseEntity.status(200)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(json);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.module;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.serializer.FhirResourceDeserializer;
|
||||
import ca.uhn.fhir.serializer.FhirResourceSerializer;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestContextJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.serializer.CdsServiceRequestContextDeserializer;
|
||||
import ca.uhn.hapi.fhir.cdshooks.serializer.CdsServiceRequestContextSerializer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
|
||||
public class CdsHooksObjectMapperFactory extends ObjectMapper {
|
||||
private final FhirContext myFhirContext;
|
||||
|
||||
public CdsHooksObjectMapperFactory(FhirContext theFhirContext) {
|
||||
myFhirContext = theFhirContext;
|
||||
}
|
||||
|
||||
public ObjectMapper newMapper() {
|
||||
Jackson2ObjectMapperBuilder b = new Jackson2ObjectMapperBuilder();
|
||||
b.indentOutput(true);
|
||||
ObjectMapper retval = b.build();
|
||||
SimpleModule module = new SimpleModule();
|
||||
module.addSerializer(new FhirResourceSerializer(myFhirContext));
|
||||
module.addSerializer(new CdsServiceRequestContextSerializer(myFhirContext, retval));
|
||||
module.addDeserializer(IBaseResource.class, new FhirResourceDeserializer(myFhirContext));
|
||||
module.addDeserializer(
|
||||
CdsServiceRequestContextJson.class, new CdsServiceRequestContextDeserializer(myFhirContext, retval));
|
||||
retval.registerModule(module);
|
||||
return retval;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.serializer;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.parser.IParser;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestContextJson;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
public class CdsServiceRequestContextDeserializer extends StdDeserializer<CdsServiceRequestContextJson> {
|
||||
private final IParser myParser;
|
||||
private final ObjectMapper myObjectMapper;
|
||||
|
||||
public CdsServiceRequestContextDeserializer(FhirContext theFhirContext, ObjectMapper theObjectMapper) {
|
||||
super(CdsServiceRequestContextJson.class);
|
||||
myParser = theFhirContext.newJsonParser().setPrettyPrint(true);
|
||||
myObjectMapper = theObjectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CdsServiceRequestContextJson deserialize(JsonParser theJsonParser, DeserializationContext theContext)
|
||||
throws IOException {
|
||||
// First deserialize the context as a LinkedHashMap
|
||||
LinkedHashMap<String, Object> map = myObjectMapper.readValue(
|
||||
theJsonParser.getCodec().readTree(theJsonParser).toString(), LinkedHashMap.class);
|
||||
CdsServiceRequestContextJson retval = new CdsServiceRequestContextJson();
|
||||
|
||||
for (String key : map.keySet()) {
|
||||
Object value = map.get(key);
|
||||
// Convert LinkedHashMap entries to Resources
|
||||
if (value instanceof LinkedHashMap) {
|
||||
String json = myObjectMapper.writeValueAsString(value);
|
||||
IBaseResource resource = myParser.parseResource(json);
|
||||
retval.put(key, resource);
|
||||
} else {
|
||||
retval.put(key, value);
|
||||
}
|
||||
}
|
||||
return retval;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.serializer;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.parser.IParser;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestContextJson;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CdsServiceRequestContextSerializer extends StdSerializer<CdsServiceRequestContextJson> {
|
||||
private final IParser myParser;
|
||||
private final ObjectMapper myObjectMapper;
|
||||
|
||||
public CdsServiceRequestContextSerializer(FhirContext theFhirContext, ObjectMapper theObjectMapper) {
|
||||
super(CdsServiceRequestContextJson.class);
|
||||
myParser = theFhirContext.newJsonParser().setPrettyPrint(true);
|
||||
myObjectMapper = theObjectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(
|
||||
CdsServiceRequestContextJson theContext, JsonGenerator theJsonGenerator, SerializerProvider theProvider)
|
||||
throws IOException {
|
||||
theJsonGenerator.writeStartObject();
|
||||
for (String key : theContext.getKeys()) {
|
||||
theJsonGenerator.writeFieldName(key);
|
||||
Object value = theContext.get(key);
|
||||
String json;
|
||||
if (value instanceof IBaseResource) {
|
||||
IBaseResource resource = (IBaseResource) value;
|
||||
json = myParser.encodeResourceToString(resource);
|
||||
} else {
|
||||
json = myObjectMapper.writeValueAsString(value);
|
||||
}
|
||||
theJsonGenerator.writeRawValue(json);
|
||||
}
|
||||
theJsonGenerator.writeEndObject();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.svc;
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsMethod;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
abstract class BaseCdsMethod implements ICdsMethod {
|
||||
private final Object myServiceBean;
|
||||
private final Method myMethod;
|
||||
|
||||
BaseCdsMethod(Object theServiceBean, Method theMethod) {
|
||||
myServiceBean = theServiceBean;
|
||||
myMethod = theMethod;
|
||||
}
|
||||
|
||||
public Object invoke(ObjectMapper theObjectMapper, IModelJson theJson, String theServiceId) {
|
||||
try {
|
||||
// If the method takes a String parameter, first serialize the json request before calling the method
|
||||
if (parameterIsString()) {
|
||||
String json = encodeRequest(theObjectMapper, theJson, theServiceId);
|
||||
|
||||
return myMethod.invoke(myServiceBean, json);
|
||||
} else {
|
||||
return myMethod.invoke(myServiceBean, theJson);
|
||||
}
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
if (e.getCause() != null && e.getCause() instanceof BaseServerResponseException) {
|
||||
throw (BaseServerResponseException) e.getCause();
|
||||
}
|
||||
throw new ConfigurationException(
|
||||
Msg.code(2376) + "Failed to invoke "
|
||||
+ myMethod.getName() + " method on "
|
||||
+ myServiceBean.getClass().getName(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean parameterIsString() {
|
||||
return String.class.isAssignableFrom(myMethod.getParameterTypes()[0]);
|
||||
}
|
||||
|
||||
private String encodeRequest(
|
||||
ObjectMapper theObjectMapper, IModelJson theCdsServiceRequestJson, String theServiceId) {
|
||||
try {
|
||||
return theObjectMapper.writeValueAsString(theCdsServiceRequestJson);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new InvalidRequestException(
|
||||
Msg.code(2377)
|
||||
+ "Failed to deserialize CDS Hooks service request json instance when calling CDS Hooks Service "
|
||||
+ theServiceId,
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.svc;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsMethod;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.util.function.Function;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
abstract class BaseDynamicCdsServiceMethod implements ICdsMethod {
|
||||
private final Function<CdsServiceRequestJson, CdsServiceResponseJson> myFunction;
|
||||
|
||||
BaseDynamicCdsServiceMethod(Function<CdsServiceRequestJson, CdsServiceResponseJson> theFunction) {
|
||||
myFunction = theFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(ObjectMapper theObjectMapper, IModelJson theJson, String theServiceId) {
|
||||
return myFunction.apply((CdsServiceRequestJson) theJson);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Function<CdsServiceRequestJson, CdsServiceResponseJson> getFunction() {
|
||||
return myFunction;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.svc;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsConfigService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class CdsConfigServiceImpl implements ICdsConfigService {
|
||||
private final FhirContext myFhirContext;
|
||||
private final ObjectMapper myObjectMapper;
|
||||
|
||||
public CdsConfigServiceImpl(@Nonnull FhirContext theFhirContext, @Nonnull ObjectMapper theObjectMapper) {
|
||||
myFhirContext = theFhirContext;
|
||||
myObjectMapper = theObjectMapper;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public FhirContext getFhirContext() {
|
||||
return myFhirContext;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ObjectMapper getObjectMapper() {
|
||||
return myObjectMapper;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.svc;
|
||||
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceMethod;
|
||||
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 java.util.function.Function;
|
||||
|
||||
public class CdsDynamicPrefetchableServiceMethod extends BaseDynamicCdsServiceMethod implements ICdsServiceMethod {
|
||||
private final CdsServiceJson myCdsServiceJson;
|
||||
private final boolean myAllowAutoFhirClientPrefetch;
|
||||
|
||||
public CdsDynamicPrefetchableServiceMethod(
|
||||
CdsServiceJson theCdsServiceJson,
|
||||
Function<CdsServiceRequestJson, CdsServiceResponseJson> theFunction,
|
||||
boolean theAllowAutoFhirClientPrefetch) {
|
||||
super(theFunction);
|
||||
myAllowAutoFhirClientPrefetch = theAllowAutoFhirClientPrefetch;
|
||||
myCdsServiceJson = theCdsServiceJson;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CdsServiceJson getCdsServiceJson() {
|
||||
return myCdsServiceJson;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAllowAutoFhirClientPrefetch() {
|
||||
return myAllowAutoFhirClientPrefetch;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.svc;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class CdsFeedbackMethod extends BaseCdsMethod {
|
||||
public CdsFeedbackMethod(Object theServiceBean, Method theMethod) {
|
||||
super(theServiceBean, theMethod);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.svc;
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
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.CdsServiceJson;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
/**
|
||||
* This bean creates a customer-defined spring context which will
|
||||
* in-turn create the various CDS Hooks services that will be
|
||||
* supported by this endpoint
|
||||
* <p>
|
||||
* Note that this class is created as a Spring bean, but it
|
||||
* does not use Autowiring! It needs to initialize before autowiring
|
||||
* is complete so that other beans can use the stuff it creates.
|
||||
*/
|
||||
public class CdsHooksContextBooter {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(CdsHooksContextBooter.class);
|
||||
private static final String CDS_SERVICES_BEAN_NAME = "cdsServices";
|
||||
private Class<?> myDefinitionsClass;
|
||||
private AnnotationConfigApplicationContext myAppCtx;
|
||||
|
||||
private List<Object> myCdsServiceBeans = new ArrayList<>();
|
||||
private final CdsServiceCache myCdsServiceCache = new CdsServiceCache();
|
||||
|
||||
public void setDefinitionsClass(Class<?> theDefinitionsClass) {
|
||||
myDefinitionsClass = theDefinitionsClass;
|
||||
}
|
||||
|
||||
public CdsServiceCache buildCdsServiceCache() {
|
||||
for (Object serviceBean : myCdsServiceBeans) {
|
||||
extractCdsServices(serviceBean);
|
||||
}
|
||||
return myCdsServiceCache;
|
||||
}
|
||||
|
||||
private void extractCdsServices(Object theServiceBean) {
|
||||
Method[] methods = theServiceBean.getClass().getMethods();
|
||||
// Sort alphabetically so service list output is deterministic (to ensure GET /cds-services is idempotent).
|
||||
// This also simplifies testing :-)
|
||||
List<Method> sortedMethods = Arrays.stream(methods)
|
||||
.sorted(Comparator.comparing(Method::getName))
|
||||
.collect(Collectors.toList());
|
||||
for (Method method : sortedMethods) {
|
||||
if (method.isAnnotationPresent(CdsService.class)) {
|
||||
CdsService annotation = method.getAnnotation(CdsService.class);
|
||||
CdsServiceJson cdsServiceJson = new CdsServiceJson();
|
||||
cdsServiceJson.setId(annotation.value());
|
||||
cdsServiceJson.setHook(annotation.hook());
|
||||
cdsServiceJson.setDescription(annotation.description());
|
||||
cdsServiceJson.setTitle(annotation.title());
|
||||
cdsServiceJson.setExtension(validateJson(annotation.extension()));
|
||||
for (CdsServicePrefetch prefetch : annotation.prefetch()) {
|
||||
cdsServiceJson.addPrefetch(prefetch.value(), prefetch.query());
|
||||
cdsServiceJson.addSource(prefetch.value(), prefetch.source());
|
||||
}
|
||||
myCdsServiceCache.registerService(
|
||||
cdsServiceJson.getId(),
|
||||
theServiceBean,
|
||||
method,
|
||||
cdsServiceJson,
|
||||
annotation.allowAutoFhirClientPrefetch());
|
||||
}
|
||||
if (method.isAnnotationPresent(CdsServiceFeedback.class)) {
|
||||
CdsServiceFeedback annotation = method.getAnnotation(CdsServiceFeedback.class);
|
||||
myCdsServiceCache.registerFeedback(annotation.value(), theServiceBean, method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String validateJson(String theExtension) {
|
||||
if (StringUtils.isEmpty(theExtension)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.readTree(theExtension);
|
||||
return theExtension;
|
||||
} catch (JsonProcessingException e) {
|
||||
final String message = String.format("Invalid JSON: %s", e.getMessage());
|
||||
ourLog.debug(message);
|
||||
throw new UnprocessableEntityException(Msg.code(2378) + message);
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
if (myDefinitionsClass == null) {
|
||||
ourLog.info("No application context defined");
|
||||
return;
|
||||
}
|
||||
ourLog.info("Starting Spring ApplicationContext for class: {}", myDefinitionsClass);
|
||||
|
||||
myAppCtx = new AnnotationConfigApplicationContext();
|
||||
myAppCtx.register(myDefinitionsClass);
|
||||
myAppCtx.refresh();
|
||||
|
||||
try {
|
||||
if (myAppCtx.containsBean(CDS_SERVICES_BEAN_NAME)) {
|
||||
myCdsServiceBeans = (List<Object>) myAppCtx.getBean(CDS_SERVICES_BEAN_NAME, List.class);
|
||||
} else {
|
||||
ourLog.info("Context has no bean named {}", CDS_SERVICES_BEAN_NAME);
|
||||
}
|
||||
|
||||
if (myCdsServiceBeans.isEmpty()) {
|
||||
throw new ConfigurationException(Msg.code(2379)
|
||||
+ "No CDS Services found in the context (need bean called " + CDS_SERVICES_BEAN_NAME + ")");
|
||||
}
|
||||
|
||||
} catch (ConfigurationException e) {
|
||||
stop();
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
stop();
|
||||
throw new ConfigurationException(Msg.code(2393) + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void stop() {
|
||||
if (myAppCtx != null) {
|
||||
ourLog.info("Shutting down CDS Hooks Application context");
|
||||
myAppCtx.close();
|
||||
myAppCtx = null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.svc;
|
||||
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsMethod;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceMethod;
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class CdsServiceCache {
|
||||
static final Logger ourLog = LoggerFactory.getLogger(CdsServiceCache.class);
|
||||
final Map<String, ICdsMethod> myServiceMap = new LinkedHashMap<>();
|
||||
final Map<String, ICdsMethod> myFeedbackMap = new LinkedHashMap<>();
|
||||
final CdsServicesJson myCdsServiceJson = new CdsServicesJson();
|
||||
|
||||
public void registerService(
|
||||
String theServiceId,
|
||||
Object theServiceBean,
|
||||
Method theMethod,
|
||||
CdsServiceJson theCdsServiceJson,
|
||||
boolean theAllowAutoFhirClientPrefetch) {
|
||||
final CdsServiceMethod cdsServiceMethod =
|
||||
new CdsServiceMethod(theCdsServiceJson, theServiceBean, theMethod, theAllowAutoFhirClientPrefetch);
|
||||
myServiceMap.put(theServiceId, cdsServiceMethod);
|
||||
myCdsServiceJson.addService(theCdsServiceJson);
|
||||
}
|
||||
|
||||
public void registerDynamicService(
|
||||
String theServiceId,
|
||||
Function<CdsServiceRequestJson, CdsServiceResponseJson> theMethod,
|
||||
CdsServiceJson theCdsServiceJson,
|
||||
boolean theAllowAutoFhirClientPrefetch,
|
||||
String theModuleId) {
|
||||
if (!isCdsServiceAlreadyRegistered(theServiceId, theModuleId)) {
|
||||
final CdsDynamicPrefetchableServiceMethod cdsDynamicPrefetchableServiceMethod =
|
||||
new CdsDynamicPrefetchableServiceMethod(
|
||||
theCdsServiceJson, theMethod, theAllowAutoFhirClientPrefetch);
|
||||
myServiceMap.put(theServiceId, cdsDynamicPrefetchableServiceMethod);
|
||||
myCdsServiceJson.addService(theCdsServiceJson);
|
||||
}
|
||||
}
|
||||
|
||||
public void registerFeedback(String theServiceId, Object theServiceBean, Method theMethod) {
|
||||
final CdsFeedbackMethod cdsFeedbackMethod = new CdsFeedbackMethod(theServiceBean, theMethod);
|
||||
myFeedbackMap.put(theServiceId, cdsFeedbackMethod);
|
||||
}
|
||||
|
||||
public ICdsMethod getServiceMethod(String theId) {
|
||||
return myServiceMap.get(theId);
|
||||
}
|
||||
|
||||
public ICdsMethod getFeedbackMethod(String theId) {
|
||||
return myFeedbackMap.get(theId);
|
||||
}
|
||||
|
||||
public CdsServicesJson getCdsServicesJson() {
|
||||
return myCdsServiceJson;
|
||||
}
|
||||
|
||||
public ICdsMethod unregisterServiceMethod(String theServiceId, String theModuleId) {
|
||||
if (myServiceMap.containsKey(theServiceId)) {
|
||||
final ICdsMethod serviceMethod = myServiceMap.get(theServiceId);
|
||||
myServiceMap.remove(theServiceId);
|
||||
if (serviceMethod instanceof ICdsServiceMethod) {
|
||||
myCdsServiceJson.removeService(((ICdsServiceMethod) serviceMethod).getCdsServiceJson());
|
||||
}
|
||||
return serviceMethod;
|
||||
} else {
|
||||
ourLog.error(
|
||||
"CDS service with serviceId: {} for moduleId: {}, is not registered. Nothing to remove!",
|
||||
theServiceId,
|
||||
theModuleId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCdsServiceAlreadyRegistered(String theServiceId, String theModuleId) {
|
||||
boolean result = myServiceMap.containsKey(theServiceId);
|
||||
if (result) {
|
||||
ourLog.error(
|
||||
"CDS service with serviceId: {} for moduleId: {}, already exists. It will not be overwritten!",
|
||||
theServiceId,
|
||||
theModuleId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.svc;
|
||||
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceMethod;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class CdsServiceMethod extends BaseCdsMethod implements ICdsServiceMethod {
|
||||
private final CdsServiceJson myCdsServiceJson;
|
||||
private final boolean myAllowAutoFhirClientPrefetch;
|
||||
|
||||
public CdsServiceMethod(
|
||||
CdsServiceJson theCdsServiceJson,
|
||||
Object theServiceBean,
|
||||
Method theMethod,
|
||||
boolean theAllowAutoFhirClientPrefetch) {
|
||||
super(theServiceBean, theMethod);
|
||||
myCdsServiceJson = theCdsServiceJson;
|
||||
myAllowAutoFhirClientPrefetch = theAllowAutoFhirClientPrefetch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CdsServiceJson getCdsServiceJson() {
|
||||
return myCdsServiceJson;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAllowAutoFhirClientPrefetch() {
|
||||
return myAllowAutoFhirClientPrefetch;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.svc;
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
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.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.svc.prefetch.CdsPrefetchSvc;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.function.Function;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
public class CdsServiceRegistryImpl implements ICdsServiceRegistry {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(CdsServiceRegistryImpl.class);
|
||||
|
||||
private CdsServiceCache myServiceCache;
|
||||
|
||||
private final CdsHooksContextBooter myCdsHooksContextBooter;
|
||||
private final CdsPrefetchSvc myCdsPrefetchSvc;
|
||||
private final ObjectMapper myObjectMapper;
|
||||
|
||||
public CdsServiceRegistryImpl(
|
||||
CdsHooksContextBooter theCdsHooksContextBooter,
|
||||
CdsPrefetchSvc theCdsPrefetchSvc,
|
||||
ObjectMapper theObjectMapper) {
|
||||
myCdsHooksContextBooter = theCdsHooksContextBooter;
|
||||
myCdsPrefetchSvc = theCdsPrefetchSvc;
|
||||
myObjectMapper = theObjectMapper;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
myServiceCache = myCdsHooksContextBooter.buildCdsServiceCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CdsServicesJson getCdsServicesJson() {
|
||||
return myServiceCache.getCdsServicesJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CdsServiceResponseJson callService(String theServiceId, CdsServiceRequestJson theCdsServiceRequestJson) {
|
||||
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) {
|
||||
ICdsMethod feedbackMethod = getCdsFeedbackMethodOrThrowException(theServiceId);
|
||||
Object response = feedbackMethod.invoke(myObjectMapper, theCdsServiceFeedbackJson, theServiceId);
|
||||
|
||||
return encodeFeedbackResponse(theServiceId, theCdsServiceFeedbackJson, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerService(
|
||||
String theServiceId,
|
||||
Function<CdsServiceRequestJson, CdsServiceResponseJson> theServiceFunction,
|
||||
CdsServiceJson theCdsServiceJson,
|
||||
boolean theAllowAutoFhirClientPrefetch,
|
||||
String theModuleId) {
|
||||
myServiceCache.registerDynamicService(
|
||||
theServiceId, theServiceFunction, theCdsServiceJson, theAllowAutoFhirClientPrefetch, theModuleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterService(String theServiceId, String theModuleId) {
|
||||
Validate.notNull(theServiceId);
|
||||
|
||||
ICdsMethod activeService = myServiceCache.unregisterServiceMethod(theServiceId, theModuleId);
|
||||
if (activeService != null) {
|
||||
ourLog.info("Unregistered active service {}", theServiceId);
|
||||
}
|
||||
}
|
||||
|
||||
private String encodeFeedbackResponse(
|
||||
String theServiceId, CdsServiceFeedbackJson theCdsServiceFeedbackJson, Object response) {
|
||||
if (response instanceof String) {
|
||||
return (String) response;
|
||||
} 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 "{}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.svc.prefetch;
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
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.model.primitive.IdDt;
|
||||
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.IVersionSpecificBundleFactory;
|
||||
import ca.uhn.fhir.rest.api.server.IBundleProvider;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class CdsPrefetchDaoSvc {
|
||||
public static final int MAX_RESOURCES_IN_BUNDLE = 1024;
|
||||
private final DaoRegistry myDaoRegistry;
|
||||
private final MatchUrlService myMatchUrlService;
|
||||
private final FhirContext myFhirContext;
|
||||
|
||||
public CdsPrefetchDaoSvc(
|
||||
DaoRegistry theDaoRegistry, MatchUrlService theMatchUrlService, FhirContext theFhirContext) {
|
||||
myDaoRegistry = theDaoRegistry;
|
||||
myMatchUrlService = theMatchUrlService;
|
||||
myFhirContext = theFhirContext;
|
||||
}
|
||||
|
||||
public IBaseResource resourceFromUrl(String theUrl) {
|
||||
UrlUtil.UrlParts parts = UrlUtil.parseUrl(theUrl);
|
||||
String resourceType = parts.getResourceType();
|
||||
if (resourceType == null) {
|
||||
throw new InvalidRequestException(
|
||||
Msg.code(2380) + "Failed to resolve " + theUrl + ". Url does not start with resource type");
|
||||
}
|
||||
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(resourceType);
|
||||
if (dao == null) {
|
||||
throw new ConfigurationException(Msg.code(2381) + "No dao registered for resource type " + resourceType);
|
||||
}
|
||||
|
||||
String resourceId = parts.getResourceId();
|
||||
if (resourceId != null) {
|
||||
// TODO KHS get a RequestDetails down here
|
||||
return dao.read(new IdDt(resourceType, resourceId));
|
||||
}
|
||||
|
||||
String matchUrl = parts.getParams();
|
||||
if (matchUrl != null) {
|
||||
return getBundleFromUrl(resourceType, dao, matchUrl);
|
||||
}
|
||||
|
||||
throw new InvalidRequestException(
|
||||
Msg.code(2382) + "Unable to translate url " + theUrl + " into a resource or a bundle");
|
||||
}
|
||||
|
||||
private IBaseResource getBundleFromUrl(String resourceType, IFhirResourceDao<?> dao, String matchUrl) {
|
||||
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(resourceType);
|
||||
SearchParameterMap searchParams = myMatchUrlService.translateMatchUrl(matchUrl, resourceDef);
|
||||
searchParams.setLoadSynchronous(true);
|
||||
// TODO KHS get a RequestDetails down here
|
||||
IBundleProvider bundleProvider = dao.search(searchParams);
|
||||
IVersionSpecificBundleFactory bundleFactory = myFhirContext.newBundleFactory();
|
||||
bundleFactory.addResourcesToBundle(
|
||||
bundleProvider.getResources(0, MAX_RESOURCES_IN_BUNDLE), BundleTypeEnum.SEARCHSET, null, null, null);
|
||||
return bundleFactory.getResourceBundle();
|
||||
}
|
||||
|
||||
public FhirContext getFhirContext() {
|
||||
return myFhirContext;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.svc.prefetch;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.rest.client.api.IClientInterceptor;
|
||||
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||
import ca.uhn.fhir.rest.client.interceptor.BearerTokenAuthInterceptor;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.util.UrlUtil;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestAuthorizationJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class CdsPrefetchFhirClientSvc {
|
||||
private final FhirContext myFhirContext;
|
||||
|
||||
public CdsPrefetchFhirClientSvc(FhirContext theFhirContext) {
|
||||
myFhirContext = theFhirContext;
|
||||
}
|
||||
|
||||
public IBaseResource resourceFromUrl(CdsServiceRequestJson theCdsServiceRequestJson, String theUrl) {
|
||||
IGenericClient client = buildClient(theCdsServiceRequestJson);
|
||||
UrlUtil.UrlParts parts = UrlUtil.parseUrl(theUrl);
|
||||
String resourceType = parts.getResourceType();
|
||||
if (StringUtils.isEmpty(resourceType)) {
|
||||
throw new InvalidRequestException(
|
||||
Msg.code(2383) + "Failed to resolve " + theUrl + ". Url does not start with a resource type.");
|
||||
}
|
||||
|
||||
String resourceId = parts.getResourceId();
|
||||
String matchUrl = parts.getParams();
|
||||
if (resourceId != null) {
|
||||
return client.read().resource(resourceType).withId(resourceId).execute();
|
||||
} else if (matchUrl != null) {
|
||||
return client.search().byUrl(theUrl).execute();
|
||||
} else {
|
||||
throw new InvalidRequestException(
|
||||
Msg.code(2384) + "Unable to translate url " + theUrl + " into a resource or a bundle.");
|
||||
}
|
||||
}
|
||||
|
||||
private IGenericClient buildClient(CdsServiceRequestJson theCdsServiceRequestJson) {
|
||||
String fhirServerBase = theCdsServiceRequestJson.getFhirServer();
|
||||
CdsServiceRequestAuthorizationJson serviceRequestAuthorization =
|
||||
theCdsServiceRequestJson.getServiceRequestAuthorizationJson();
|
||||
|
||||
IGenericClient client = myFhirContext.newRestfulGenericClient(fhirServerBase);
|
||||
if (serviceRequestAuthorization != null && serviceRequestAuthorization.getAccessToken() != null) {
|
||||
IClientInterceptor authInterceptor =
|
||||
new BearerTokenAuthInterceptor(serviceRequestAuthorization.getAccessToken());
|
||||
client.registerInterceptor(authInterceptor);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.svc.prefetch;
|
||||
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.CdsResolutionStrategyEnum;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsHooksDaoAuthorizationSvc;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceMethod;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class CdsPrefetchSvc {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(CdsPrefetchSvc.class);
|
||||
private final CdsResolutionStrategySvc myCdsResolutionStrategySvc;
|
||||
private final CdsPrefetchDaoSvc myResourcePrefetchDao;
|
||||
private final CdsPrefetchFhirClientSvc myResourcePrefetchFhirClient;
|
||||
private final ICdsHooksDaoAuthorizationSvc myCdsHooksDaoAuthorizationSvc;
|
||||
|
||||
public CdsPrefetchSvc(
|
||||
CdsResolutionStrategySvc theCdsResolutionStrategySvc,
|
||||
CdsPrefetchDaoSvc theResourcePrefetchDao,
|
||||
CdsPrefetchFhirClientSvc theResourcePrefetchFhirClient,
|
||||
ICdsHooksDaoAuthorizationSvc theCdsHooksDaoAuthorizationSvc) {
|
||||
myCdsResolutionStrategySvc = theCdsResolutionStrategySvc;
|
||||
myResourcePrefetchDao = theResourcePrefetchDao;
|
||||
myResourcePrefetchFhirClient = theResourcePrefetchFhirClient;
|
||||
myCdsHooksDaoAuthorizationSvc = theCdsHooksDaoAuthorizationSvc;
|
||||
}
|
||||
|
||||
public void augmentRequest(CdsServiceRequestJson theCdsServiceRequestJson, ICdsServiceMethod theServiceMethod) {
|
||||
CdsServiceJson serviceSpec = theServiceMethod.getCdsServiceJson();
|
||||
Set<String> missingPrefetch = findMissingPrefetch(serviceSpec, theCdsServiceRequestJson);
|
||||
if (missingPrefetch.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Set<CdsResolutionStrategyEnum> strategies =
|
||||
myCdsResolutionStrategySvc.determineResolutionStrategy(theServiceMethod, theCdsServiceRequestJson);
|
||||
String serviceId = theServiceMethod.getCdsServiceJson().getId();
|
||||
try {
|
||||
fetchMissingPrefetchElements(theCdsServiceRequestJson, serviceSpec, missingPrefetch, strategies);
|
||||
} catch (BaseServerResponseException e) {
|
||||
// Per the CDS Hooks specification
|
||||
throw new PreconditionFailedException(Msg.code(2385) + "Unable to fetch missing resource(s) with key(s) "
|
||||
+ missingPrefetch + " for CDS Hooks service " + serviceId + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchMissingPrefetchElements(
|
||||
CdsServiceRequestJson theCdsServiceRequestJson,
|
||||
CdsServiceJson theServiceSpec,
|
||||
Set<String> theMissingPrefetch,
|
||||
Set<CdsResolutionStrategyEnum> theStrategies) {
|
||||
for (String key : theMissingPrefetch) {
|
||||
String template = theServiceSpec.getPrefetch().get(key);
|
||||
CdsResolutionStrategyEnum source = theServiceSpec.getSource().get(key);
|
||||
if (!theStrategies.contains(source)) {
|
||||
throw new PreconditionFailedException(
|
||||
Msg.code(2386) + "Unable to fetch missing resource(s) with source " + source);
|
||||
}
|
||||
if (source == CdsResolutionStrategyEnum.NONE) {
|
||||
if (theStrategies.contains(CdsResolutionStrategyEnum.FHIR_CLIENT)) {
|
||||
source = CdsResolutionStrategyEnum.FHIR_CLIENT;
|
||||
} else if (theStrategies.contains(CdsResolutionStrategyEnum.SERVICE)) {
|
||||
source = CdsResolutionStrategyEnum.SERVICE;
|
||||
} else if (theStrategies.contains(CdsResolutionStrategyEnum.DAO)) {
|
||||
source = CdsResolutionStrategyEnum.DAO;
|
||||
} else {
|
||||
// Per the CDS Hooks specification
|
||||
throw new PreconditionFailedException(
|
||||
Msg.code(2387) + "Unable to fetch missing resource(s) with source " + source);
|
||||
}
|
||||
}
|
||||
|
||||
if (source == CdsResolutionStrategyEnum.SERVICE) {
|
||||
// The service will manage missing prefetch elements
|
||||
continue;
|
||||
}
|
||||
String url = PrefetchTemplateUtil.substituteTemplate(
|
||||
template, theCdsServiceRequestJson.getContext(), myResourcePrefetchDao.getFhirContext());
|
||||
ourLog.info("missing: {}. Fetching with {}", theMissingPrefetch, url);
|
||||
IBaseResource resource;
|
||||
if (source == CdsResolutionStrategyEnum.FHIR_CLIENT) {
|
||||
resource = myResourcePrefetchFhirClient.resourceFromUrl(theCdsServiceRequestJson, url);
|
||||
} else if (source == CdsResolutionStrategyEnum.DAO) {
|
||||
resource = getResourceFromDaoWithPermissionCheck(url);
|
||||
} else {
|
||||
// should never happen
|
||||
throw new IllegalStateException(Msg.code(2388) + "Unexpected strategy " + theStrategies);
|
||||
}
|
||||
|
||||
theCdsServiceRequestJson.addPrefetch(key, resource);
|
||||
}
|
||||
}
|
||||
|
||||
private IBaseResource getResourceFromDaoWithPermissionCheck(String theUrl) {
|
||||
IBaseResource resource;
|
||||
resource = myResourcePrefetchDao.resourceFromUrl(theUrl);
|
||||
myCdsHooksDaoAuthorizationSvc.authorizePreShow(resource);
|
||||
return resource;
|
||||
}
|
||||
|
||||
public Set<String> findMissingPrefetch(
|
||||
CdsServiceJson theServiceSpec, CdsServiceRequestJson theCdsServiceRequestJson) {
|
||||
Set<String> expectedPrefetchKeys = theServiceSpec.getPrefetch().keySet();
|
||||
Set<String> actualPrefetchKeys = theCdsServiceRequestJson.getPrefetchKeys();
|
||||
Set<String> retval = new HashSet<>(expectedPrefetchKeys);
|
||||
retval.removeAll(actualPrefetchKeys);
|
||||
return retval;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.svc.prefetch;
|
||||
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.CdsResolutionStrategyEnum;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceMethod;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class CdsResolutionStrategySvc {
|
||||
|
||||
private final DaoRegistry myDaoRegistry;
|
||||
|
||||
public CdsResolutionStrategySvc(DaoRegistry theDaoRegistry) {
|
||||
myDaoRegistry = theDaoRegistry;
|
||||
}
|
||||
|
||||
public Set<CdsResolutionStrategyEnum> determineResolutionStrategy(
|
||||
ICdsServiceMethod theMethod, CdsServiceRequestJson theRequest) {
|
||||
Set<CdsResolutionStrategyEnum> strategies = new HashSet<>();
|
||||
strategies.add(CdsResolutionStrategyEnum.NONE);
|
||||
if (theRequest.getFhirServer() != null) {
|
||||
strategies.add(CdsResolutionStrategyEnum.SERVICE);
|
||||
if (theMethod.isAllowAutoFhirClientPrefetch()) {
|
||||
strategies.add(CdsResolutionStrategyEnum.FHIR_CLIENT);
|
||||
}
|
||||
}
|
||||
if (myDaoRegistry != null) {
|
||||
strategies.add(CdsResolutionStrategyEnum.DAO);
|
||||
}
|
||||
return strategies;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR - CDS Hooks
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.hapi.fhir.cdshooks.svc.prefetch;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.i18n.Msg;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.util.BundleUtil;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestContextJson;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PrefetchTemplateUtil {
|
||||
private static final Pattern ourPlaceholder = Pattern.compile("\\{\\{context\\.(\\w+)}}");
|
||||
private static final Pattern daVinciPreFetch = Pattern.compile("\\{\\{context\\.(\\w+)\\.(\\w+)\\.(id)}}");
|
||||
|
||||
private static final int GROUP_WITH_KEY = 1;
|
||||
private static final int DAVINCI_RESOURCETYPE_KEY = 2;
|
||||
|
||||
private PrefetchTemplateUtil() {}
|
||||
|
||||
public static String substituteTemplate(
|
||||
String theTemplate, CdsServiceRequestContextJson theContext, FhirContext theFhirContext) {
|
||||
String parsedDaVinciPrefetchTemplate = handleDaVinciPrefetchTemplate(theTemplate, theContext, theFhirContext);
|
||||
return handleDefaultPrefetchTemplate(parsedDaVinciPrefetchTemplate, theContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* The below DaVinci Prefetch template support is implemented based on IG specifications described
|
||||
* <a href="http://hl7.org/fhir/us/davinci-crd/hooks.html#additional-prefetch-capabilities">here</a> version 1.0.0 - STU 1
|
||||
* This is subject to change as the IG can be updated by the working committee.
|
||||
*/
|
||||
private static String handleDaVinciPrefetchTemplate(
|
||||
String theTemplate, CdsServiceRequestContextJson theContext, FhirContext theFhirContext) {
|
||||
Matcher matcher = daVinciPreFetch.matcher(theTemplate);
|
||||
String returnValue = theTemplate;
|
||||
while (matcher.find()) {
|
||||
String key = matcher.group(GROUP_WITH_KEY);
|
||||
if (!theContext.containsKey(key)) {
|
||||
throw new InvalidRequestException(Msg.code(2372) + "Request context did not provide a value for key <"
|
||||
+ key + ">" + ". Available keys in context are: " + theContext.getKeys());
|
||||
}
|
||||
try {
|
||||
IBaseBundle bundle = (IBaseBundle) theContext.getResource(key);
|
||||
String resourceType = matcher.group(DAVINCI_RESOURCETYPE_KEY);
|
||||
String resourceIds = BundleUtil.toListOfResources(theFhirContext, bundle).stream()
|
||||
.filter(x -> x.fhirType().equals(resourceType))
|
||||
.map(x -> x.getIdElement().getIdPart())
|
||||
.collect(Collectors.joining(","));
|
||||
if (StringUtils.isEmpty(resourceIds)) {
|
||||
throw new InvalidRequestException(Msg.code(2373)
|
||||
+ "Request context did not provide for resource(s) matching template. ResourceType missing is: "
|
||||
+ resourceType);
|
||||
}
|
||||
String keyToReplace = key + "." + resourceType + "\\.(id)";
|
||||
returnValue = substitute(returnValue, keyToReplace, resourceIds);
|
||||
} catch (ClassCastException e) {
|
||||
throw new InvalidRequestException(Msg.code(2374) + "Request context did not provide valid "
|
||||
+ theFhirContext.getVersion().getVersion() + " Bundle resource for template key <" + key + ">");
|
||||
}
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
private static String handleDefaultPrefetchTemplate(String theTemplate, CdsServiceRequestContextJson theContext) {
|
||||
Matcher matcher = ourPlaceholder.matcher(theTemplate);
|
||||
String returnValue = theTemplate;
|
||||
while (matcher.find()) {
|
||||
String key = matcher.group(GROUP_WITH_KEY);
|
||||
// Check to see if the context map is empty, or doesn't contain the key.
|
||||
// Note we cannot return the keyset as for cases where the map is empty this will throw a
|
||||
// NullPointerException.
|
||||
if (theContext.getString(key) == null) {
|
||||
throw new InvalidRequestException(
|
||||
Msg.code(2375) + "Either request context was empty or it did not provide a value for key <"
|
||||
+ key
|
||||
+ ">. Please make sure you are including a context with valid keys.");
|
||||
}
|
||||
String value = theContext.getString(key);
|
||||
returnValue = substitute(returnValue, key, value);
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
private static String substitute(String theString, String theKey, String theValue) {
|
||||
return theString.replaceAll("\\{\\{context\\." + theKey + "}}", theValue);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class CdsServiceResponseJsonTest {
|
||||
private final CdsServiceResponseJson fixture = new CdsServiceResponseJson();
|
||||
|
||||
@Test
|
||||
void testAddCard() {
|
||||
//setup
|
||||
final CdsServiceResponseCardJson expected = new CdsServiceResponseCardJson();
|
||||
fixture.addCard(expected);
|
||||
//execute
|
||||
final List<CdsServiceResponseCardJson> actual = fixture.getCards();
|
||||
//validate
|
||||
assertNotNull(actual);
|
||||
assertEquals(1, actual.size());
|
||||
assertEquals(expected, actual.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCardsNotNull() {
|
||||
//execute
|
||||
final List<CdsServiceResponseCardJson> actual = fixture.getCards();
|
||||
//validate
|
||||
assertNotNull(actual);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.api.json;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import ca.uhn.test.util.HasGetterOrSetterForAllJsonFields;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.reflections.Reflections;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
|
||||
public class JsonBeanTest {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(JsonBeanTest.class);
|
||||
|
||||
@Test
|
||||
public void testAllCdsHooksJsonClasses() {
|
||||
Reflections reflections = new Reflections("ca.uhn.hapi.fhir.cdshooks.api.json");
|
||||
|
||||
Set<Class<? extends IModelJson>> allJsonClasses =
|
||||
reflections.getSubTypesOf(IModelJson.class);
|
||||
|
||||
assertThat(allJsonClasses, hasItem(CdsServiceJson.class));
|
||||
for (Class<? extends IModelJson> item : allJsonClasses) {
|
||||
assertThat(item, HasGetterOrSetterForAllJsonFields.hasGetterOrSetterForAllJsonFields());
|
||||
}
|
||||
|
||||
ourLog.info("Tested {} Json classes", allJsonClasses.size());
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.config;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsHooksDaoAuthorizationSvc;
|
||||
import ca.uhn.hapi.fhir.cdshooks.controller.TestServerAppCtx;
|
||||
import ca.uhn.hapi.fhir.cdshooks.svc.CdsHooksContextBooter;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class TestCdsHooksConfig {
|
||||
@Bean
|
||||
FhirContext fhirContext() {
|
||||
return FhirContext.forR4Cached();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CdsHooksContextBooter cdsHooksContextBooter() {
|
||||
CdsHooksContextBooter retVal = new CdsHooksContextBooter();
|
||||
retVal.setDefinitionsClass(TestServerAppCtx.class);
|
||||
retVal.start();
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Bean
|
||||
DaoRegistry daoRegistry() {
|
||||
return new DaoRegistry();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ICdsHooksDaoAuthorizationSvc cdsHooksDaoAuthorizationSvc() {
|
||||
return new TestCdsHooksDaoAuthorizationSvc();
|
||||
}
|
||||
|
||||
private static class TestCdsHooksDaoAuthorizationSvc implements ICdsHooksDaoAuthorizationSvc {
|
||||
@Override
|
||||
public void authorizePreShow(IBaseResource theResource) {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.controller;/*-
|
||||
* #%L
|
||||
* Smile CDR - CDR
|
||||
* %%
|
||||
* Copyright (C) 2016 - 2017 Simpatico Intelligent Systems Inc
|
||||
* %%
|
||||
* All rights reserved.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.util.JsonUtil;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsServiceRegistry;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceFeebackOutcomeEnum;
|
||||
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.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.svc.prefetch.CdsPrefetchFhirClientSvc;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
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.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.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
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;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = {CdsHooksConfig.class, TestCdsHooksConfig.class})
|
||||
public class CdsHooksControllerTest {
|
||||
public static final String TEST_FHIR_SERVER = "http://localhost:9999/";
|
||||
public static final String TEST_PATIENT_ID = "P2401";
|
||||
public static final String TEST_USER_ID = "Practitioner/FREDDY";
|
||||
|
||||
public static final String TEST_HOOK_INSTANCE = UUID.randomUUID().toString();
|
||||
public static final String OUTCOME_TIMESTAMP = "2020-12-16";
|
||||
private static final String TEST_KEY = "CdsServiceRegistryImplTest.testKey";
|
||||
private static final String TEST_SERVICE_ID = "CdsServiceRegistryImplTest.testServiceId";
|
||||
private final String MODULE_ID = "moduleId";
|
||||
|
||||
@Autowired
|
||||
ICdsServiceRegistry myCdsHooksRegistry;
|
||||
@Autowired
|
||||
ICdsServiceRegistry myCdsServiceRegistry;
|
||||
@Autowired
|
||||
@Qualifier(CdsHooksConfig.CDS_HOOKS_OBJECT_MAPPER_FACTORY)
|
||||
protected ObjectMapper myObjectMapper;
|
||||
|
||||
@MockBean
|
||||
CdsPrefetchFhirClientSvc myCdsPrefetchFhirClientSvc;
|
||||
|
||||
|
||||
MockMvc myMockMvc;
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
myMockMvc = MockMvcBuilders.standaloneSetup(new CdsHooksController(myCdsHooksRegistry)).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCdsServices() throws Exception {
|
||||
myMockMvc
|
||||
.perform(get(CdsHooksController.BASE))
|
||||
.andDo(print())
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("services[2].hook").value(GreeterCdsService.TEST_HOOK))
|
||||
.andExpect(jsonPath("services[2].description").value(GreeterCdsService.TEST_HOOK_DESCRIPTION))
|
||||
.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));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExampleFeedback() throws Exception {
|
||||
CdsServiceFeedbackJson request = new CdsServiceFeedbackJson();
|
||||
request.setCard(TEST_HOOK_INSTANCE);
|
||||
request.setOutcome(CdsServiceFeebackOutcomeEnum.accepted);
|
||||
request.setOutcomeTimestamp(OUTCOME_TIMESTAMP);
|
||||
|
||||
String requestBody = myObjectMapper.writeValueAsString(request);
|
||||
|
||||
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 + "!"))
|
||||
;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallHelloWorld() throws Exception {
|
||||
CdsServiceRequestJson request = new CdsServiceRequestJson();
|
||||
request.setHookInstance(TEST_HOOK_INSTANCE);
|
||||
request.setHook(HelloWorldService.TEST_HOOK);
|
||||
request.setFhirServer(TEST_FHIR_SERVER);
|
||||
|
||||
String requestBody = myObjectMapper.writeValueAsString(request);
|
||||
|
||||
myMockMvc
|
||||
.perform(post(CdsHooksController.BASE + "/" + HelloWorldService.TEST_HOOK_WORLD_ID).contentType(MediaType.APPLICATION_JSON).content(requestBody))
|
||||
.andDo(print())
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("cards[0].summary").value("Hello World!"))
|
||||
.andExpect(jsonPath("cards[0].indicator").value("warning"))
|
||||
.andExpect(jsonPath("cards[0].source.label").value("World Greeter"))
|
||||
.andExpect(jsonPath("cards[0].detail").value("This is a test. Do not be alarmed."))
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testCallHelloUniverse() throws Exception {
|
||||
CdsServiceRequestJson request = new CdsServiceRequestJson();
|
||||
request.setFhirServer(TEST_FHIR_SERVER);
|
||||
|
||||
String requestBody = myObjectMapper.writeValueAsString(request);
|
||||
|
||||
myMockMvc
|
||||
.perform(post(CdsHooksController.BASE + "/" + HelloWorldService.TEST_HOOK_UNIVERSE_ID).contentType(MediaType.APPLICATION_JSON).content(requestBody))
|
||||
.andDo(print())
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("cards[0].summary").value("Hello Universe!"))
|
||||
.andExpect(jsonPath("cards[0].indicator").value("critical"))
|
||||
;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallPlayback() throws Exception {
|
||||
CdsServiceRequestJson request = new CdsServiceRequestJson();
|
||||
request.setHookInstance(TEST_HOOK_INSTANCE);
|
||||
request.setHook(HelloWorldService.TEST_HOOK);
|
||||
request.setFhirServer(TEST_FHIR_SERVER);
|
||||
|
||||
String requestBody = myObjectMapper.writeValueAsString(request);
|
||||
|
||||
myMockMvc
|
||||
.perform(post(CdsHooksController.BASE + "/" + HelloWorldService.TEST_HOOK_PLAYBACK_ID).contentType(MediaType.APPLICATION_JSON).content(requestBody))
|
||||
.andDo(print())
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("cards[0].summary").value("FhirServer: " + TEST_FHIR_SERVER + " Hook: " + HelloWorldService.TEST_HOOK + " Hook Instance: " + TEST_HOOK_INSTANCE))
|
||||
.andExpect(jsonPath("cards[0].indicator").value("critical"))
|
||||
;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHelloWorldFeedback() throws Exception {
|
||||
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
|
||||
.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 + "!"))
|
||||
;
|
||||
TestServerAppCtx.ourHelloWorldService.awaitExpected();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallDynamicallyRegisteredService() throws Exception {
|
||||
// Register cds hook
|
||||
Function<CdsServiceRequestJson, CdsServiceResponseJson> serviceFunction = (CdsServiceRequestJson theCdsServiceRequestJson) -> {
|
||||
CdsServiceResponseJson retval = new CdsServiceResponseJson();
|
||||
CdsServiceResponseCardJson card = new CdsServiceResponseCardJson();
|
||||
card.setSummary(TEST_KEY);
|
||||
retval.addCard(card);
|
||||
return retval;
|
||||
};
|
||||
CdsServiceJson cdsServiceJson = new CdsServiceJson();
|
||||
cdsServiceJson.setId(TEST_SERVICE_ID);
|
||||
boolean allowAutoFhirClientPrefetch = false;
|
||||
|
||||
myCdsServiceRegistry.registerService(TEST_SERVICE_ID, serviceFunction, cdsServiceJson, allowAutoFhirClientPrefetch, MODULE_ID);
|
||||
|
||||
// Call hook
|
||||
String hookInstance = UUID.randomUUID().toString();
|
||||
CdsServiceRequestJson request = buildRequest(hookInstance);
|
||||
String requestBody = myObjectMapper.writeValueAsString(request);
|
||||
|
||||
myMockMvc
|
||||
.perform(post(CdsHooksController.BASE + "/" + TEST_SERVICE_ID).contentType(MediaType.APPLICATION_JSON).content(requestBody))
|
||||
.andDo(print())
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("cards[0].summary").value(TEST_KEY))
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testEmptyCardsResponse() throws Exception {
|
||||
//setup
|
||||
final String expected = "{ \"cards\" : [ ]}";
|
||||
final Function<CdsServiceRequestJson, CdsServiceResponseJson> serviceFunction = (CdsServiceRequestJson theCdsServiceRequestJson) -> new CdsServiceResponseJson();
|
||||
myCdsServiceRegistry.unregisterService(TEST_SERVICE_ID, MODULE_ID);
|
||||
myCdsServiceRegistry.registerService(TEST_SERVICE_ID, serviceFunction, new CdsServiceJson().setId(TEST_SERVICE_ID), false, MODULE_ID);
|
||||
final CdsServiceRequestJson request = buildRequest(UUID.randomUUID().toString());
|
||||
final String requestBody = myObjectMapper.writeValueAsString(request);
|
||||
//execute
|
||||
final MvcResult result = myMockMvc
|
||||
.perform(post(CdsHooksController.BASE + "/" + TEST_SERVICE_ID).contentType(MediaType.APPLICATION_JSON).content(requestBody))
|
||||
.andDo(print())
|
||||
.andExpect(status().is2xxSuccessful())
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andReturn();
|
||||
//validate
|
||||
final String actual = result.getResponse().getContentAsString();
|
||||
assertEquals(prettyJson(expected), prettyJson(actual));
|
||||
}
|
||||
|
||||
|
||||
@Nonnull
|
||||
protected CdsServiceRequestJson buildRequest(String hookInstance) {
|
||||
CdsServiceRequestJson request = new CdsServiceRequestJson();
|
||||
request.setHookInstance(hookInstance);
|
||||
request.setHook(GreeterCdsService.TEST_HOOK);
|
||||
request.addContext(GreeterCdsService.TEST_HOOK_CONTEXT_PATIENTID_KEY, TEST_PATIENT_ID);
|
||||
request.addContext(GreeterCdsService.TEST_HOOK_CONTEXT_USERID_KEY, TEST_USER_ID);
|
||||
request.setFhirServer(TEST_FHIR_SERVER);
|
||||
return request;
|
||||
}
|
||||
|
||||
public static String prettyJson(String theInput) {
|
||||
JsonNode input = JsonUtil.deserialize(theInput, JsonNode.class);
|
||||
return JsonUtil.serialize(input, true);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
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.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 org.hl7.fhir.r4.model.Patient;
|
||||
|
||||
public class ExampleCdsService {
|
||||
@CdsService(value = "example-service",
|
||||
hook = "patient-view",
|
||||
title = "Greet a patient",
|
||||
description = "This service says hello to a patient",
|
||||
prefetch = {
|
||||
@CdsServicePrefetch(value = "patient", query = "Patient/{{context.patientId}}")
|
||||
})
|
||||
public CdsServiceResponseJson exampleService(CdsServiceRequestJson theCdsRequest) {
|
||||
Patient patient = (Patient) theCdsRequest.getPrefetch("patient");
|
||||
CdsServiceResponseJson response = new CdsServiceResponseJson();
|
||||
CdsServiceResponseCardJson card = new CdsServiceResponseCardJson();
|
||||
card.setSummary("Hello " + patient.getNameFirstRep().getNameAsSingleString());
|
||||
card.setIndicator(CdsServiceIndicatorEnum.INFO);
|
||||
CdsServiceResponseCardSourceJson source = new CdsServiceResponseCardSourceJson();
|
||||
source.setLabel("Smile CDR");
|
||||
card.setSource(source);
|
||||
response.addCard(card);
|
||||
return response;
|
||||
}
|
||||
|
||||
@CdsServiceFeedback("example-service")
|
||||
public String exampleServiceFeedback(CdsServiceFeedbackJson theFeedback) {
|
||||
return "{\"message\": \"Thank you for your feedback dated " + theFeedback.getOutcomeTimestamp() + "!\"}";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.controller;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||
import ca.uhn.fhir.parser.IParser;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.CdsService;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.CdsServicePrefetch;
|
||||
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.test.concurrency.IPointcutLatch;
|
||||
import ca.uhn.test.concurrency.PointcutLatch;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.hl7.fhir.r4.model.HumanName;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.Practitioner;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class GreeterCdsService implements IPointcutLatch {
|
||||
public static final String TEST_HOOK_STRING_ID = "greeter-service-string";
|
||||
public static final String TEST_HOOK_OBJECT_ID = "greeter-service-object";
|
||||
public static final String TEST_HOOK = "test-hook";
|
||||
public static final String TEST_HOOK_DESCRIPTION = "Hook description";
|
||||
public static final String TEST_HOOK_TITLE = "Hook title";
|
||||
public static final CdsServiceIndicatorEnum TEST_HOOK_RESPONSE_INDICATOR = CdsServiceIndicatorEnum.INFO;
|
||||
public static final String TEST_HOOK_RESPONSE_SOURCE_LABEL = "Response source label";
|
||||
public static final String TEST_HOOK_RESPONSE_DETAIL = "Response detail";
|
||||
public static final String TEST_HOOK_CONTEXT_USERID_KEY = "userId";
|
||||
public static final String TEST_HOOK_CONTEXT_PATIENTID_KEY = "patientId";
|
||||
public static final String TEST_HOOK_PREFETCH_PATIENT_KEY = "patient";
|
||||
public static final String TEST_HOOK_PREFETCH_USER_KEY = "user";
|
||||
public static final String TEST_HOOK_PREFETCH_PATIENT_QUERY = "Patient/{{context.patientId}}";
|
||||
public static final String TEST_HOOK_PREFETCH_USER_QUERY = "{{context.userId}}";
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(GreeterCdsService.class);
|
||||
private final PointcutLatch myPointcutLatch = new PointcutLatch("Greeter CDS-Hook");
|
||||
@Autowired
|
||||
FhirContext myFhirContext;
|
||||
|
||||
@CdsService(value = TEST_HOOK_STRING_ID,
|
||||
hook = TEST_HOOK,
|
||||
title = TEST_HOOK_TITLE,
|
||||
description = TEST_HOOK_DESCRIPTION,
|
||||
prefetch = {
|
||||
@CdsServicePrefetch(value = TEST_HOOK_PREFETCH_PATIENT_KEY, query = TEST_HOOK_PREFETCH_PATIENT_QUERY),
|
||||
@CdsServicePrefetch(value = TEST_HOOK_PREFETCH_USER_KEY, query = TEST_HOOK_PREFETCH_USER_QUERY)
|
||||
}
|
||||
)
|
||||
public TestResponseJson greetWithString(String theJsonRequest) {
|
||||
myPointcutLatch.call(theJsonRequest);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
Patient patient;
|
||||
Practitioner user;
|
||||
try {
|
||||
JsonNode root = mapper.readTree(theJsonRequest);
|
||||
String patientJson = root.path("prefetch").path(TEST_HOOK_PREFETCH_PATIENT_KEY).toPrettyString();
|
||||
String practitionerJson = root.path("prefetch").path(TEST_HOOK_PREFETCH_USER_KEY).toString();
|
||||
IParser parser = myFhirContext.newJsonParser();
|
||||
patient = parser.parseResource(Patient.class, patientJson);
|
||||
user = parser.parseResource(Practitioner.class, practitionerJson);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Failed to parse request: " + theJsonRequest);
|
||||
}
|
||||
|
||||
|
||||
TestResponseJson retval = new TestResponseJson();
|
||||
TestCardJson card = new TestCardJson();
|
||||
|
||||
card.mySummary = "Hello " + getName(patient) + " and " + getName(user);
|
||||
card.myIndicator = TEST_HOOK_RESPONSE_INDICATOR.toString().toLowerCase();
|
||||
TestCardSourceJson source = new TestCardSourceJson();
|
||||
source.myLabel = TEST_HOOK_RESPONSE_SOURCE_LABEL;
|
||||
card.mySource = source;
|
||||
card.myDetail = TEST_HOOK_RESPONSE_DETAIL;
|
||||
retval.addCard(card);
|
||||
return retval;
|
||||
}
|
||||
|
||||
private String getName(Patient thePatient) {
|
||||
if (thePatient == null) {
|
||||
return null;
|
||||
}
|
||||
List<HumanName> names = thePatient.getName();
|
||||
if (names == null || names.size() == 0 || names.get(0).isEmpty()) {
|
||||
return "This patient";
|
||||
}
|
||||
HumanName nameItem = names.get(0);
|
||||
return nameItem.getNameAsSingleString();
|
||||
}
|
||||
|
||||
private String getName(Practitioner thePractitioner) {
|
||||
if (thePractitioner == null) {
|
||||
return null;
|
||||
}
|
||||
List<HumanName> names = thePractitioner.getName();
|
||||
if (names == null || names.size() == 0 || names.get(0).isEmpty()) {
|
||||
return "This patient";
|
||||
}
|
||||
HumanName nameItem = names.get(0);
|
||||
return nameItem.getNameAsSingleString();
|
||||
}
|
||||
|
||||
@CdsService(value = TEST_HOOK_OBJECT_ID,
|
||||
hook = TEST_HOOK,
|
||||
title = TEST_HOOK_TITLE,
|
||||
description = TEST_HOOK_DESCRIPTION,
|
||||
prefetch = {
|
||||
@CdsServicePrefetch(value = TEST_HOOK_PREFETCH_PATIENT_KEY, query = TEST_HOOK_PREFETCH_PATIENT_QUERY),
|
||||
@CdsServicePrefetch(value = TEST_HOOK_PREFETCH_USER_KEY, query = TEST_HOOK_PREFETCH_USER_QUERY)
|
||||
},
|
||||
allowAutoFhirClientPrefetch = true)
|
||||
|
||||
public CdsServiceResponseJson greetWithJson(CdsServiceRequestJson theCdsRequest) {
|
||||
myPointcutLatch.call(theCdsRequest);
|
||||
Patient patient = (Patient) theCdsRequest.getPrefetch("patient");
|
||||
CdsServiceResponseJson response = new CdsServiceResponseJson();
|
||||
CdsServiceResponseCardJson card = new CdsServiceResponseCardJson();
|
||||
Practitioner practitioner = (Practitioner) theCdsRequest.getPrefetch("user");
|
||||
|
||||
card.setSummary("Hello " + getName(patient) + " and " + getName(practitioner));
|
||||
card.setIndicator(CdsServiceIndicatorEnum.INFO);
|
||||
CdsServiceResponseCardSourceJson source = new CdsServiceResponseCardSourceJson();
|
||||
source.setLabel(TEST_HOOK_RESPONSE_SOURCE_LABEL);
|
||||
card.setSource(source);
|
||||
response.addCard(card);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
myPointcutLatch.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExpectedCount(int theCount) {
|
||||
myPointcutLatch.setExpectedCount(theCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HookParams> awaitExpected() throws InterruptedException {
|
||||
return myPointcutLatch.awaitExpected();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.controller;
|
||||
|
||||
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.CdsServiceFeedbackJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
|
||||
import ca.uhn.test.concurrency.IPointcutLatch;
|
||||
import ca.uhn.test.concurrency.PointcutLatch;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class HelloWorldService implements IPointcutLatch {
|
||||
public static final String TEST_HOOK = "hello-world";
|
||||
public static final String TEST_HOOK_DESCRIPTION = "hwdesc";
|
||||
public static final String TEST_HOOK_TITLE = "hwname";
|
||||
public static final String TEST_HOOK_WORLD_ID = "hwid";
|
||||
public static final String TEST_HOOK_UNIVERSE_ID = "hwid2";
|
||||
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";
|
||||
|
||||
private final PointcutLatch myPointcutLatch = new PointcutLatch("Hello World CDS-Hook");
|
||||
|
||||
@CdsService(value = TEST_HOOK_WORLD_ID,
|
||||
hook = TEST_HOOK,
|
||||
title = TEST_HOOK_TITLE,
|
||||
description = TEST_HOOK_DESCRIPTION,
|
||||
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 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" +
|
||||
"}";
|
||||
}
|
||||
|
||||
@CdsServiceFeedback(TEST_HOOK_WORLD_ID)
|
||||
public String feedback(CdsServiceFeedbackJson theFeedback) {
|
||||
myPointcutLatch.call(theFeedback);
|
||||
return "{\"message\": \"Thank you for your feedback dated " + theFeedback.getOutcomeTimestamp() + "!\"}";
|
||||
}
|
||||
|
||||
@CdsService(value = TEST_HOOK_UNIVERSE_ID,
|
||||
hook = TEST_HOOK,
|
||||
title = TEST_HOOK_TITLE,
|
||||
description = TEST_HOOK_DESCRIPTION,
|
||||
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" +
|
||||
"}";
|
||||
}
|
||||
|
||||
@CdsService(value = TEST_HOOK_PLAYBACK_ID,
|
||||
hook = TEST_HOOK,
|
||||
title = TEST_HOOK_TITLE,
|
||||
description = TEST_HOOK_DESCRIPTION,
|
||||
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() +
|
||||
" 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" +
|
||||
"}";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
myPointcutLatch.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExpectedCount(int theCount) {
|
||||
myPointcutLatch.setExpectedCount(theCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HookParams> awaitExpected() throws InterruptedException {
|
||||
return myPointcutLatch.awaitExpected();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.controller;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class TestCardJson {
|
||||
@JsonProperty("summary")
|
||||
String mySummary;
|
||||
|
||||
@JsonProperty("indicator")
|
||||
String myIndicator;
|
||||
|
||||
@JsonProperty("source")
|
||||
TestCardSourceJson mySource;
|
||||
|
||||
@JsonProperty("detail")
|
||||
String myDetail;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.controller;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class TestCardSourceJson {
|
||||
@JsonProperty("label")
|
||||
String myLabel;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.controller;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TestResponseJson {
|
||||
@JsonProperty("cards")
|
||||
List<TestCardJson> myCards;
|
||||
|
||||
public void addCard(TestCardJson theCdsServiceResponseCardJson) {
|
||||
if (myCards == null) {
|
||||
myCards = new ArrayList<>();
|
||||
}
|
||||
myCards.add(theCdsServiceResponseCardJson);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.controller;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class TestServerAppCtx {
|
||||
public static ExampleCdsService ourExampleCdsService = new ExampleCdsService();
|
||||
public static GreeterCdsService ourGreeterCdsService = new GreeterCdsService();
|
||||
public static HelloWorldService ourHelloWorldService = new HelloWorldService();
|
||||
private static final FhirContext ourFhirContext = FhirContext.forR4();
|
||||
|
||||
@Bean(name = "cdsServices")
|
||||
public List<Object> cdsServices(){
|
||||
List<Object> retVal = new ArrayList<>();
|
||||
retVal.add(exampleCdsService());
|
||||
retVal.add(greeterCdsService());
|
||||
retVal.add(ourHelloWorldService);
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ExampleCdsService exampleCdsService() {
|
||||
return ourExampleCdsService;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GreeterCdsService greeterCdsService() {
|
||||
return ourGreeterCdsService;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FhirContext fhirContext() {
|
||||
return ourFhirContext;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.model;/*-
|
||||
* #%L
|
||||
* Smile CDR - CDR
|
||||
* %%
|
||||
* Copyright (C) 2016 - 2017 Simpatico Intelligent Systems Inc
|
||||
* %%
|
||||
* All rights reserved.
|
||||
* #L%
|
||||
*/
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.module.CdsHooksObjectMapperFactory;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class CdsServiceResponseJsonTest {
|
||||
@Test
|
||||
void testCdsHooksResponseCardsShouldContainCardsWhenEmpty() throws Exception {
|
||||
// setup
|
||||
final String expected = """
|
||||
{
|
||||
"cards" : [ ]
|
||||
}""";
|
||||
final CdsServiceResponseJson cdsServiceResponseJson = new CdsServiceResponseJson();
|
||||
final ObjectMapper objectMapper = new CdsHooksObjectMapperFactory(FhirContext.forR4()).newMapper();
|
||||
// execute
|
||||
final String actual = objectMapper.writeValueAsString(cdsServiceResponseJson);
|
||||
// validate
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.module;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
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.CdsServiceResponseJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseSuggestionActionJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseSuggestionJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseSystemActionJson;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Charsets;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.MedicationRequest;
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.equalToIgnoringWhiteSpace;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
public class SerializationTest {
|
||||
public static final String HOOK_NAME = "Hook Name";
|
||||
public static final String FHIR_SERVER = "https://localhost:2401";
|
||||
public static final String FAMILY = "Jehoshaphat";
|
||||
public static final String PATIENT_KEY = "patientKey";
|
||||
public static final String CONTEXT_PATIENT_KEY = "patientId";
|
||||
public static final String CONTEXT_SELECTIONS_KEY = "selections";
|
||||
public static final String CONTEXT_DRAFT_ORDERS_KEY = "draftOrders";
|
||||
public static final String CONTEXT_PATIENT_VALUE = "Patient/123";
|
||||
public static final String CONTEXT_SELECTIONS_VALUE1 = "MedicationRequest/456";
|
||||
public static final String CONTEXT_SELECTIONS_VALUE2 = "MedicationRequest/789";
|
||||
public static final String CONTEXT_DRAFT_ORDERS_VALUE1 = "abc";
|
||||
public static final String CONTEXT_DRAFT_ORDERS_VALUE2 = "def";
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(SerializationTest.class);
|
||||
private static final String DAUGHTER_KEY = "daughterKey";
|
||||
private static final String PARENT_KEY = "parentKey";
|
||||
private static final String DAUGHTER = "Athaliah";
|
||||
private final FhirContext ourFhirContext = FhirContext.forR4();
|
||||
private final ObjectMapper ourObjectMapper = new CdsHooksObjectMapperFactory(ourFhirContext).newMapper();
|
||||
public String myResponseJson;
|
||||
@Value("classpath:CdsServiceRequestJson.json")
|
||||
Resource myRequestJsonResource;
|
||||
@Value("classpath:CdsServiceResponseJson.json")
|
||||
Resource myResponseJsonResource;
|
||||
private String myRequestJson;
|
||||
|
||||
@BeforeEach
|
||||
public void loadJson() throws IOException {
|
||||
myRequestJson = IOUtils.toString(myRequestJsonResource.getInputStream(), Charsets.UTF_8);
|
||||
myResponseJson = IOUtils.toString(myResponseJsonResource.getInputStream(), Charsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* From the Spec:
|
||||
* <p>
|
||||
* The CDS Client MUST NOT send any prefetch template key that it chooses not to satisfy. Similarly, if the CDS Client
|
||||
* encounters an error while prefetching any data, the prefetch template key MUST NOT be sent to the CDS Service. If the
|
||||
* CDS Client has no data to populate a template prefetch key, the prefetch template key MUST have a value of null. Note
|
||||
* that the null result is used rather than a bundle with zero entries to account for the possibility that the prefetch
|
||||
* url is a single-resource request.
|
||||
*/
|
||||
|
||||
// These tests verify that nulls prefetch values are preserved in serialization and deserialization so their
|
||||
// missing status is properly determined
|
||||
@Test
|
||||
public void testSerializeRequest() throws JsonProcessingException {
|
||||
CdsServiceRequestJson cdsServiceRequestJson = new CdsServiceRequestJson();
|
||||
cdsServiceRequestJson.setHook(HOOK_NAME);
|
||||
cdsServiceRequestJson.setFhirServer(FHIR_SERVER);
|
||||
|
||||
Patient patient = new Patient();
|
||||
patient.addName().setFamily(FAMILY);
|
||||
cdsServiceRequestJson.addPrefetch(PATIENT_KEY, patient);
|
||||
|
||||
Patient daughter = new Patient();
|
||||
daughter.addName().setFamily(DAUGHTER);
|
||||
cdsServiceRequestJson.addPrefetch(DAUGHTER_KEY, daughter);
|
||||
|
||||
cdsServiceRequestJson.addPrefetch(PARENT_KEY, null);
|
||||
|
||||
cdsServiceRequestJson.addContext(CONTEXT_PATIENT_KEY, CONTEXT_PATIENT_VALUE);
|
||||
cdsServiceRequestJson.addContext(CONTEXT_SELECTIONS_KEY, List.of(CONTEXT_SELECTIONS_VALUE1, CONTEXT_SELECTIONS_VALUE2));
|
||||
MedicationRequest order1 = new MedicationRequest();
|
||||
order1.setId(CONTEXT_DRAFT_ORDERS_VALUE1);
|
||||
MedicationRequest order2 = new MedicationRequest();
|
||||
order2.setId(CONTEXT_DRAFT_ORDERS_VALUE2);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.addEntry().setResource(order1);
|
||||
bundle.addEntry().setResource(order2);
|
||||
cdsServiceRequestJson.getContext().put(CONTEXT_DRAFT_ORDERS_KEY, bundle);
|
||||
|
||||
String json = ourObjectMapper.writeValueAsString(cdsServiceRequestJson);
|
||||
ourLog.debug(json);
|
||||
assertThat(json, equalToIgnoringWhiteSpace(myRequestJson));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeserializeRequest() throws JsonProcessingException {
|
||||
CdsServiceRequestJson cdsServiceRequestJson = ourObjectMapper.readValue(myRequestJson, CdsServiceRequestJson.class);
|
||||
assertEquals(HOOK_NAME, cdsServiceRequestJson.getHook());
|
||||
|
||||
assertThat(cdsServiceRequestJson.getPrefetchKeys(), contains(PATIENT_KEY, DAUGHTER_KEY, PARENT_KEY));
|
||||
Patient patient = (Patient) cdsServiceRequestJson.getPrefetch(PATIENT_KEY);
|
||||
assertEquals(FAMILY, patient.getNameFirstRep().getFamily());
|
||||
|
||||
Patient daughter = (Patient) cdsServiceRequestJson.getPrefetch(DAUGHTER_KEY);
|
||||
assertEquals(DAUGHTER, daughter.getNameFirstRep().getFamily());
|
||||
|
||||
Patient parent = (Patient) cdsServiceRequestJson.getPrefetch(PARENT_KEY);
|
||||
assertNull(parent);
|
||||
|
||||
assertEquals(CONTEXT_PATIENT_VALUE, cdsServiceRequestJson.getContext().getString(CONTEXT_PATIENT_KEY));
|
||||
List<String> selections = cdsServiceRequestJson.getContext().getArray(CONTEXT_SELECTIONS_KEY);
|
||||
assertThat(selections, contains(CONTEXT_SELECTIONS_VALUE1, CONTEXT_SELECTIONS_VALUE2));
|
||||
Bundle bundle = (Bundle) cdsServiceRequestJson.getContext().getResource(CONTEXT_DRAFT_ORDERS_KEY);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeResponse() throws JsonProcessingException {
|
||||
Patient patient = new Patient();
|
||||
patient.addName().setFamily(FAMILY);
|
||||
CdsServiceResponseSystemActionJson systemAction = new CdsServiceResponseSystemActionJson();
|
||||
systemAction.setResource(patient);
|
||||
CdsServiceResponseJson cdsServiceRequestJson = new CdsServiceResponseJson();
|
||||
cdsServiceRequestJson.addServiceAction(systemAction);
|
||||
|
||||
Patient daughter = new Patient();
|
||||
daughter.addName().setFamily(DAUGHTER);
|
||||
CdsServiceResponseSuggestionActionJson suggestionAction = new CdsServiceResponseSuggestionActionJson();
|
||||
suggestionAction.setResource(daughter);
|
||||
CdsServiceResponseSuggestionJson suggestion = new CdsServiceResponseSuggestionJson();
|
||||
suggestion.addAction(suggestionAction);
|
||||
CdsServiceResponseCardJson card = new CdsServiceResponseCardJson();
|
||||
card.addSuggestion(suggestion);
|
||||
cdsServiceRequestJson.addCard(card);
|
||||
|
||||
String json = ourObjectMapper.writeValueAsString(cdsServiceRequestJson);
|
||||
ourLog.debug(json);
|
||||
assertThat(json, equalToIgnoringWhiteSpace(myResponseJson));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeserializeResponse() throws JsonProcessingException {
|
||||
CdsServiceResponseJson cdsServiceResponseJson = ourObjectMapper.readValue(myResponseJson, CdsServiceResponseJson.class);
|
||||
Patient patient = (Patient) cdsServiceResponseJson.getServiceActions().get(0).getResource();
|
||||
assertEquals(FAMILY, patient.getNameFirstRep().getFamily());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.svc;
|
||||
|
||||
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
|
||||
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;
|
||||
|
||||
class CdsHooksContextBooterTest {
|
||||
|
||||
private CdsHooksContextBooter myFixture;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
myFixture = new CdsHooksContextBooter();
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateJsonReturnsNullWhenInputIsEmptyString() {
|
||||
// execute
|
||||
final String actual = myFixture.validateJson("");
|
||||
// validate
|
||||
assertNull(actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateJsonThrowsExceptionWhenInputIsInvalid() {
|
||||
// setup
|
||||
final String expected = "Invalid JSON: Unrecognized token 'abc': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')\n" +
|
||||
" at [Source: (String)\"abc\"; line: 1, column: 4]";
|
||||
// execute
|
||||
final UnprocessableEntityException actual = assertThrows(UnprocessableEntityException.class, () -> myFixture.validateJson("abc"));
|
||||
// validate
|
||||
assertEquals(expected, actual.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateJsonReturnsInputWhenInputIsValidJsonString() {
|
||||
// setup
|
||||
final String expected = "{\n \"com.example.timestamp\": \"2017-11-27T22:13:25Z\",\n \"myextension-practitionerspecialty\" : \"gastroenterology\"\n }";
|
||||
// execute
|
||||
final String actual = myFixture.validateJson(expected);
|
||||
// validate
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.svc;
|
||||
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsMethod;
|
||||
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.CdsServiceResponseCardJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceResponseJson;
|
||||
import ca.uhn.test.util.LogbackCaptureTestExtension;
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static ca.uhn.test.util.LogbackCaptureTestExtension.eventWithLevelAndMessageContains;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CdsServiceCacheTest {
|
||||
private static final String TEST_KEY = "testKey";
|
||||
private static final String MODULE_ID = "moduleId";
|
||||
@RegisterExtension
|
||||
final LogbackCaptureTestExtension myLogCapture = new LogbackCaptureTestExtension((Logger) CdsServiceCache.ourLog, Level.ERROR);
|
||||
@InjectMocks
|
||||
private CdsServiceCache myFixture;
|
||||
|
||||
@Test
|
||||
void registerDynamicServiceShouldRegisterServiceWhenServiceNotRegistered() {
|
||||
// setup
|
||||
final Function<CdsServiceRequestJson, CdsServiceResponseJson> serviceFunction = withFunction();
|
||||
final CdsServiceJson cdsServiceJson = withCdsServiceJson();
|
||||
// execute
|
||||
myFixture.registerDynamicService(TEST_KEY, serviceFunction, cdsServiceJson, true, MODULE_ID);
|
||||
// validate
|
||||
assertEquals(1, myFixture.myServiceMap.size());
|
||||
final CdsDynamicPrefetchableServiceMethod cdsMethod = (CdsDynamicPrefetchableServiceMethod) myFixture.myServiceMap.get(TEST_KEY);
|
||||
assertEquals(serviceFunction, cdsMethod.getFunction());
|
||||
assertEquals(cdsServiceJson, cdsMethod.getCdsServiceJson());
|
||||
assertTrue(cdsMethod.isAllowAutoFhirClientPrefetch());
|
||||
assertEquals(1, myFixture.myCdsServiceJson.getServices().size());
|
||||
assertEquals(cdsServiceJson, myFixture.myCdsServiceJson.getServices().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void registerDynamicServiceShouldNotRegisterServiceWhenServiceAlreadyRegistered() {
|
||||
// setup
|
||||
final Function<CdsServiceRequestJson, CdsServiceResponseJson> serviceFunction = withFunction();
|
||||
final CdsServiceJson cdsServiceJson = withCdsServiceJson();
|
||||
final Function<CdsServiceRequestJson, CdsServiceResponseJson> serviceFunction2 = withFunction();
|
||||
final CdsServiceJson cdsServiceJson2 = withCdsServiceJson();
|
||||
final String expectedLogMessage = "CDS service with serviceId: testKey for moduleId: moduleId, already exists. It will not be overwritten!";
|
||||
// execute
|
||||
myFixture.registerDynamicService(TEST_KEY, serviceFunction, cdsServiceJson, true, MODULE_ID);
|
||||
myFixture.registerDynamicService(TEST_KEY, serviceFunction2, cdsServiceJson2, false, MODULE_ID);
|
||||
// validate
|
||||
assertEquals(1, myFixture.myServiceMap.size());
|
||||
final CdsDynamicPrefetchableServiceMethod cdsMethod = (CdsDynamicPrefetchableServiceMethod) myFixture.myServiceMap.get(TEST_KEY);
|
||||
assertEquals(serviceFunction, cdsMethod.getFunction());
|
||||
assertEquals(cdsServiceJson, cdsMethod.getCdsServiceJson());
|
||||
assertTrue(cdsMethod.isAllowAutoFhirClientPrefetch());
|
||||
assertEquals(1, myFixture.myCdsServiceJson.getServices().size());
|
||||
assertEquals(cdsServiceJson, myFixture.myCdsServiceJson.getServices().get(0));
|
||||
assertThat(myLogCapture.getLogEvents(), contains(eventWithLevelAndMessageContains(Level.ERROR, expectedLogMessage)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unregisterServiceMethodShouldReturnsServiceWhenServiceRegistered() {
|
||||
// setup
|
||||
final Function<CdsServiceRequestJson, CdsServiceResponseJson> serviceFunction = withFunction();
|
||||
final CdsServiceJson cdsServiceJson = withCdsServiceJson();
|
||||
myFixture.registerDynamicService(TEST_KEY, serviceFunction, cdsServiceJson, true, MODULE_ID);
|
||||
// execute
|
||||
final CdsDynamicPrefetchableServiceMethod cdsMethod = (CdsDynamicPrefetchableServiceMethod) myFixture.unregisterServiceMethod(TEST_KEY, MODULE_ID);
|
||||
// validate
|
||||
assertTrue(myFixture.myServiceMap.isEmpty());
|
||||
assertEquals(serviceFunction, cdsMethod.getFunction());
|
||||
assertEquals(cdsServiceJson, cdsMethod.getCdsServiceJson());
|
||||
assertTrue(cdsMethod.isAllowAutoFhirClientPrefetch());
|
||||
assertTrue(myFixture.myCdsServiceJson.getServices().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unregisterServiceMethodShouldReturnNullWhenServiceNotRegistered() {
|
||||
// setup
|
||||
final String expectedLogMessage = "CDS service with serviceId: testKey for moduleId: moduleId, is not registered. Nothing to remove!";
|
||||
// execute
|
||||
final ICdsMethod actual = myFixture.unregisterServiceMethod(TEST_KEY, MODULE_ID);
|
||||
// validate
|
||||
assertNull(actual);
|
||||
assertThat(myLogCapture.getLogEvents(), contains(eventWithLevelAndMessageContains(Level.ERROR, expectedLogMessage)));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static CdsServiceJson withCdsServiceJson() {
|
||||
final CdsServiceJson cdsServiceJson = new CdsServiceJson();
|
||||
cdsServiceJson.setId(TEST_KEY);
|
||||
return cdsServiceJson;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static Function<CdsServiceRequestJson, CdsServiceResponseJson> withFunction() {
|
||||
return (CdsServiceRequestJson theCdsServiceRequestJson) -> {
|
||||
final CdsServiceResponseJson cdsServiceResponseJson = new CdsServiceResponseJson();
|
||||
final CdsServiceResponseCardJson card = new CdsServiceResponseCardJson();
|
||||
cdsServiceResponseJson.addCard(card);
|
||||
return cdsServiceResponseJson;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.svc.prefetch;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestAuthorizationJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class CdsPrefetchFhirClientSvcTest {
|
||||
|
||||
private IGenericClient myMockClient;
|
||||
private FhirContext myMockFhirContext;
|
||||
private CdsPrefetchFhirClientSvc myCdsPrefetchFhirClientSvc;
|
||||
|
||||
@BeforeEach
|
||||
public void beforeEach() {
|
||||
myMockClient = Mockito.mock(IGenericClient.class, Mockito.RETURNS_DEEP_STUBS);
|
||||
myMockFhirContext = Mockito.mock(FhirContext.class);
|
||||
myCdsPrefetchFhirClientSvc = new CdsPrefetchFhirClientSvc(myMockFhirContext);
|
||||
Bundle results = new Bundle();
|
||||
Mockito.when(myMockFhirContext.newRestfulGenericClient(any(String.class))).thenReturn(myMockClient);
|
||||
Mockito.when(myMockClient.search().byUrl(any(String.class)).execute()).thenReturn(results);
|
||||
Mockito.when(
|
||||
myMockClient.read()
|
||||
.resource(any(String.class))
|
||||
.withId(any(String.class))
|
||||
.execute())
|
||||
.thenReturn(results);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseResourceWithSearchParams() throws IllegalArgumentException {
|
||||
CdsServiceRequestJson cdsServiceRequestJson = new CdsServiceRequestJson();
|
||||
cdsServiceRequestJson.setFhirServer("http://localhost:8000");
|
||||
|
||||
IBaseResource srq = myCdsPrefetchFhirClientSvc.resourceFromUrl(cdsServiceRequestJson, "ServiceRequest?_id=1234");
|
||||
assertNotNull(srq);
|
||||
verify(myMockClient.search(), times(1)).byUrl("ServiceRequest?_id=1234");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseResourceWithAdditionalSearchParams() throws IllegalArgumentException {
|
||||
CdsServiceRequestJson cdsServiceRequestJson = new CdsServiceRequestJson();
|
||||
cdsServiceRequestJson.setFhirServer("http://localhost:8000");
|
||||
IBaseResource srq = myCdsPrefetchFhirClientSvc.resourceFromUrl(cdsServiceRequestJson, "ServiceRequest?_id=1234&_include=ServiceRequest:performer&_include=ServiceRequest:requester");
|
||||
assertNotNull(srq);
|
||||
verify(myMockClient.search(), times(1)).byUrl("ServiceRequest?_id=1234&_include=ServiceRequest:performer&_include=ServiceRequest:requester");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseResourceWithReferenceUrlAndAuth() throws Exception {
|
||||
CdsServiceRequestJson cdsServiceRequestJson = new CdsServiceRequestJson();
|
||||
CdsServiceRequestAuthorizationJson cdsServiceRequestAuthorizationJson = spy(new CdsServiceRequestAuthorizationJson());
|
||||
cdsServiceRequestAuthorizationJson.setAccessToken("test123");
|
||||
cdsServiceRequestJson.setFhirServer("http://localhost:8000");
|
||||
cdsServiceRequestJson.setServiceRequestAuthorizationJson(cdsServiceRequestAuthorizationJson);
|
||||
|
||||
IBaseResource srq = myCdsPrefetchFhirClientSvc.resourceFromUrl(cdsServiceRequestJson, "ServiceRequest/1234");
|
||||
assertNotNull(srq);
|
||||
verify(cdsServiceRequestAuthorizationJson, times(2)).getAccessToken();
|
||||
verify(myMockClient.read().resource("ServiceRequest"), times(1)).withId("1234");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseResourceWithReferenceUrl() {
|
||||
CdsServiceRequestJson cdsServiceRequestJson = new CdsServiceRequestJson();
|
||||
cdsServiceRequestJson.setFhirServer("http://localhost:8000");
|
||||
|
||||
IBaseResource srq = myCdsPrefetchFhirClientSvc.resourceFromUrl(cdsServiceRequestJson, "ServiceRequest/1234");
|
||||
assertNotNull(srq);
|
||||
verify(myMockClient.read().resource("ServiceRequest"), times(1)).withId("1234");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseResourceWithNoResourceType() {
|
||||
CdsServiceRequestJson cdsServiceRequestJson = new CdsServiceRequestJson();
|
||||
cdsServiceRequestJson.setFhirServer("http://localhost:8000");
|
||||
|
||||
try {
|
||||
IBaseResource srq = myCdsPrefetchFhirClientSvc.resourceFromUrl(cdsServiceRequestJson, "1234");
|
||||
fail("should throw, no resource present");
|
||||
} catch (InvalidRequestException e) {
|
||||
assertEquals("Unable to translate url 1234 into a resource or a bundle.", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseResourceWithNoResourceTypeAndSlash() {
|
||||
CdsServiceRequestJson cdsServiceRequestJson = new CdsServiceRequestJson();
|
||||
cdsServiceRequestJson.setFhirServer("http://localhost:8000");
|
||||
|
||||
try {
|
||||
IBaseResource srq = myCdsPrefetchFhirClientSvc.resourceFromUrl(cdsServiceRequestJson, "/1234");
|
||||
fail("should throw, no resource present");
|
||||
} catch (InvalidRequestException e) {
|
||||
assertEquals("Failed to resolve /1234. Url does not start with a resource type.", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.svc.prefetch;
|
||||
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.ICdsHooksDaoAuthorizationSvc;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CdsPrefetchSvcTest {
|
||||
|
||||
@Mock
|
||||
private CdsResolutionStrategySvc myCdsResolutionStrategySvc;
|
||||
@Mock
|
||||
private CdsPrefetchDaoSvc myCdsPrefetchDaoSvc;
|
||||
@Mock
|
||||
private CdsPrefetchFhirClientSvc myCdsPrefetchFhirClientSvc;
|
||||
@Mock
|
||||
private ICdsHooksDaoAuthorizationSvc myCdsHooksDaoAuthorizationSvc;
|
||||
@InjectMocks
|
||||
private CdsPrefetchSvc myCdsPrefetchSvc;
|
||||
|
||||
@Test
|
||||
void testFindMissingPrefetch() {
|
||||
Set<String> result;
|
||||
CdsServiceJson spec = new CdsServiceJson();
|
||||
CdsServiceRequestJson input = new CdsServiceRequestJson();
|
||||
|
||||
result = myCdsPrefetchSvc.findMissingPrefetch(spec, input);
|
||||
assertThat(result, hasSize(0));
|
||||
|
||||
spec.addPrefetch("foo", "fooval");
|
||||
result = myCdsPrefetchSvc.findMissingPrefetch(spec, input);
|
||||
assertThat(result, contains("foo"));
|
||||
|
||||
input.addPrefetch("foo", new Patient());
|
||||
result = myCdsPrefetchSvc.findMissingPrefetch(spec, input);
|
||||
assertThat(result, hasSize(0));
|
||||
|
||||
spec.addPrefetch("bar", "barval");
|
||||
spec.addPrefetch("baz", "bazval");
|
||||
result = myCdsPrefetchSvc.findMissingPrefetch(spec, input);
|
||||
assertThat(result, contains("bar", "baz"));
|
||||
|
||||
/**
|
||||
* From the Spec:
|
||||
*
|
||||
* The CDS Client MUST NOT send any prefetch template key that it chooses not to satisfy. Similarly, if the CDS Client
|
||||
* encounters an error while prefetching any data, the prefetch template key MUST NOT be sent to the CDS Service. If the
|
||||
* CDS Client has no data to populate a template prefetch key, the prefetch template key MUST have a value of null. Note
|
||||
* that the null result is used rather than a bundle with zero entries to account for the possibility that the prefetch
|
||||
* url is a single-resource request.
|
||||
*
|
||||
*/
|
||||
|
||||
// Per the spec above, null is not considered missing
|
||||
input.addPrefetch("baz", null);
|
||||
result = myCdsPrefetchSvc.findMissingPrefetch(spec, input);
|
||||
assertThat(result, contains("bar"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.svc.prefetch;
|
||||
|
||||
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.CdsResolutionStrategyEnum;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestJson;
|
||||
import ca.uhn.hapi.fhir.cdshooks.svc.CdsServiceMethod;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class CdsResolutionStrategySvcTest {
|
||||
private static final String TEST_FHIR_SERVER = "http://example.com/fhir";
|
||||
private final DaoRegistry myDaoRegistry = new DaoRegistry();
|
||||
|
||||
@Test
|
||||
public void testScenarios() {
|
||||
Set<CdsResolutionStrategyEnum> set = new HashSet<>();
|
||||
set.add(CdsResolutionStrategyEnum.NONE);
|
||||
assertEquals(set, strategyWith(null, false, false));
|
||||
assertEquals(set, strategyWith(null, true, false));
|
||||
set.clear();
|
||||
set.addAll(Arrays.asList(CdsResolutionStrategyEnum.NONE, CdsResolutionStrategyEnum.DAO));
|
||||
assertEquals(set, strategyWith(myDaoRegistry, false, false));
|
||||
assertEquals(set, strategyWith(myDaoRegistry, true, false));
|
||||
set.clear();
|
||||
set.addAll(Arrays.asList(CdsResolutionStrategyEnum.NONE, CdsResolutionStrategyEnum.SERVICE));
|
||||
assertEquals(set, strategyWith(null, false, true));
|
||||
set.clear();
|
||||
set.addAll(Arrays.asList(CdsResolutionStrategyEnum.NONE, CdsResolutionStrategyEnum.DAO, CdsResolutionStrategyEnum.SERVICE));
|
||||
assertEquals(set, strategyWith(myDaoRegistry, false, true));
|
||||
set.clear();
|
||||
set.addAll(Arrays.asList(CdsResolutionStrategyEnum.NONE, CdsResolutionStrategyEnum.SERVICE, CdsResolutionStrategyEnum.FHIR_CLIENT));
|
||||
assertEquals(set, strategyWith(null, true, true));
|
||||
set.clear();
|
||||
set.addAll(Arrays.asList(CdsResolutionStrategyEnum.NONE, CdsResolutionStrategyEnum.DAO, CdsResolutionStrategyEnum.SERVICE, CdsResolutionStrategyEnum.FHIR_CLIENT));
|
||||
assertEquals(set, strategyWith(myDaoRegistry, true, true));
|
||||
set.clear();
|
||||
}
|
||||
|
||||
private Set<CdsResolutionStrategyEnum> strategyWith(DaoRegistry theDaoRegistry, boolean theMethodAllowsAutoFhirClientPrefetch, boolean theRequestHasFhirServer) {
|
||||
CdsResolutionStrategySvc svc = new CdsResolutionStrategySvc(theDaoRegistry);
|
||||
CdsServiceRequestJson request = new CdsServiceRequestJson();
|
||||
if (theRequestHasFhirServer) {
|
||||
request.setFhirServer(TEST_FHIR_SERVER);
|
||||
}
|
||||
CdsServiceMethod method = new CdsServiceMethod(null, null, null, theMethodAllowsAutoFhirClientPrefetch);
|
||||
return svc.determineResolutionStrategy(method, request);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package ca.uhn.hapi.fhir.cdshooks.svc.prefetch;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.FhirVersionEnum;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.util.BundleBuilder;
|
||||
import ca.uhn.hapi.fhir.cdshooks.api.json.CdsServiceRequestContextJson;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
class PrefetchTemplateUtilTest {
|
||||
private static final String TEST_PATIENT_ID = "P2401";
|
||||
private static final String TEST_USER_ID = "userfoo";
|
||||
private static final String SERVICE_ID1 = "serviceId1";
|
||||
private static final String OBSERVATION_ID = "observationId1";
|
||||
private static final String SERVICE_ID2 = "serviceId2";
|
||||
|
||||
@Test
|
||||
public void testShouldInterpolatePrefetchTokensWithContextValues() {
|
||||
String template = "{{context.userId}} a {{context.patientId}} b {{context.patientId}}";
|
||||
CdsServiceRequestContextJson context = new CdsServiceRequestContextJson();
|
||||
context.put("patientId", TEST_PATIENT_ID);
|
||||
context.put("userId", TEST_USER_ID);
|
||||
String result = PrefetchTemplateUtil.substituteTemplate(template, context, FhirContext.forR4());
|
||||
assertEquals(TEST_USER_ID + " a " + TEST_PATIENT_ID + " b " + TEST_PATIENT_ID, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldThrowForMissingPrefetchTokens() {
|
||||
String template = "{{context.userId}} a {{context.patientId}}";
|
||||
CdsServiceRequestContextJson context = new CdsServiceRequestContextJson();
|
||||
context.put("patientId", TEST_PATIENT_ID);
|
||||
try {
|
||||
PrefetchTemplateUtil.substituteTemplate(template, context, FhirContext.forR4());
|
||||
fail();
|
||||
} catch (InvalidRequestException e) {
|
||||
assertEquals("Either request context was empty or it did not provide a value for key <userId>. Please make sure you are including a context with valid keys.", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldThrow400ForMissingContext() {
|
||||
String template = "{{context.userId}} a {{context.patientId}}";
|
||||
//Leave the context empty for the test.
|
||||
CdsServiceRequestContextJson context = new CdsServiceRequestContextJson();
|
||||
|
||||
try {
|
||||
PrefetchTemplateUtil.substituteTemplate(template, context, FhirContext.forR4());
|
||||
fail();
|
||||
} catch (InvalidRequestException e) {
|
||||
assertEquals("Either request context was empty or it did not provide a value for key <userId>. Please make sure you are including a context with valid keys.", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldThrowForMissingNestedPrefetchTokens() {
|
||||
String template = "{{context.draftOrders.ServiceRequest.id}} a {{context.patientId}}";
|
||||
CdsServiceRequestContextJson context = new CdsServiceRequestContextJson();
|
||||
context.put("patientId", TEST_PATIENT_ID);
|
||||
try {
|
||||
PrefetchTemplateUtil.substituteTemplate(template, context, FhirContext.forR4());
|
||||
fail();
|
||||
} catch (InvalidRequestException e) {
|
||||
assertEquals("Request context did not provide a value for key <draftOrders>. Available keys in context are: [patientId]", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldSupportNestedPrefetchTokensForSTU3() {
|
||||
String template = "{{context.draftOrders.Observation.id}} a {{context.patientId}}";
|
||||
BundleBuilder builder = new BundleBuilder(new FhirContext(FhirVersionEnum.DSTU3));
|
||||
org.hl7.fhir.dstu3.model.Observation observation = new org.hl7.fhir.dstu3.model.Observation();
|
||||
observation.setId(OBSERVATION_ID);
|
||||
builder.addCollectionEntry(observation);
|
||||
CdsServiceRequestContextJson context = new CdsServiceRequestContextJson();
|
||||
context.put("patientId", TEST_PATIENT_ID);
|
||||
context.put("draftOrders", builder.getBundle());
|
||||
String result = PrefetchTemplateUtil.substituteTemplate(template, context, FhirContext.forDstu3());
|
||||
assertEquals(OBSERVATION_ID + " a " + TEST_PATIENT_ID, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldReturnCSVForMultipleSupportNestedPrefetchTokensForR4() {
|
||||
String template = "{{context.draftOrders.ServiceRequest.id}} a {{context.patientId}}";
|
||||
BundleBuilder builder = new BundleBuilder(new FhirContext(FhirVersionEnum.R4));
|
||||
builder.addCollectionEntry(new org.hl7.fhir.r4.model.ServiceRequest().setId(SERVICE_ID1));
|
||||
builder.addCollectionEntry(new org.hl7.fhir.r4.model.ServiceRequest().setId(SERVICE_ID2));
|
||||
CdsServiceRequestContextJson context = new CdsServiceRequestContextJson();
|
||||
context.put("patientId", TEST_PATIENT_ID);
|
||||
context.put("draftOrders", builder.getBundle());
|
||||
String result = PrefetchTemplateUtil.substituteTemplate(template, context, FhirContext.forR4());
|
||||
assertEquals(SERVICE_ID1 + "," + SERVICE_ID2 + " a " + TEST_PATIENT_ID, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldSupportMultipleDaVincePrefetchTokensForR5() {
|
||||
String template = "{{context.draftOrders.ServiceRequest.id}} a {{context.draftOrders.Observation.id}} a {{context.patientId}}";
|
||||
BundleBuilder builder = new BundleBuilder(new FhirContext(FhirVersionEnum.R5));
|
||||
builder.addCollectionEntry(new org.hl7.fhir.r5.model.ServiceRequest().setId(SERVICE_ID1));
|
||||
builder.addCollectionEntry(new org.hl7.fhir.r5.model.ServiceRequest().setId(SERVICE_ID2));
|
||||
builder.addCollectionEntry(new org.hl7.fhir.r5.model.Observation().setId(OBSERVATION_ID));
|
||||
CdsServiceRequestContextJson context = new CdsServiceRequestContextJson();
|
||||
context.put("patientId", TEST_PATIENT_ID);
|
||||
context.put("draftOrders", builder.getBundle());
|
||||
String result = PrefetchTemplateUtil.substituteTemplate(template, context, FhirContext.forR5());
|
||||
assertEquals(SERVICE_ID1 + "," + SERVICE_ID2 + " a " + OBSERVATION_ID +" a " + TEST_PATIENT_ID, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldThrowForDaVinciTemplateIfResourcesAreNotFoundInContextForR4() {
|
||||
String template = "{{context.draftOrders.ServiceRequest.id}} a {{context.patientId}}";
|
||||
BundleBuilder builder = new BundleBuilder(new FhirContext(FhirVersionEnum.R4));
|
||||
CdsServiceRequestContextJson context = new CdsServiceRequestContextJson();
|
||||
context.put("patientId", TEST_PATIENT_ID);
|
||||
context.put("draftOrders", builder.getBundle());
|
||||
try {
|
||||
PrefetchTemplateUtil.substituteTemplate(template, context, FhirContext.forR4());
|
||||
fail("substituteTemplate call was successful with a null context field.");
|
||||
} catch (InvalidRequestException e) {
|
||||
assertEquals("Request context did not provide for resource(s) matching template. ResourceType missing is: ServiceRequest", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldThrowForDaVinciTemplateIfResourceIsNotBundle() {
|
||||
String template = "{{context.draftOrders.ServiceRequest.id}} a {{context.patientId}}";
|
||||
FhirContext fhirContextR4 = new FhirContext(FhirVersionEnum.R4);
|
||||
CdsServiceRequestContextJson context = new CdsServiceRequestContextJson();
|
||||
context.put("patientId", TEST_PATIENT_ID);
|
||||
context.put("draftOrders", new org.hl7.fhir.r4.model.Observation().setId(OBSERVATION_ID));
|
||||
try {
|
||||
PrefetchTemplateUtil.substituteTemplate(template, context, FhirContext.forR4());
|
||||
fail();
|
||||
} catch (InvalidRequestException e) {
|
||||
assertEquals("Request context did not provide valid " + fhirContextR4.getVersion().getVersion() + " Bundle resource for template key <draftOrders>" , e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"hook" : "Hook Name",
|
||||
"fhirServer" : "https://localhost:2401",
|
||||
"context" : {
|
||||
"patientId" : "Patient/123",
|
||||
"selections" : [ "MedicationRequest/456", "MedicationRequest/789" ],
|
||||
"draftOrders" : {
|
||||
"resourceType": "Bundle",
|
||||
"entry": [ {
|
||||
"resource": {
|
||||
"resourceType": "MedicationRequest",
|
||||
"id": "abc"
|
||||
}
|
||||
}, {
|
||||
"resource": {
|
||||
"resourceType": "MedicationRequest",
|
||||
"id": "def"
|
||||
}
|
||||
} ]
|
||||
}
|
||||
},
|
||||
"prefetch" : {
|
||||
"patientKey" : {
|
||||
"resourceType": "Patient",
|
||||
"name": [ {
|
||||
"family": "Jehoshaphat"
|
||||
} ]
|
||||
},
|
||||
"daughterKey" : {
|
||||
"resourceType": "Patient",
|
||||
"name": [ {
|
||||
"family": "Athaliah"
|
||||
} ]
|
||||
},
|
||||
"parentKey" : null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"cards" : [ {
|
||||
"suggestions" : [ {
|
||||
"actions" : [ {
|
||||
"resource" : {
|
||||
"resourceType": "Patient",
|
||||
"name": [ {
|
||||
"family": "Athaliah"
|
||||
} ]
|
||||
}
|
||||
} ]
|
||||
} ]
|
||||
} ],
|
||||
"systemActions" : [ {
|
||||
"resource" : {
|
||||
"resourceType": "Patient",
|
||||
"name": [ {
|
||||
"family": "Jehoshaphat"
|
||||
} ]
|
||||
}
|
||||
} ]
|
||||
}
|
|
@ -130,6 +130,11 @@
|
|||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations</artifactId>
|
||||
<version>${swagger_version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Jetty -->
|
||||
<dependency>
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
/*-
|
||||
* #%L
|
||||
* HAPI FHIR Test Utilities
|
||||
* %%
|
||||
* Copyright (C) 2014 - 2023 Smile CDR, Inc.
|
||||
* %%
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
package ca.uhn.test.util;
|
||||
|
||||
import ca.uhn.fhir.model.api.IModelJson;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.beans.BeanInfo;
|
||||
import java.beans.FeatureDescriptor;
|
||||
import java.beans.IntrospectionException;
|
||||
import java.beans.Introspector;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.hamcrest.Matchers.hasItems;
|
||||
|
||||
public class HasGetterOrSetterForAllJsonFields extends TypeSafeMatcher<Class<? extends IModelJson>> {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(HasGetterOrSetterForAllJsonFields.class);
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("All @JsonProperty annotated fields have getters and setters.");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(Class<? extends IModelJson> item) {
|
||||
List<String> jsonPropertyFields = getJsonPropertyFields(item);
|
||||
Matcher<Iterable<Object>> matcher = hasItems(jsonPropertyFields.toArray());
|
||||
List<String> properties = getProperties(item);
|
||||
ourLog.info("{}: testing {} @JsonProperty fields", item.getSimpleName(), jsonPropertyFields.size());
|
||||
return matcher.matches(properties);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private List<String> getJsonPropertyFields(Class<? extends IModelJson> item) {
|
||||
List<Field> fields = new ArrayList<>();
|
||||
|
||||
populateFields(fields, item);
|
||||
|
||||
return fields.stream()
|
||||
.filter(this::isJsonProperty)
|
||||
.filter(this::isNotCollection)
|
||||
.filter(this::isNotMap)
|
||||
.map(Field::getName)
|
||||
.map(this::stripPrefix)
|
||||
.map(this::stripUnderscoreSuffix)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private boolean isNotCollection(Field theField) {
|
||||
return !Collection.class.isAssignableFrom(theField.getType());
|
||||
}
|
||||
|
||||
private boolean isNotMap(Field theField) {
|
||||
return !Map.class.isAssignableFrom(theField.getType());
|
||||
}
|
||||
|
||||
private boolean isJsonProperty(Field theField) {
|
||||
if (!theField.isAnnotationPresent(JsonProperty.class)) {
|
||||
return false;
|
||||
}
|
||||
Schema apiModelProperty = theField.getAnnotation(Schema.class);
|
||||
if (apiModelProperty != null && apiModelProperty.accessMode() == Schema.AccessMode.READ_ONLY) {
|
||||
return false;
|
||||
}
|
||||
return apiModelProperty == null || !apiModelProperty.hidden();
|
||||
}
|
||||
|
||||
private String stripPrefix(String theFieldName) {
|
||||
if (theFieldName.startsWith("my")) {
|
||||
return theFieldName.substring(2, 3).toLowerCase() + theFieldName.substring(3);
|
||||
}
|
||||
return theFieldName;
|
||||
}
|
||||
|
||||
private String stripUnderscoreSuffix(String theFieldName) {
|
||||
if (theFieldName.endsWith("_")) {
|
||||
return theFieldName.substring(0, theFieldName.length() - 1);
|
||||
}
|
||||
return theFieldName;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void describeMismatchSafely(Class<? extends IModelJson> item, Description mismatchDescription) {
|
||||
mismatchDescription.appendText(" for class ").appendText(item.getName()).appendText(", ");
|
||||
List<String> jsonFields = getJsonPropertyFields(item);
|
||||
Matcher<Iterable<Object>> matcher = hasItems(jsonFields.toArray());
|
||||
List<String> properties = getProperties(item);
|
||||
matcher.describeMismatch(properties, mismatchDescription);
|
||||
mismatchDescription.appendText("\n All non-collection @JsonProperty fields: " + String.join(", ", jsonFields));
|
||||
mismatchDescription.appendText("\n Have get/set methods for: " + String.join(", ", properties));
|
||||
}
|
||||
|
||||
private List<String> getProperties(Class<? extends IModelJson> item) {
|
||||
try {
|
||||
BeanInfo beanInfo = Introspector.getBeanInfo(item);
|
||||
return Arrays.stream(beanInfo.getPropertyDescriptors())
|
||||
.map(FeatureDescriptor::getName)
|
||||
.filter(name -> !"class".equals(name))
|
||||
.map(this::lowerCaseFirstLetter)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
} catch (IntrospectionException e) {
|
||||
throw new AssertionError("Unable to introspect " + item.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String lowerCaseFirstLetter(String thePropertyName) {
|
||||
return thePropertyName.substring(0, 1).toLowerCase() + thePropertyName.substring(1);
|
||||
}
|
||||
|
||||
private static void populateFields(List<Field> theFields, Class<?> theItem) {
|
||||
theFields.addAll(Arrays.asList(theItem.getDeclaredFields()));
|
||||
|
||||
if (theItem.getSuperclass() != null) {
|
||||
populateFields(theFields, theItem.getSuperclass());
|
||||
}
|
||||
}
|
||||
|
||||
public static HasGetterOrSetterForAllJsonFields hasGetterOrSetterForAllJsonFields() {
|
||||
return new HasGetterOrSetterForAllJsonFields();
|
||||
}
|
||||
}
|
61
pom.xml
61
pom.xml
|
@ -124,7 +124,8 @@
|
|||
<module>tests/hapi-fhir-base-test-mindeps-server</module>
|
||||
<module>hapi-fhir-spring-boot</module>
|
||||
<module>hapi-fhir-jacoco</module>
|
||||
</modules>
|
||||
<module>hapi-fhir-server-cds-hooks</module>
|
||||
</modules>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
|
@ -835,7 +836,7 @@
|
|||
<id>XcrigX</id>
|
||||
<name>Craig McClendon</name>
|
||||
</developer>
|
||||
<developer>
|
||||
<developer>
|
||||
<id>dyoung-work</id>
|
||||
</developer>
|
||||
<developer>
|
||||
|
@ -944,7 +945,7 @@
|
|||
<hibernate_version>5.6.15.Final</hibernate_version>
|
||||
<hibernate_search_version>6.1.6.Final</hibernate_search_version>
|
||||
<logback_version>1.4.4</logback_version>
|
||||
<!-- Update lucene version when you update hibernate-search version -->
|
||||
<!-- Update lucene version when you update hibernate-search version -->
|
||||
<lucene_version>8.11.1</lucene_version>
|
||||
<hamcrest_version>2.2</hamcrest_version>
|
||||
<hibernate_validator_version>6.1.5.Final</hibernate_validator_version>
|
||||
|
@ -961,6 +962,7 @@
|
|||
<ph_schematron_version>5.6.5</ph_schematron_version>
|
||||
<ph_commons_version>9.5.4</ph_commons_version>
|
||||
<plexus_compiler_api_version>2.9.0</plexus_compiler_api_version>
|
||||
<reflections_version>0.9.11</reflections_version>
|
||||
<servicemix_saxon_version>9.8.0-15</servicemix_saxon_version>
|
||||
<servicemix_xmlresolver_version>1.2_5</servicemix_xmlresolver_version>
|
||||
<swagger_version>2.1.12</swagger_version>
|
||||
|
@ -998,7 +1000,7 @@
|
|||
|
||||
<!-- Jacoco -->
|
||||
<argLine></argLine>
|
||||
</properties>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
|
@ -1186,21 +1188,21 @@
|
|||
<artifactId>jakarta.activation</artifactId>
|
||||
<version>2.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vladsch.flexmark</groupId>
|
||||
<artifactId>flexmark</artifactId>
|
||||
<version>${flexmark_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vladsch.flexmark</groupId>
|
||||
<artifactId>flexmark-ext-tables</artifactId>
|
||||
<version>${flexmark_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vladsch.flexmark</groupId>
|
||||
<artifactId>flexmark-profile-pegdown</artifactId>
|
||||
<version>${flexmark_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vladsch.flexmark</groupId>
|
||||
<artifactId>flexmark</artifactId>
|
||||
<version>${flexmark_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vladsch.flexmark</groupId>
|
||||
<artifactId>flexmark-ext-tables</artifactId>
|
||||
<version>${flexmark_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.vladsch.flexmark</groupId>
|
||||
<artifactId>flexmark-profile-pegdown</artifactId>
|
||||
<version>${flexmark_version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-beanutils</groupId>
|
||||
<artifactId>commons-beanutils</artifactId>
|
||||
|
@ -2628,9 +2630,9 @@
|
|||
<copy todir="target/site/apidocs-server-mdm">
|
||||
<fileset dir="hapi-fhir-server-mdm/target/site/apidocs"/>
|
||||
</copy>
|
||||
<copy todir="target/site/apidocs-server-openapi">
|
||||
<fileset dir="hapi-fhir-server-mdm/target/site/openapi"/>
|
||||
</copy>
|
||||
<copy todir="target/site/apidocs-server-openapi">
|
||||
<fileset dir="hapi-fhir-server-mdm/target/site/openapi"/>
|
||||
</copy>
|
||||
<copy todir="target/site/xref-jpaserver">
|
||||
<fileset dir="hapi-fhir-jpaserver-base/target/site/xref"/>
|
||||
</copy>
|
||||
|
@ -2982,7 +2984,10 @@
|
|||
<profile>
|
||||
<id>CI</id>
|
||||
<properties>
|
||||
<surefire_jvm_args>-Dspring.test.context.cache.maxSize=2 -Dfile.encoding=UTF-8 -Xmx2648m -XX:-TieredCompilation -Dfile.encoding=UTF-8 -Xss128M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=2048M -XX:ReservedCodeCacheSize=220M</surefire_jvm_args>
|
||||
<surefire_jvm_args>-Dspring.test.context.cache.maxSize=2 -Dfile.encoding=UTF-8 -Xmx2648m
|
||||
-XX:-TieredCompilation -Dfile.encoding=UTF-8 -Xss128M -XX:MetaspaceSize=512M -XX:MaxMetaspaceSize=2048M
|
||||
-XX:ReservedCodeCacheSize=220M
|
||||
</surefire_jvm_args>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
|
@ -3088,14 +3093,20 @@
|
|||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<executions>
|
||||
<execution><id>integration-test</id><phase>none</phase></execution>
|
||||
<execution>
|
||||
<id>integration-test</id>
|
||||
<phase>none</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<executions>
|
||||
<execution><id>validate</id><phase>none</phase></execution>
|
||||
<execution>
|
||||
<id>validate</id>
|
||||
<phase>none</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
|
Loading…
Reference in New Issue