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:
David Conlan
2024-03-04 02:39:03 +10:00
committed by GitHub
parent 3ea85a05aa
commit 4226648867
7 changed files with 193 additions and 0 deletions

View File

@@ -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);
}
}

View 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();
}
}

View 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();
}
}