Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow custom operations/providers in addition to interceptors #654

Merged
merged 3 commits into from
Mar 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,16 @@ or

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

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.
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,16 @@ public class AppProperties {

private final List<String> custom_interceptor_classes = new ArrayList<>();

private final List<String> custom_provider_classes = new ArrayList<>();


public List<String> getCustomInterceptorClasses() {
return custom_interceptor_classes;
}

public List<String> getCustomProviderClasses() {
return custom_provider_classes;
}


public Boolean getOpenapi_enabled() {
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,9 @@ public RestfulServer restfulServer(
fhirServer.registerProvider(theIpsOperationProvider.get());
}

// register custom providers
registerCustomProviders(fhirServer, appContext, appProperties.getCustomProviderClasses());

return fhirServer;
}

Expand Down Expand Up @@ -497,6 +500,45 @@ private void registerCustomInterceptors(
}
}

/**
* 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(
IFhirSystemDao fhirSystemDao,
RestfulServer fhirServer,
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ hapi:
# or will be instantiated via reflection using an no-arg contructor; then registered with the server
#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.
# bundle_batch_pool_size: 10
# bundle_batch_pool_max_size: 50
Expand Down
71 changes: 71 additions & 0 deletions src/test/java/ca/uhn/fhir/jpa/starter/CustomOperationTest.java
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions src/test/java/some/custom/pkg1/CustomOperationBean.java
Original file line number Diff line number Diff line change
@@ -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 changes: 28 additions & 0 deletions src/test/java/some/custom/pkg1/CustomOperationPojo.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading