Skip to content

Commit

Permalink
Merge branch 'hapifhir:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
amrit110 authored Mar 11, 2024
2 parents acd1e6f + 7c76755 commit 9c80f79
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 6 deletions.
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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.0.0</version>
<version>7.0.2</version>
</parent>

<artifactId>hapi-fhir-jpaserver-starter</artifactId>
Expand Down
16 changes: 15 additions & 1 deletion src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings.ClientIdStrategyEnum;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings.IdStrategyEnum;
import ca.uhn.fhir.jpa.model.entity.NormalizedQuantitySearchLevel;
import ca.uhn.fhir.jpa.packages.PackageInstallationSpec;
import ca.uhn.fhir.rest.api.EncodingEnum;
Expand Down Expand Up @@ -65,6 +66,7 @@ public class AppProperties {
private EncodingEnum default_encoding = EncodingEnum.JSON;
private FhirVersionEnum fhir_version = FhirVersionEnum.R4;
private ClientIdStrategyEnum client_id_strategy = ClientIdStrategyEnum.ALPHANUMERIC;
private IdStrategyEnum server_id_strategy = null;
private List<String> supported_resource_types = new ArrayList<>();
private List<Bundle.BundleType> allowed_bundle_types = null;
private Boolean narrative_enabled = true;
Expand Down Expand Up @@ -97,12 +99,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 Expand Up @@ -259,7 +265,15 @@ public void setClient_id_strategy(
this.client_id_strategy = client_id_strategy;
}

public boolean getAdvanced_lucene_indexing() {
public IdStrategyEnum getServer_id_strategy() {
return server_id_strategy;
}

public void setServer_id_strategy(IdStrategyEnum server_id_strategy) {
this.server_id_strategy = server_id_strategy;
}

public boolean getAdvanced_lucene_indexing() {
return this.advanced_lucene_indexing;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,27 @@ public JpaStorageSettings jpaStorageSettings(AppProperties appProperties) {
jpaStorageSettings.setDeferIndexingForCodesystemsOfSize(
appProperties.getDefer_indexing_for_codesystems_of_size());

jpaStorageSettings.setResourceClientIdStrategy(appProperties.getClient_id_strategy());
ourLog.info("Server configured to use '" + appProperties.getClient_id_strategy() + "' Client ID Strategy");

// Set and/or recommend default Server ID Strategy of UUID when using the ANY Client ID Strategy
if (appProperties.getClient_id_strategy() == JpaStorageSettings.ClientIdStrategyEnum.ANY) {
jpaStorageSettings.setResourceServerIdStrategy(JpaStorageSettings.IdStrategyEnum.UUID);
jpaStorageSettings.setResourceClientIdStrategy(appProperties.getClient_id_strategy());
if (appProperties.getServer_id_strategy() == null) {
ourLog.info("Defaulting server to use '" + JpaStorageSettings.IdStrategyEnum.UUID
+ "' Server ID Strategy when using the '" + JpaStorageSettings.ClientIdStrategyEnum.ANY
+ "' Client ID Strategy");
appProperties.setServer_id_strategy(JpaStorageSettings.IdStrategyEnum.UUID);
} else if (appProperties.getServer_id_strategy() != JpaStorageSettings.IdStrategyEnum.UUID) {
ourLog.warn("WARNING: '" + JpaStorageSettings.IdStrategyEnum.UUID
+ "' Server ID Strategy is highly recommended when using the '"
+ JpaStorageSettings.ClientIdStrategyEnum.ANY + "' Client ID Strategy");
}
}
if (appProperties.getServer_id_strategy() != null) {
jpaStorageSettings.setResourceServerIdStrategy(appProperties.getServer_id_strategy());
ourLog.info("Server configured to use '" + appProperties.getServer_id_strategy() + "' Server ID Strategy");
}

// Parallel Batch GET execution settings
jpaStorageSettings.setBundleBatchPoolSize(appProperties.getBundle_batch_pool_size());
jpaStorageSettings.setBundleBatchPoolSize(appProperties.getBundle_batch_pool_max_size());
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
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ public Parameters install(@OperationParam(name = "npmContent", min = 1, max = 1)


@Operation(name = "$uninstall", typeName = "ImplementationGuide")
public org.hl7.fhir.r4.model.Parameters uninstall(@OperationParam(name = "name", min = 1, max = 1) String name, @OperationParam(name = "version", min = 1, max = 1) String version) {
public Parameters uninstall(@OperationParam(name = "name", min = 1, max = 1) String name, @OperationParam(name = "version", min = 1, max = 1) String version) {

packageInstallerSvc.uninstall(new PackageInstallationSpec().setName(name).setVersion(version));
return new org.hl7.fhir.r4.model.Parameters();
return new Parameters();
}
}
6 changes: 6 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ hapi:
# etag_support_enabled: true
# expunge_enabled: true
# client_id_strategy: ALPHANUMERIC
# server_id_strategy: SEQUENTIAL_NUMERIC
# fhirpath_interceptor_enabled: false
# filter_search_enabled: true
# graphql_enabled: true
Expand Down Expand Up @@ -178,6 +179,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();
}
}

0 comments on commit 9c80f79

Please sign in to comment.