allow interceptors to be registered via properties

This commit is contained in:
Craig McClendon
2022-12-20 20:07:18 -06:00
parent 19c68e7cfc
commit ba58a71624
9 changed files with 234 additions and 7 deletions

View File

@@ -251,6 +251,16 @@ Because the integration tests within the project rely on the default H2 database
NOTE: MS SQL Server by default uses a case-insensitive codepage. This will cause errors with some operations - such as when expanding case-sensitive valuesets (UCUM) as there are unique indexes defined on the terminology tables for codes. NOTE: MS SQL Server by default uses a case-insensitive codepage. This will cause errors with some operations - such as when expanding case-sensitive valuesets (UCUM) as there are unique indexes defined on the terminology tables for codes.
It is recommended to deploy a case-sensitive database prior to running HAPI FHIR when using MS SQL Server to avoid these and potentially other issues. It is recommended to deploy a case-sensitive database prior to running HAPI FHIR when using MS SQL Server to avoid these and potentially other issues.
## Adding custom interceptors
Custom interceptors can be registered with the server by including the property `hapi.fhir.custom-interceptor-classes`. This will take a comma separated list of fully-qualified class names which will be registered with the server.
Interceptors 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.

View File

@@ -82,6 +82,12 @@ public class AppProperties {
private Integer bundle_batch_pool_size = 20; private Integer bundle_batch_pool_size = 20;
private Integer bundle_batch_pool_max_size = 100; private Integer bundle_batch_pool_max_size = 100;
private final List<String> local_base_urls = new ArrayList<>(); private final List<String> local_base_urls = new ArrayList<>();
private final List<String> custom_interceptor_classes = new ArrayList<>();
public List<String> getCustomInterceptorClasses() {
return custom_interceptor_classes;
}
public Boolean getOpenapi_enabled() { public Boolean getOpenapi_enabled() {
return openapi_enabled; return openapi_enabled;

View File

@@ -63,9 +63,11 @@ import ca.uhn.fhir.validation.ResultSeverityEnum;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.*; import org.springframework.context.annotation.*;
import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -75,13 +77,16 @@ import org.springframework.web.cors.CorsConfiguration;
import javax.persistence.EntityManagerFactory; import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource; import javax.sql.DataSource;
import java.util.*; import java.util.*;
import static ca.uhn.fhir.jpa.starter.common.validation.IRepositoryValidationInterceptorFactory.ENABLE_REPOSITORY_VALIDATING_INTERCEPTOR; import static ca.uhn.fhir.jpa.starter.common.validation.IRepositoryValidationInterceptorFactory.ENABLE_REPOSITORY_VALIDATING_INTERCEPTOR;
@Configuration @Configuration
//allow users to configure custom packages to scan for additional beans
@ComponentScan(basePackages = { "${hapi.fhir.custom-bean-packages:}" })
@Import( @Import(
ThreadPoolFactoryConfig.class ThreadPoolFactoryConfig.class
) )
public class StarterJpaConfig { public class StarterJpaConfig {
@@ -243,11 +248,9 @@ public class StarterJpaConfig {
} }
@Bean @Bean
public RestfulServer restfulServer(IFhirSystemDao<?, ?> fhirSystemDao, AppProperties appProperties, DaoRegistry daoRegistry, Optional<MdmProviderLoader> mdmProviderProvider, IJpaSystemProvider jpaSystemProvider, ResourceProviderFactory resourceProviderFactory, DaoConfig daoConfig, ISearchParamRegistry searchParamRegistry, IValidationSupport theValidationSupport, DatabaseBackedPagingProvider databaseBackedPagingProvider, LoggingInterceptor loggingInterceptor, Optional<TerminologyUploaderProvider> terminologyUploaderProvider, Optional<SubscriptionTriggeringProvider> subscriptionTriggeringProvider, Optional<CorsInterceptor> corsInterceptor, IInterceptorBroadcaster interceptorBroadcaster, Optional<BinaryAccessProvider> binaryAccessProvider, BinaryStorageInterceptor binaryStorageInterceptor, IValidatorModule validatorModule, Optional<GraphQLProvider> graphQLProvider, BulkDataExportProvider bulkDataExportProvider, BulkDataImportProvider bulkDataImportProvider, ValueSetOperationProvider theValueSetOperationProvider, ReindexProvider reindexProvider, PartitionManagementProvider partitionManagementProvider, Optional<RepositoryValidatingInterceptor> repositoryValidatingInterceptor, IPackageInstallerSvc packageInstallerSvc, ThreadSafeResourceDeleterSvc theThreadSafeResourceDeleterSvc) { public RestfulServer restfulServer(IFhirSystemDao<?, ?> fhirSystemDao, AppProperties appProperties, DaoRegistry daoRegistry, Optional<MdmProviderLoader> mdmProviderProvider, IJpaSystemProvider jpaSystemProvider, ResourceProviderFactory resourceProviderFactory, DaoConfig daoConfig, ISearchParamRegistry searchParamRegistry, IValidationSupport theValidationSupport, DatabaseBackedPagingProvider databaseBackedPagingProvider, LoggingInterceptor loggingInterceptor, Optional<TerminologyUploaderProvider> terminologyUploaderProvider, Optional<SubscriptionTriggeringProvider> subscriptionTriggeringProvider, Optional<CorsInterceptor> corsInterceptor, IInterceptorBroadcaster interceptorBroadcaster, Optional<BinaryAccessProvider> binaryAccessProvider, BinaryStorageInterceptor binaryStorageInterceptor, IValidatorModule validatorModule, Optional<GraphQLProvider> graphQLProvider, BulkDataExportProvider bulkDataExportProvider, BulkDataImportProvider bulkDataImportProvider, ValueSetOperationProvider theValueSetOperationProvider, ReindexProvider reindexProvider, PartitionManagementProvider partitionManagementProvider, Optional<RepositoryValidatingInterceptor> repositoryValidatingInterceptor, IPackageInstallerSvc packageInstallerSvc, ThreadSafeResourceDeleterSvc theThreadSafeResourceDeleterSvc, ApplicationContext appContext) {
RestfulServer fhirServer = new RestfulServer(fhirSystemDao.getContext()); RestfulServer fhirServer = new RestfulServer(fhirSystemDao.getContext());
List<String> supportedResourceTypes = appProperties.getSupported_resource_types(); List<String> supportedResourceTypes = appProperties.getSupported_resource_types();
if (!supportedResourceTypes.isEmpty()) { if (!supportedResourceTypes.isEmpty()) {
@@ -410,15 +413,52 @@ public class StarterJpaConfig {
fhirServer.setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy()); fhirServer.setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy());
fhirServer.registerProviders(partitionManagementProvider); fhirServer.registerProviders(partitionManagementProvider);
} }
repositoryValidatingInterceptor.ifPresent(fhirServer::registerInterceptor); repositoryValidatingInterceptor.ifPresent(fhirServer::registerInterceptor);
// register custom interceptors
registerCustomInterceptors(fhirServer, appContext, appProperties.getCustomInterceptorClasses());
return fhirServer; return fhirServer;
} }
/**
* check the properties for custom interceptor classes and registers them.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private void registerCustomInterceptors(RestfulServer fhirServer, ApplicationContext theAppContext, List<String> customInterceptorClasses) {
if (customInterceptorClasses == null) {
return;
}
for (String className : customInterceptorClasses) {
Class clazz;
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new ConfigurationException("Interceptor class was not found on classpath: " + className, e);
}
// first check if the class a Bean in the app context
Object interceptor = null;
try {
interceptor = 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 (interceptor == null) {
try {
interceptor = clazz.getConstructor().newInstance();
} catch (Exception e) {
throw new ConfigurationException("Unable to instantiate interceptor class : " + className, e);
}
}
fhirServer.registerInterceptor(interceptor);
}
}
public static IServerConformanceProvider<?> calculateConformanceProvider(IFhirSystemDao fhirSystemDao, RestfulServer fhirServer, DaoConfig daoConfig, ISearchParamRegistry searchParamRegistry, IValidationSupport theValidationSupport) { public static IServerConformanceProvider<?> calculateConformanceProvider(IFhirSystemDao fhirSystemDao, RestfulServer fhirServer, DaoConfig daoConfig, ISearchParamRegistry searchParamRegistry, IValidationSupport theValidationSupport) {
FhirVersionEnum fhirVersion = fhirSystemDao.getContext().getVersion().getVersion(); FhirVersionEnum fhirVersion = fhirSystemDao.getContext().getVersion().getVersion();
if (fhirVersion == FhirVersionEnum.DSTU2) { if (fhirVersion == FhirVersionEnum.DSTU2) {

View File

@@ -131,6 +131,14 @@ hapi:
search-coord-core-pool-size: 20 search-coord-core-pool-size: 20
search-coord-max-pool-size: 100 search-coord-max-pool-size: 100
search-coord-queue-capacity: 200 search-coord-queue-capacity: 200
# comma-separated package names, will be @ComponentScan'ed by Spring to allow for creating custom Spring beans
#custom-bean-packages:
# comma-separated list of fully qualified interceptor 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-interceptor-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

View File

@@ -0,0 +1,24 @@
package ca.uhn.fhir.jpa.starter;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class, properties = {
"hapi.fhir.custom-bean-packages=some.custom.pkg1,some.custom.pkg2",
"spring.datasource.url=jdbc:h2:mem:dbr4",
// "hapi.fhir.enable_repository_validating_interceptor=true",
"hapi.fhir.fhir_version=r4"
})
public class CustomBeanTest {
@Autowired
some.custom.pkg1.CustomBean customBean1;
@Test
void testCustomBeanExists() {
Assertions.assertNotNull(customBean1);
Assertions.assertEquals("I am alive", customBean1.getInitFlag());
}
}

View File

@@ -0,0 +1,64 @@
package ca.uhn.fhir.jpa.starter;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = Application.class, properties = {
"hapi.fhir.custom-bean-packages=some.custom.pkg1",
"hapi.fhir.custom-interceptor-classes=some.custom.pkg1.CustomInterceptorBean,some.custom.pkg1.CustomInterceptorPojo",
"spring.datasource.url=jdbc:h2:mem:dbr4",
// "hapi.fhir.enable_repository_validating_interceptor=true",
"hapi.fhir.fhir_version=r4"
})
public class CustomInterceptorTest {
@LocalServerPort
private int port;
@Autowired
private IFhirResourceDao<Patient> patientResourceDao;
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 testAuditInterceptors() {
// we registered two custom interceptors via the property 'hapi.fhir.custom-interceptor-classes'
// one is discovered as a Spring Bean, one instantiated via reflection
// both should be registered with the server and will add a custom extension to any Patient resource created
// so we can verify they were registered
Patient pat = new Patient();
String patId = client.create().resource(pat).execute().getId().getIdPart();
Patient readPat = client.read().resource(Patient.class).withId(patId).execute();
Assertions.assertNotNull(readPat.getExtensionByUrl("http://some.custom.pkg1/CustomInterceptorBean"));
Assertions.assertNotNull(readPat.getExtensionByUrl("http://some.custom.pkg1/CustomInterceptorPojo"));
}
}

View File

@@ -0,0 +1,18 @@
package some.custom.pkg1;
import org.springframework.stereotype.Component;
@Component
public class CustomBean {
private String initFlag;
public CustomBean() {
initFlag = "I am alive";
}
public String getInitFlag() {
return initFlag;
}
}

View File

@@ -0,0 +1,31 @@
package some.custom.pkg1;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType;
import org.springframework.stereotype.Component;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
@Interceptor
@Component
public class CustomInterceptorBean {
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
void preHandleResource(ServletRequestDetails servletRequestDetails, RestOperationTypeEnum opType) {
IBaseResource resource = servletRequestDetails.getResource();
// add an extension before saving the resource to mark it
if (opType == RestOperationTypeEnum.CREATE && resource instanceof Patient) {
Patient pat = (Patient) resource;
Extension ext = pat.addExtension();
ext.setUrl("http://some.custom.pkg1/CustomInterceptorBean");
ext.setValue(new StringType("CustomInterceptorBean wuz here"));
}
}
}

View File

@@ -0,0 +1,26 @@
package some.custom.pkg1;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.StringType;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
public class CustomInterceptorPojo {
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
void preHandleResource(ServletRequestDetails servletRequestDetails, RestOperationTypeEnum opType) {
IBaseResource resource = servletRequestDetails.getResource();
// add an extension before saving the resource to mark it
if (opType == RestOperationTypeEnum.CREATE && resource instanceof Patient) {
Patient pat = (Patient) resource;
Extension ext = pat.addExtension();
ext.setUrl("http://some.custom.pkg1/CustomInterceptorPojo");
ext.setValue(new StringType("CustomInterceptorPojo wuz here"));
}
}
}