allow interceptors to be registered via properties
This commit is contained in:
10
README.md
10
README.md
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
24
src/test/java/ca/uhn/fhir/jpa/starter/CustomBeanTest.java
Normal file
24
src/test/java/ca/uhn/fhir/jpa/starter/CustomBeanTest.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/test/java/some/custom/pkg1/CustomBean.java
Normal file
18
src/test/java/some/custom/pkg1/CustomBean.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
31
src/test/java/some/custom/pkg1/CustomInterceptorBean.java
Normal file
31
src/test/java/some/custom/pkg1/CustomInterceptorBean.java
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/test/java/some/custom/pkg1/CustomInterceptorPojo.java
Normal file
26
src/test/java/some/custom/pkg1/CustomInterceptorPojo.java
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user