Allow custom operations/providers in addition to interceptors (#654)
* Allow custom operations/providers to be added in the same way custom interceptors are currently loaded * Add new property to documentation and default yaml file * Add test for custom operation
This commit is contained in:
10
README.md
10
README.md
@@ -345,6 +345,16 @@ or
|
|||||||
|
|
||||||
2) classes will be instantiated via reflection if no matching Bean is found
|
2) classes will be instantiated via reflection if no matching Bean is found
|
||||||
|
|
||||||
|
## Adding custom operations(providers)
|
||||||
|
Custom operations(providers) can be registered with the server by including the property `hapi.fhir.custom-provider-classes`. This will take a comma separated list of fully-qualified class names which will be registered with the server.
|
||||||
|
Providers will be discovered in one of two ways:
|
||||||
|
|
||||||
|
1) discovered from the Spring application context as existing Beans (can be used in conjunction with `hapi.fhir.custom-bean-packages`) or registered with Spring via other methods
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
2) classes will be instantiated via reflection if no matching Bean is found
|
||||||
|
|
||||||
## Customizing The Web Testpage UI
|
## Customizing The Web Testpage UI
|
||||||
|
|
||||||
The UI that comes with this server is an exact clone of the server available at [http://hapi.fhir.org](http://hapi.fhir.org). You may skin this UI if you'd like. For example, you might change the introductory text or replace the logo with your own.
|
The UI that comes with this server is an exact clone of the server available at [http://hapi.fhir.org](http://hapi.fhir.org). You may skin this UI if you'd like. For example, you might change the introductory text or replace the logo with your own.
|
||||||
|
|||||||
@@ -97,12 +97,16 @@ public class AppProperties {
|
|||||||
|
|
||||||
private final List<String> custom_interceptor_classes = new ArrayList<>();
|
private final List<String> custom_interceptor_classes = new ArrayList<>();
|
||||||
|
|
||||||
|
private final List<String> custom_provider_classes = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
public List<String> getCustomInterceptorClasses() {
|
public List<String> getCustomInterceptorClasses() {
|
||||||
return custom_interceptor_classes;
|
return custom_interceptor_classes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getCustomProviderClasses() {
|
||||||
|
return custom_provider_classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public Boolean getOpenapi_enabled() {
|
public Boolean getOpenapi_enabled() {
|
||||||
|
|||||||
@@ -455,6 +455,9 @@ public class StarterJpaConfig {
|
|||||||
fhirServer.registerProvider(theIpsOperationProvider.get());
|
fhirServer.registerProvider(theIpsOperationProvider.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// register custom providers
|
||||||
|
registerCustomProviders(fhirServer, appContext, appProperties.getCustomProviderClasses());
|
||||||
|
|
||||||
return fhirServer;
|
return fhirServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,6 +500,45 @@ public class StarterJpaConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check the properties for custom provider classes and registers them.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
|
private void registerCustomProviders(
|
||||||
|
RestfulServer fhirServer, ApplicationContext theAppContext, List<String> customProviderClasses) {
|
||||||
|
|
||||||
|
if (customProviderClasses == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String className : customProviderClasses) {
|
||||||
|
Class clazz;
|
||||||
|
try {
|
||||||
|
clazz = Class.forName(className);
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
throw new ConfigurationException("Provider class was not found on classpath: " + className, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// first check if the class a Bean in the app context
|
||||||
|
Object provider = null;
|
||||||
|
try {
|
||||||
|
provider = theAppContext.getBean(clazz);
|
||||||
|
} catch (NoSuchBeanDefinitionException ex) {
|
||||||
|
// no op - if it's not a bean we'll try to create it
|
||||||
|
}
|
||||||
|
|
||||||
|
// if not a bean, instantiate the interceptor via reflection
|
||||||
|
if (provider == null) {
|
||||||
|
try {
|
||||||
|
provider = clazz.getConstructor().newInstance();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new ConfigurationException("Unable to instantiate provider class : " + className, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fhirServer.registerProvider(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static IServerConformanceProvider<?> calculateConformanceProvider(
|
public static IServerConformanceProvider<?> calculateConformanceProvider(
|
||||||
IFhirSystemDao fhirSystemDao,
|
IFhirSystemDao fhirSystemDao,
|
||||||
RestfulServer fhirServer,
|
RestfulServer fhirServer,
|
||||||
|
|||||||
@@ -178,6 +178,11 @@ hapi:
|
|||||||
# or will be instantiated via reflection using an no-arg contructor; then registered with the server
|
# or will be instantiated via reflection using an no-arg contructor; then registered with the server
|
||||||
#custom-interceptor-classes:
|
#custom-interceptor-classes:
|
||||||
|
|
||||||
|
# comma-separated list of fully qualified provider classes.
|
||||||
|
# classes listed here will be fetched from the Spring context when combined with 'custom-bean-packages',
|
||||||
|
# or will be instantiated via reflection using an no-arg contructor; then registered with the server
|
||||||
|
#custom-provider-classes:
|
||||||
|
|
||||||
# Threadpool size for BATCH'ed GETs in a bundle.
|
# Threadpool size for BATCH'ed GETs in a bundle.
|
||||||
# bundle_batch_pool_size: 10
|
# bundle_batch_pool_size: 10
|
||||||
# bundle_batch_pool_max_size: 50
|
# bundle_batch_pool_max_size: 50
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package ca.uhn.fhir.jpa.starter;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import ca.uhn.fhir.rest.api.MethodOutcome;
|
||||||
|
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||||
|
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
|
||||||
|
import org.hl7.fhir.r4.model.Binary;
|
||||||
|
import org.hl7.fhir.r4.model.Parameters;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||||
|
|
||||||
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {Application.class}, properties = {
|
||||||
|
"hapi.fhir.custom-bean-packages=some.custom.pkg1",
|
||||||
|
"hapi.fhir.custom-provider-classes=some.custom.pkg1.CustomOperationBean,some.custom.pkg1.CustomOperationPojo",
|
||||||
|
"spring.datasource.url=jdbc:h2:mem:dbr4",
|
||||||
|
"hapi.fhir.cr_enabled=false",
|
||||||
|
// "hapi.fhir.enable_repository_validating_interceptor=true",
|
||||||
|
"hapi.fhir.fhir_version=r4"
|
||||||
|
})
|
||||||
|
|
||||||
|
class CustomOperationTest {
|
||||||
|
|
||||||
|
@LocalServerPort
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
private IGenericClient client;
|
||||||
|
private FhirContext ctx;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
ctx = FhirContext.forR4();
|
||||||
|
ctx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
|
||||||
|
ctx.getRestfulClientFactory().setSocketTimeout(1200 * 1000);
|
||||||
|
String ourServerBase = "http://localhost:" + port + "/fhir/";
|
||||||
|
client = ctx.newRestfulGenericClient(ourServerBase);
|
||||||
|
|
||||||
|
// Properties props = new Properties();
|
||||||
|
// props.put("spring.autoconfigure.exclude", "org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCustomOperations() {
|
||||||
|
|
||||||
|
// we registered two custom operations via the property 'hapi.fhir.custom-provider-classes'
|
||||||
|
// one is discovered as a Spring Bean ($springBeanOperation), one instantiated via reflection ($pojoOperation)
|
||||||
|
// both should be registered with the server and will add a custom operation.
|
||||||
|
|
||||||
|
// test Spring bean operation
|
||||||
|
MethodOutcome springBeanOutcome = client.operation().onServer().named("$springBeanOperation")
|
||||||
|
.withNoParameters(Parameters.class).returnMethodOutcome().execute();
|
||||||
|
|
||||||
|
// the hapi client will return our operation result (just a string) as a Binary with the string stored as the
|
||||||
|
// data
|
||||||
|
Assertions.assertEquals(200, springBeanOutcome.getResponseStatusCode());
|
||||||
|
Binary springReturnResource = (Binary) springBeanOutcome.getResource();
|
||||||
|
String springReturn = new String(springReturnResource.getData());
|
||||||
|
Assertions.assertEquals("springBean", springReturn);
|
||||||
|
|
||||||
|
// test Pojo bean
|
||||||
|
MethodOutcome pojoOutcome = client.operation().onServer().named("$pojoOperation")
|
||||||
|
.withNoParameters(Parameters.class).returnMethodOutcome().execute();
|
||||||
|
|
||||||
|
Assertions.assertEquals(200, pojoOutcome.getResponseStatusCode());
|
||||||
|
Binary pojoReturnResource = (Binary) pojoOutcome.getResource();
|
||||||
|
String pojoReturn = new String(pojoReturnResource.getData());
|
||||||
|
Assertions.assertEquals("pojo", pojoReturn);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/test/java/some/custom/pkg1/CustomOperationBean.java
Normal file
33
src/test/java/some/custom/pkg1/CustomOperationBean.java
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package some.custom.pkg1;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.rest.annotation.Operation;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code taken from hapi documentation on how to implement an operation which handles its own request/response
|
||||||
|
* <a href="https://hapifhir.io/hapi-fhir/docs/server_plain/rest_operations_operations.html#manually-handing-requestresponse">...</a>
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class CustomOperationBean {
|
||||||
|
|
||||||
|
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CustomOperationBean.class);
|
||||||
|
|
||||||
|
@Operation(name = "$springBeanOperation", manualResponse = true, manualRequest = true)
|
||||||
|
public void springBeanOperation(HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
|
||||||
|
throws IOException {
|
||||||
|
String contentType = theServletRequest.getContentType();
|
||||||
|
byte[] bytes = IOUtils.toByteArray(theServletRequest.getInputStream());
|
||||||
|
|
||||||
|
ourLog.info("Received call with content type {} and {} bytes", contentType, bytes.length);
|
||||||
|
|
||||||
|
theServletResponse.setContentType("text/plain");
|
||||||
|
theServletResponse.getWriter().write("springBean");
|
||||||
|
theServletResponse.getWriter().close();
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/test/java/some/custom/pkg1/CustomOperationPojo.java
Normal file
28
src/test/java/some/custom/pkg1/CustomOperationPojo.java
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package some.custom.pkg1;
|
||||||
|
|
||||||
|
import ca.uhn.fhir.rest.annotation.Operation;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class CustomOperationPojo {
|
||||||
|
|
||||||
|
private final Logger LOGGER = LoggerFactory.getLogger(CustomOperationPojo.class);
|
||||||
|
|
||||||
|
@Operation(name = "$pojoOperation", manualResponse = true, manualRequest = true)
|
||||||
|
public void $pojoOperation(HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
|
||||||
|
throws IOException {
|
||||||
|
String contentType = theServletRequest.getContentType();
|
||||||
|
byte[] bytes = IOUtils.toByteArray(theServletRequest.getInputStream());
|
||||||
|
|
||||||
|
LOGGER.info("Received call with content type {} and {} bytes", contentType, bytes.length);
|
||||||
|
|
||||||
|
theServletResponse.setContentType("text/plain");
|
||||||
|
theServletResponse.getWriter().write("pojo");
|
||||||
|
theServletResponse.getWriter().close();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user