diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..1abb583
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+target
+.idea
+.git
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index ab39bef..18220bd 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,18 +2,16 @@ FROM hapiproject/hapi:base as build-hapi
ARG HAPI_FHIR_URL=https://github.com/jamesagnew/hapi-fhir/
ARG HAPI_FHIR_BRANCH=master
-ARG HAPI_FHIR_STARTER_URL=https://github.com/hapifhir/hapi-fhir-jpaserver-starter/
-ARG HAPI_FHIR_STARTER_BRANCH=master
RUN git clone --branch ${HAPI_FHIR_BRANCH} ${HAPI_FHIR_URL}
WORKDIR /tmp/hapi-fhir/
RUN /tmp/apache-maven-3.6.2/bin/mvn dependency:resolve
RUN /tmp/apache-maven-3.6.2/bin/mvn install -DskipTests
-WORKDIR /tmp
-RUN git clone --branch ${HAPI_FHIR_STARTER_BRANCH} ${HAPI_FHIR_STARTER_URL}
-
WORKDIR /tmp/hapi-fhir-jpaserver-starter
+
+COPY . .
+
RUN /tmp/apache-maven-3.6.2/bin/mvn clean install -DskipTests
FROM tomcat:9-jre11
diff --git a/README.md b/README.md
index 8ebf6cd..25464fd 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,57 @@ In order to use this sample, you should have:
- Oracle Java (JDK) installed: Minimum JDK8 or newer.
- Apache Maven build tool (newest version)
+## Running via [Docker Hub](https://hub.docker.com/repository/docker/hapiproject/hapi)
+
+Each tagged/released version of `hapi-fhir-jpaserver` is built as a Docker image and published to Docker hub. To run the published Docker image from DockerHub:
+
+```
+docker pull hapiproject/hapi:latest
+docker run -p 8080:8080 hapiproject/hapi:tagname
+```
+
+This will run the docker image with the default configuration, mapping port 8080 from the container to port 8080 in the host. Once running, you can access `http://localhost:8080/hapi-fhir-jpaserver/fhir` in the browser to access the HAPI FHIR server's UI.
+
+If you change the mapped port, you need to change the configuration used by HAPI to have the correct `server_address` property/value.
+
+### Configuration via environment variables
+
+You can customize HAPI directly from the `run` command using environment variables. For example:
+
+`docker run -p 8090:8080 -e server_address=http://localhost:8090/hapi-fhir-jpaserver/fhir hapiproject/hapi:tagname`
+
+HAPI looks in the environment variables for properties in the [hapi.properties](https://github.com/hapifhir/hapi-fhir-jpaserver-starter/blob/master/src/main/resources/hapi.properties) file.
+
+### Configuration via overridden hapi.properties file
+
+You can customize HAPI by telling HAPI to look for the `hapi.properties` file in a different location:
+
+`docker run -p 8090:8080 -e hapi.properties=/some/directory/with/hapi.properties hapiproject/hapi:tagname`
+
+### Example docker-compose.yml
+
+```
+version: '3.7'
+services:
+ web:
+ image: "hapiproject/hapi:tagname"
+ ports:
+ - "8090:8080"
+ configs:
+ - source: hapi
+ target: /data/hapi/hapi.properties
+ volumes:
+ - hapi-data:/data/hapi
+ environment:
+ JAVA_OPTS: '-Dhapi.properties=/data/hapi/hapi.properties'
+configs:
+ hapi:
+ external: true
+volumes:
+ hapi-data:
+ external: true
+```
+
## Running locally
The easiest way to run this server is to run it directly in Maven using a built-in Jetty server. To do this, change `src/main/resources/hapi.properties` `server_address` and `server.base` with the values commented out as _For Jetty, use this_ and then execute the following command:
diff --git a/pom.xml b/pom.xml
index 2d6db5d..6f5a2a1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,7 +48,7 @@
mysql
mysql-connector-java
- 8.0.11
+ 8.0.16
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/BaseJpaRestfulServer.java b/src/main/java/ca/uhn/fhir/jpa/starter/BaseJpaRestfulServer.java
new file mode 100644
index 0000000..9eafd27
--- /dev/null
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/BaseJpaRestfulServer.java
@@ -0,0 +1,337 @@
+package ca.uhn.fhir.jpa.starter;
+
+import ca.uhn.fhir.context.FhirContext;
+import ca.uhn.fhir.context.FhirVersionEnum;
+import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
+import ca.uhn.fhir.interceptor.api.IInterceptorService;
+import ca.uhn.fhir.jpa.api.config.DaoConfig;
+import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
+import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
+import ca.uhn.fhir.jpa.api.rp.ResourceProviderFactory;
+import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor;
+import ca.uhn.fhir.jpa.bulk.BulkDataExportProvider;
+import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
+import ca.uhn.fhir.jpa.partition.PartitionManagementProvider;
+import ca.uhn.fhir.jpa.provider.GraphQLProvider;
+import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2;
+import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2;
+import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
+import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
+import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3;
+import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
+import ca.uhn.fhir.jpa.provider.r4.JpaConformanceProviderR4;
+import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4;
+import ca.uhn.fhir.jpa.provider.r5.JpaConformanceProviderR5;
+import ca.uhn.fhir.jpa.provider.r5.JpaSystemProviderR5;
+import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
+import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
+import ca.uhn.fhir.jpa.subscription.util.SubscriptionDebugLogInterceptor;
+import ca.uhn.fhir.model.dstu2.composite.MetaDt;
+import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
+import ca.uhn.fhir.rest.server.HardcodedServerAddressStrategy;
+import ca.uhn.fhir.rest.server.RestfulServer;
+import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
+import ca.uhn.fhir.rest.server.interceptor.FhirPathFilterInterceptor;
+import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
+import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
+import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
+import ca.uhn.fhir.rest.server.interceptor.ResponseValidatingInterceptor;
+import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor;
+import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy;
+import ca.uhn.fhir.validation.IValidatorModule;
+import ca.uhn.fhir.validation.ResultSeverityEnum;
+import org.hl7.fhir.dstu3.model.Bundle;
+import org.hl7.fhir.dstu3.model.Meta;
+import org.hl7.fhir.r4.model.Bundle.BundleType;
+import org.springframework.context.ApplicationContext;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.cors.CorsConfiguration;
+
+import javax.servlet.ServletException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.TreeSet;
+
+public class BaseJpaRestfulServer extends RestfulServer {
+
+ private static final long serialVersionUID = 1L;
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void initialize() throws ServletException {
+ super.initialize();
+
+ /*
+ * Create a FhirContext object that uses the version of FHIR
+ * specified in the properties file.
+ */
+ ApplicationContext appCtx = (ApplicationContext) getServletContext()
+ .getAttribute("org.springframework.web.context.WebApplicationContext.ROOT");
+ // Customize supported resource types
+ Set supportedResourceTypes = HapiProperties.getSupportedResourceTypes();
+
+ if (!supportedResourceTypes.isEmpty() && !supportedResourceTypes.contains("SearchParameter")) {
+ supportedResourceTypes.add("SearchParameter");
+ }
+
+ if (!supportedResourceTypes.isEmpty()) {
+ DaoRegistry daoRegistry = appCtx.getBean(DaoRegistry.class);
+ daoRegistry.setSupportedResourceTypes(supportedResourceTypes);
+ }
+
+ /*
+ * ResourceProviders are fetched from the Spring context
+ */
+ FhirVersionEnum fhirVersion = HapiProperties.getFhirVersion();
+ ResourceProviderFactory resourceProviders;
+ Object systemProvider;
+ if (fhirVersion == FhirVersionEnum.DSTU2) {
+ resourceProviders = appCtx.getBean("myResourceProvidersDstu2", ResourceProviderFactory.class);
+ systemProvider = appCtx.getBean("mySystemProviderDstu2", JpaSystemProviderDstu2.class);
+ } else if (fhirVersion == FhirVersionEnum.DSTU3) {
+ resourceProviders = appCtx.getBean("myResourceProvidersDstu3", ResourceProviderFactory.class);
+ systemProvider = appCtx.getBean("mySystemProviderDstu3", JpaSystemProviderDstu3.class);
+ } else if (fhirVersion == FhirVersionEnum.R4) {
+ resourceProviders = appCtx.getBean("myResourceProvidersR4", ResourceProviderFactory.class);
+ systemProvider = appCtx.getBean("mySystemProviderR4", JpaSystemProviderR4.class);
+ } else if (fhirVersion == FhirVersionEnum.R5) {
+ resourceProviders = appCtx.getBean("myResourceProvidersR5", ResourceProviderFactory.class);
+ systemProvider = appCtx.getBean("mySystemProviderR5", JpaSystemProviderR5.class);
+ } else {
+ throw new IllegalStateException();
+ }
+
+ setFhirContext(appCtx.getBean(FhirContext.class));
+
+ registerProviders(resourceProviders.createProviders());
+ registerProvider(systemProvider);
+
+ /*
+ * The conformance provider exports the supported resources, search parameters, etc for
+ * this server. The JPA version adds resourceProviders counts to the exported statement, so it
+ * is a nice addition.
+ *
+ * You can also create your own subclass of the conformance provider if you need to
+ * provide further customization of your server's CapabilityStatement
+ */
+ DaoConfig daoConfig = appCtx.getBean(DaoConfig.class);
+ ISearchParamRegistry searchParamRegistry = appCtx.getBean(ISearchParamRegistry.class);
+ if (fhirVersion == FhirVersionEnum.DSTU2) {
+ IFhirSystemDao systemDao = appCtx.getBean("mySystemDaoDstu2", IFhirSystemDao.class);
+ JpaConformanceProviderDstu2 confProvider = new JpaConformanceProviderDstu2(this, systemDao, daoConfig);
+ confProvider.setImplementationDescription("HAPI FHIR DSTU2 Server");
+ setServerConformanceProvider(confProvider);
+ } else {
+ if (fhirVersion == FhirVersionEnum.DSTU3) {
+ IFhirSystemDao systemDao = appCtx.getBean("mySystemDaoDstu3", IFhirSystemDao.class);
+ JpaConformanceProviderDstu3 confProvider = new JpaConformanceProviderDstu3(this, systemDao, daoConfig, searchParamRegistry);
+ confProvider.setImplementationDescription("HAPI FHIR DSTU3 Server");
+ setServerConformanceProvider(confProvider);
+ } else if (fhirVersion == FhirVersionEnum.R4) {
+ IFhirSystemDao systemDao = appCtx.getBean("mySystemDaoR4", IFhirSystemDao.class);
+ JpaConformanceProviderR4 confProvider = new JpaConformanceProviderR4(this, systemDao, daoConfig, searchParamRegistry);
+ confProvider.setImplementationDescription("HAPI FHIR R4 Server");
+ setServerConformanceProvider(confProvider);
+ } else if (fhirVersion == FhirVersionEnum.R5) {
+ IFhirSystemDao systemDao = appCtx.getBean("mySystemDaoR5", IFhirSystemDao.class);
+ JpaConformanceProviderR5 confProvider = new JpaConformanceProviderR5(this, systemDao, daoConfig, searchParamRegistry);
+ confProvider.setImplementationDescription("HAPI FHIR R5 Server");
+ setServerConformanceProvider(confProvider);
+ } else {
+ throw new IllegalStateException();
+ }
+ }
+
+ /*
+ * ETag Support
+ */
+ setETagSupport(HapiProperties.getEtagSupport());
+
+ /*
+ * This server tries to dynamically generate narratives
+ */
+ FhirContext ctx = getFhirContext();
+ ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
+
+ /*
+ * Default to JSON and pretty printing
+ */
+ setDefaultPrettyPrint(HapiProperties.getDefaultPrettyPrint());
+
+ /*
+ * Default encoding
+ */
+ setDefaultResponseEncoding(HapiProperties.getDefaultEncoding());
+
+ /*
+ * This configures the server to page search results to and from
+ * the database, instead of only paging them to memory. This may mean
+ * a performance hit when performing searches that return lots of results,
+ * but makes the server much more scalable.
+ */
+ setPagingProvider(appCtx.getBean(DatabaseBackedPagingProvider.class));
+
+ /*
+ * This interceptor formats the output using nice colourful
+ * HTML output when the request is detected to come from a
+ * browser.
+ */
+ ResponseHighlighterInterceptor responseHighlighterInterceptor = new ResponseHighlighterInterceptor();
+ this.registerInterceptor(responseHighlighterInterceptor);
+
+ if (HapiProperties.isFhirPathFilterInterceptorEnabled()) {
+ registerInterceptor(new FhirPathFilterInterceptor());
+ }
+
+ /*
+ * Add some logging for each request
+ */
+ LoggingInterceptor loggingInterceptor = new LoggingInterceptor();
+ loggingInterceptor.setLoggerName(HapiProperties.getLoggerName());
+ loggingInterceptor.setMessageFormat(HapiProperties.getLoggerFormat());
+ loggingInterceptor.setErrorMessageFormat(HapiProperties.getLoggerErrorFormat());
+ loggingInterceptor.setLogExceptions(HapiProperties.getLoggerLogExceptions());
+ this.registerInterceptor(loggingInterceptor);
+
+ /*
+ * If you are hosting this server at a specific DNS name, the server will try to
+ * figure out the FHIR base URL based on what the web container tells it, but
+ * this doesn't always work. If you are setting links in your search bundles that
+ * just refer to "localhost", you might want to use a server address strategy:
+ */
+ String serverAddress = HapiProperties.getServerAddress();
+ if (serverAddress != null && serverAddress.length() > 0) {
+ setServerAddressStrategy(new HardcodedServerAddressStrategy(serverAddress));
+ }
+
+ /*
+ * If you are using DSTU3+, you may want to add a terminology uploader, which allows
+ * uploading of external terminologies such as Snomed CT. Note that this uploader
+ * does not have any security attached (any anonymous user may use it by default)
+ * so it is a potential security vulnerability. Consider using an AuthorizationInterceptor
+ * with this feature.
+ */
+ if (false) { // <-- DISABLED RIGHT NOW
+ registerProvider(appCtx.getBean(TerminologyUploaderProvider.class));
+ }
+
+ // If you want to enable the $trigger-subscription operation to allow
+ // manual triggering of a subscription delivery, enable this provider
+ if (false) { // <-- DISABLED RIGHT NOW
+ SubscriptionTriggeringProvider retriggeringProvider = appCtx
+ .getBean(SubscriptionTriggeringProvider.class);
+ registerProvider(retriggeringProvider);
+ }
+
+ // Define your CORS configuration. This is an example
+ // showing a typical setup. You should customize this
+ // to your specific needs
+ if (HapiProperties.getCorsEnabled()) {
+ CorsConfiguration config = new CorsConfiguration();
+ config.addAllowedHeader(HttpHeaders.ORIGIN);
+ config.addAllowedHeader(HttpHeaders.ACCEPT);
+ config.addAllowedHeader(HttpHeaders.CONTENT_TYPE);
+ config.addAllowedHeader(HttpHeaders.AUTHORIZATION);
+ config.addAllowedHeader(HttpHeaders.CACHE_CONTROL);
+ config.addAllowedHeader("x-fhir-starter");
+ config.addAllowedHeader("X-Requested-With");
+ config.addAllowedHeader("Prefer");
+ String allAllowedCORSOrigins = HapiProperties.getCorsAllowedOrigin();
+ Arrays.stream(allAllowedCORSOrigins.split(",")).forEach(o -> {
+ config.addAllowedOrigin(o);
+ });
+ config.addAllowedOrigin(HapiProperties.getCorsAllowedOrigin());
+
+ config.addExposedHeader("Location");
+ config.addExposedHeader("Content-Location");
+ config.setAllowedMethods(
+ Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD"));
+ config.setAllowCredentials(HapiProperties.getCorsAllowedCredentials());
+
+ // Create the interceptor and register it
+ CorsInterceptor interceptor = new CorsInterceptor(config);
+ registerInterceptor(interceptor);
+ }
+
+ // If subscriptions are enabled, we want to register the interceptor that
+ // will activate them and match results against them
+ if (HapiProperties.getSubscriptionWebsocketEnabled() ||
+ HapiProperties.getSubscriptionEmailEnabled() ||
+ HapiProperties.getSubscriptionRestHookEnabled()) {
+ // Subscription debug logging
+ IInterceptorService interceptorService = appCtx.getBean(IInterceptorService.class);
+ interceptorService.registerInterceptor(new SubscriptionDebugLogInterceptor());
+ }
+
+ // Cascading deletes
+ DaoRegistry daoRegistry = appCtx.getBean(DaoRegistry.class);
+ IInterceptorBroadcaster interceptorBroadcaster = appCtx.getBean(IInterceptorBroadcaster.class);
+ if (HapiProperties.getAllowCascadingDeletes()) {
+ CascadingDeleteInterceptor cascadingDeleteInterceptor = new CascadingDeleteInterceptor(ctx, daoRegistry, interceptorBroadcaster);
+ getInterceptorService().registerInterceptor(cascadingDeleteInterceptor);
+ }
+
+ // Binary Storage
+ if (HapiProperties.isBinaryStorageEnabled()) {
+ BinaryStorageInterceptor binaryStorageInterceptor = appCtx
+ .getBean(BinaryStorageInterceptor.class);
+ getInterceptorService().registerInterceptor(binaryStorageInterceptor);
+ }
+
+ // Validation
+ IValidatorModule validatorModule = appCtx.getBean(IValidatorModule.class);
+ if (validatorModule != null) {
+ if (HapiProperties.getValidateRequestsEnabled()) {
+ RequestValidatingInterceptor interceptor = new RequestValidatingInterceptor();
+ interceptor.setFailOnSeverity(ResultSeverityEnum.ERROR);
+ interceptor.setValidatorModules(Collections.singletonList(validatorModule));
+ registerInterceptor(interceptor);
+ }
+ if (HapiProperties.getValidateResponsesEnabled()) {
+ ResponseValidatingInterceptor interceptor = new ResponseValidatingInterceptor();
+ interceptor.setFailOnSeverity(ResultSeverityEnum.ERROR);
+ interceptor.setValidatorModules(Collections.singletonList(validatorModule));
+ registerInterceptor(interceptor);
+ }
+ }
+
+ // GraphQL
+ if (HapiProperties.getGraphqlEnabled()) {
+ if (fhirVersion.isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
+ registerProvider(appCtx.getBean(GraphQLProvider.class));
+ }
+ }
+
+ if (!HapiProperties.getAllowedBundleTypes().isEmpty()) {
+ String allowedBundleTypesString = HapiProperties.getAllowedBundleTypes();
+ Set allowedBundleTypes = new HashSet<>();
+ Arrays.stream(allowedBundleTypesString.split(",")).forEach(o -> {
+ BundleType type = BundleType.valueOf(o);
+ allowedBundleTypes.add(type.toCode());
+ });
+ DaoConfig config = daoConfig;
+ config.setBundleTypesAllowedForStorage(
+ Collections.unmodifiableSet(new TreeSet<>(allowedBundleTypes)));
+ }
+
+ // Bulk Export
+ if (HapiProperties.getBulkExportEnabled()) {
+ registerProvider(appCtx.getBean(BulkDataExportProvider.class));
+ }
+
+ // Partitioning
+ if (HapiProperties.getPartitioningMultitenancyEnabled()) {
+ registerInterceptor(new RequestTenantPartitionInterceptor());
+ setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy());
+ registerProviders(appCtx.getBean(PartitionManagementProvider.class));
+ }
+
+ if (HapiProperties.getClientIdStrategy() == DaoConfig.ClientIdStrategyEnum.ANY) {
+ daoConfig.setResourceServerIdStrategy(DaoConfig.IdStrategyEnum.UUID);
+ daoConfig.setResourceClientIdStrategy(HapiProperties.getClientIdStrategy());
+ }
+ }
+
+}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java b/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java
index ab66ef2..5cd67ea 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java
@@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.starter;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirVersionEnum;
+import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.search.elastic.ElasticsearchHibernatePropertiesBuilder;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.ETagSupportEnum;
@@ -17,6 +18,7 @@ import java.util.Arrays;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;
+import java.util.Map;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.defaultString;
@@ -78,6 +80,7 @@ public class HapiProperties {
static final String MAX_BINARY_SIZE = "max_binary_size";
static final String PARTITIONING_MULTITENANCY_ENABLED = "partitioning.multitenancy.enabled";
private static final String PARTITIONING_INCLUDE_PARTITION_IN_SEARCH_HASHES = "partitioning.partitioning_include_in_search_hashes";
+ static final String CLIENT_ID_STRATEGY = "daoconfig.client_id_strategy";
private static Properties ourProperties;
public static boolean isElasticSearchEnabled() {
@@ -143,6 +146,10 @@ public class HapiProperties {
if (overrideProps != null) {
properties.putAll(overrideProps);
}
+ properties.putAll(System.getenv().entrySet()
+ .stream()
+ .filter(e -> e.getValue() != null && properties.containsKey(e.getKey()))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
return properties;
}
@@ -243,6 +250,16 @@ public class HapiProperties {
return ETagSupportEnum.ENABLED;
}
+ public static DaoConfig.ClientIdStrategyEnum getClientIdStrategy() {
+ String idStrategy = HapiProperties.getProperty(CLIENT_ID_STRATEGY);
+
+ if (idStrategy != null && idStrategy.length() > 0) {
+ return DaoConfig.ClientIdStrategyEnum.valueOf(idStrategy);
+ }
+
+ return DaoConfig.ClientIdStrategyEnum.ALPHANUMERIC;
+ }
+
public static EncodingEnum getDefaultEncoding() {
String defaultEncodingString = HapiProperties.getProperty(DEFAULT_ENCODING);
@@ -515,5 +532,8 @@ public class HapiProperties {
return HapiProperties.getBooleanProperty("fhirpath_interceptor.enabled", false);
}
+ public static boolean getPartitioningMultitenancyEnabled() {
+ return HapiProperties.getBooleanProperty(PARTITIONING_MULTITENANCY_ENABLED, false);
+ }
}
diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java b/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java
index 275b62f..f13ab64 100644
--- a/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java
+++ b/src/main/java/ca/uhn/fhir/jpa/starter/JpaRestfulServer.java
@@ -1,342 +1,17 @@
package ca.uhn.fhir.jpa.starter;
-import ca.uhn.fhir.context.FhirContext;
-import ca.uhn.fhir.context.FhirVersionEnum;
-import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
-import ca.uhn.fhir.interceptor.api.IInterceptorService;
-import ca.uhn.fhir.jpa.api.config.DaoConfig;
-import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
-import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
-import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor;
-import ca.uhn.fhir.jpa.bulk.provider.BulkDataExportProvider;
-import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
-import ca.uhn.fhir.jpa.model.config.PartitionSettings;
-import ca.uhn.fhir.jpa.partition.PartitionManagementProvider;
-import ca.uhn.fhir.jpa.provider.GraphQLProvider;
-import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu2;
-import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2;
-import ca.uhn.fhir.jpa.provider.SubscriptionTriggeringProvider;
-import ca.uhn.fhir.jpa.provider.TerminologyUploaderProvider;
-import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3;
-import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3;
-import ca.uhn.fhir.jpa.provider.r4.JpaConformanceProviderR4;
-import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4;
-import ca.uhn.fhir.jpa.provider.r5.JpaConformanceProviderR5;
-import ca.uhn.fhir.jpa.provider.r5.JpaSystemProviderR5;
-import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
-import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
-import ca.uhn.fhir.jpa.subscription.util.SubscriptionDebugLogInterceptor;
-import ca.uhn.fhir.model.dstu2.composite.MetaDt;
-import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
-import ca.uhn.fhir.rest.server.HardcodedServerAddressStrategy;
-import ca.uhn.fhir.rest.server.RestfulServer;
-import ca.uhn.fhir.rest.server.interceptor.CorsInterceptor;
-import ca.uhn.fhir.rest.server.interceptor.FhirPathFilterInterceptor;
-import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor;
-import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
-import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
-import ca.uhn.fhir.rest.server.interceptor.ResponseValidatingInterceptor;
-import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor;
-import ca.uhn.fhir.rest.server.provider.ResourceProviderFactory;
-import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy;
-import ca.uhn.fhir.validation.IValidatorModule;
-import ca.uhn.fhir.validation.ResultSeverityEnum;
-import org.hl7.fhir.dstu3.model.Bundle;
-import org.hl7.fhir.dstu3.model.Meta;
-import org.hl7.fhir.r4.model.Bundle.BundleType;
-import org.springframework.context.ApplicationContext;
-import org.springframework.http.HttpHeaders;
-import org.springframework.web.cors.CorsConfiguration;
-
import javax.servlet.ServletException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.TreeSet;
-public class JpaRestfulServer extends RestfulServer {
+public class JpaRestfulServer extends BaseJpaRestfulServer {
private static final long serialVersionUID = 1L;
- @SuppressWarnings("unchecked")
@Override
protected void initialize() throws ServletException {
super.initialize();
- /*
- * Create a FhirContext object that uses the version of FHIR
- * specified in the properties file.
- */
- ApplicationContext appCtx = (ApplicationContext) getServletContext()
- .getAttribute("org.springframework.web.context.WebApplicationContext.ROOT");
- // Customize supported resource types
- Set supportedResourceTypes = HapiProperties.getSupportedResourceTypes();
-
- if (!supportedResourceTypes.isEmpty() && !supportedResourceTypes.contains("SearchParameter")) {
- supportedResourceTypes.add("SearchParameter");
- }
-
- if (!supportedResourceTypes.isEmpty()) {
- DaoRegistry daoRegistry = appCtx.getBean(DaoRegistry.class);
- daoRegistry.setSupportedResourceTypes(supportedResourceTypes);
- }
-
- /*
- * ResourceProviders are fetched from the Spring context
- */
- FhirVersionEnum fhirVersion = HapiProperties.getFhirVersion();
- ResourceProviderFactory resourceProviders;
- Object systemProvider;
- if (fhirVersion == FhirVersionEnum.DSTU2) {
- resourceProviders = appCtx.getBean("myResourceProvidersDstu2", ResourceProviderFactory.class);
- systemProvider = appCtx.getBean("mySystemProviderDstu2", JpaSystemProviderDstu2.class);
- } else if (fhirVersion == FhirVersionEnum.DSTU3) {
- resourceProviders = appCtx.getBean("myResourceProvidersDstu3", ResourceProviderFactory.class);
- systemProvider = appCtx.getBean("mySystemProviderDstu3", JpaSystemProviderDstu3.class);
- } else if (fhirVersion == FhirVersionEnum.R4) {
- resourceProviders = appCtx.getBean("myResourceProvidersR4", ResourceProviderFactory.class);
- systemProvider = appCtx.getBean("mySystemProviderR4", JpaSystemProviderR4.class);
- } else if (fhirVersion == FhirVersionEnum.R5) {
- resourceProviders = appCtx.getBean("myResourceProvidersR5", ResourceProviderFactory.class);
- systemProvider = appCtx.getBean("mySystemProviderR5", JpaSystemProviderR5.class);
- } else {
- throw new IllegalStateException();
- }
-
- setFhirContext(appCtx.getBean(FhirContext.class));
-
- registerProviders(resourceProviders.createProviders());
- registerProvider(systemProvider);
-
- /*
- * The conformance provider exports the supported resources, search parameters, etc for
- * this server. The JPA version adds resourceProviders counts to the exported statement, so it
- * is a nice addition.
- *
- * You can also create your own subclass of the conformance provider if you need to
- * provide further customization of your server's CapabilityStatement
- */
- DaoConfig daoConfig = appCtx.getBean(DaoConfig.class);
- ISearchParamRegistry searchParamRegistry = appCtx.getBean(ISearchParamRegistry.class);
- if (fhirVersion == FhirVersionEnum.DSTU2) {
- IFhirSystemDao systemDao = appCtx.getBean("mySystemDaoDstu2", IFhirSystemDao.class);
- JpaConformanceProviderDstu2 confProvider = new JpaConformanceProviderDstu2(this, systemDao, daoConfig);
- confProvider.setImplementationDescription("HAPI FHIR DSTU2 Server");
- setServerConformanceProvider(confProvider);
- } else {
- if (fhirVersion == FhirVersionEnum.DSTU3) {
- IFhirSystemDao systemDao = appCtx.getBean("mySystemDaoDstu3", IFhirSystemDao.class);
- JpaConformanceProviderDstu3 confProvider = new JpaConformanceProviderDstu3(this, systemDao, daoConfig, searchParamRegistry);
- confProvider.setImplementationDescription("HAPI FHIR DSTU3 Server");
- setServerConformanceProvider(confProvider);
- } else if (fhirVersion == FhirVersionEnum.R4) {
- IFhirSystemDao systemDao = appCtx.getBean("mySystemDaoR4", IFhirSystemDao.class);
- JpaConformanceProviderR4 confProvider = new JpaConformanceProviderR4(this, systemDao, daoConfig, searchParamRegistry);
- confProvider.setImplementationDescription("HAPI FHIR R4 Server");
- setServerConformanceProvider(confProvider);
- } else if (fhirVersion == FhirVersionEnum.R5) {
- IFhirSystemDao systemDao = appCtx.getBean("mySystemDaoR5", IFhirSystemDao.class);
- JpaConformanceProviderR5 confProvider = new JpaConformanceProviderR5(this, systemDao, daoConfig, searchParamRegistry);
- confProvider.setImplementationDescription("HAPI FHIR R5 Server");
- setServerConformanceProvider(confProvider);
- } else {
- throw new IllegalStateException();
- }
- }
-
- /*
- * ETag Support
- */
- setETagSupport(HapiProperties.getEtagSupport());
-
- /*
- * This server tries to dynamically generate narratives
- */
- FhirContext ctx = getFhirContext();
- ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
-
- /*
- * Default to JSON and pretty printing
- */
- setDefaultPrettyPrint(HapiProperties.getDefaultPrettyPrint());
-
- /*
- * Default encoding
- */
- setDefaultResponseEncoding(HapiProperties.getDefaultEncoding());
-
- /*
- * This configures the server to page search results to and from
- * the database, instead of only paging them to memory. This may mean
- * a performance hit when performing searches that return lots of results,
- * but makes the server much more scalable.
- */
- setPagingProvider(appCtx.getBean(DatabaseBackedPagingProvider.class));
-
- /*
- * This interceptor formats the output using nice colourful
- * HTML output when the request is detected to come from a
- * browser.
- */
- ResponseHighlighterInterceptor responseHighlighterInterceptor = new ResponseHighlighterInterceptor();
- this.registerInterceptor(responseHighlighterInterceptor);
-
- if (HapiProperties.isFhirPathFilterInterceptorEnabled()) {
- registerInterceptor(new FhirPathFilterInterceptor());
- }
-
- /*
- * Add some logging for each request
- */
- LoggingInterceptor loggingInterceptor = new LoggingInterceptor();
- loggingInterceptor.setLoggerName(HapiProperties.getLoggerName());
- loggingInterceptor.setMessageFormat(HapiProperties.getLoggerFormat());
- loggingInterceptor.setErrorMessageFormat(HapiProperties.getLoggerErrorFormat());
- loggingInterceptor.setLogExceptions(HapiProperties.getLoggerLogExceptions());
- this.registerInterceptor(loggingInterceptor);
-
- /*
- * If you are hosting this server at a specific DNS name, the server will try to
- * figure out the FHIR base URL based on what the web container tells it, but
- * this doesn't always work. If you are setting links in your search bundles that
- * just refer to "localhost", you might want to use a server address strategy:
- */
- String serverAddress = HapiProperties.getServerAddress();
- if (serverAddress != null && serverAddress.length() > 0) {
- setServerAddressStrategy(new HardcodedServerAddressStrategy(serverAddress));
- }
-
- /*
- * If you are using DSTU3+, you may want to add a terminology uploader, which allows
- * uploading of external terminologies such as Snomed CT. Note that this uploader
- * does not have any security attached (any anonymous user may use it by default)
- * so it is a potential security vulnerability. Consider using an AuthorizationInterceptor
- * with this feature.
- */
- if (false) { // <-- DISABLED RIGHT NOW
- registerProvider(appCtx.getBean(TerminologyUploaderProvider.class));
- }
-
- // If you want to enable the $trigger-subscription operation to allow
- // manual triggering of a subscription delivery, enable this provider
- if (false) { // <-- DISABLED RIGHT NOW
- SubscriptionTriggeringProvider retriggeringProvider = appCtx
- .getBean(SubscriptionTriggeringProvider.class);
- registerProvider(retriggeringProvider);
- }
-
- // Define your CORS configuration. This is an example
- // showing a typical setup. You should customize this
- // to your specific needs
- if (HapiProperties.getCorsEnabled()) {
- CorsConfiguration config = new CorsConfiguration();
- config.addAllowedHeader(HttpHeaders.ORIGIN);
- config.addAllowedHeader(HttpHeaders.ACCEPT);
- config.addAllowedHeader(HttpHeaders.CONTENT_TYPE);
- config.addAllowedHeader(HttpHeaders.AUTHORIZATION);
- config.addAllowedHeader(HttpHeaders.CACHE_CONTROL);
- config.addAllowedHeader("x-fhir-starter");
- config.addAllowedHeader("X-Requested-With");
- config.addAllowedHeader("Prefer");
- String allAllowedCORSOrigins = HapiProperties.getCorsAllowedOrigin();
- Arrays.stream(allAllowedCORSOrigins.split(",")).forEach(o -> {
- config.addAllowedOrigin(o);
- });
- config.addAllowedOrigin(HapiProperties.getCorsAllowedOrigin());
-
- config.addExposedHeader("Location");
- config.addExposedHeader("Content-Location");
- config.setAllowedMethods(
- Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD"));
- config.setAllowCredentials(HapiProperties.getCorsAllowedCredentials());
-
- // Create the interceptor and register it
- CorsInterceptor interceptor = new CorsInterceptor(config);
- registerInterceptor(interceptor);
- }
-
- // If subscriptions are enabled, we want to register the interceptor that
- // will activate them and match results against them
- if (HapiProperties.getSubscriptionWebsocketEnabled() ||
- HapiProperties.getSubscriptionEmailEnabled() ||
- HapiProperties.getSubscriptionRestHookEnabled()) {
- // Subscription debug logging
- IInterceptorService interceptorService = appCtx.getBean(IInterceptorService.class);
- interceptorService.registerInterceptor(new SubscriptionDebugLogInterceptor());
- }
-
- // Cascading deletes
- DaoRegistry daoRegistry = appCtx.getBean(DaoRegistry.class);
- IInterceptorBroadcaster interceptorBroadcaster = appCtx.getBean(IInterceptorBroadcaster.class);
- if (HapiProperties.getAllowCascadingDeletes()) {
- CascadingDeleteInterceptor cascadingDeleteInterceptor = new CascadingDeleteInterceptor(ctx, daoRegistry, interceptorBroadcaster);
- getInterceptorService().registerInterceptor(cascadingDeleteInterceptor);
- }
-
- // Binary Storage
- if (HapiProperties.isBinaryStorageEnabled()) {
- BinaryStorageInterceptor binaryStorageInterceptor = appCtx
- .getBean(BinaryStorageInterceptor.class);
- getInterceptorService().registerInterceptor(binaryStorageInterceptor);
- }
-
- // Validation
- IValidatorModule validatorModule = appCtx.getBean(IValidatorModule.class);
- if (validatorModule != null) {
- if (HapiProperties.getValidateRequestsEnabled()) {
- RequestValidatingInterceptor interceptor = new RequestValidatingInterceptor();
- interceptor.setFailOnSeverity(ResultSeverityEnum.ERROR);
- interceptor.setValidatorModules(Collections.singletonList(validatorModule));
- registerInterceptor(interceptor);
- }
- if (HapiProperties.getValidateResponsesEnabled()) {
- ResponseValidatingInterceptor interceptor = new ResponseValidatingInterceptor();
- interceptor.setFailOnSeverity(ResultSeverityEnum.ERROR);
- interceptor.setValidatorModules(Collections.singletonList(validatorModule));
- registerInterceptor(interceptor);
- }
- }
-
- // GraphQL
- if (HapiProperties.getGraphqlEnabled()) {
- if (fhirVersion.isEqualOrNewerThan(FhirVersionEnum.DSTU3)) {
- registerProvider(appCtx.getBean(GraphQLProvider.class));
- }
- }
-
- if (!HapiProperties.getAllowedBundleTypes().isEmpty()) {
- String allowedBundleTypesString = HapiProperties.getAllowedBundleTypes();
- Set allowedBundleTypes = new HashSet<>();
- Arrays.stream(allowedBundleTypesString.split(",")).forEach(o -> {
- BundleType type = BundleType.valueOf(o);
- allowedBundleTypes.add(type.toCode());
- });
- DaoConfig config = daoConfig;
- config.setBundleTypesAllowedForStorage(
- Collections.unmodifiableSet(new TreeSet<>(allowedBundleTypes)));
- }
-
- // Bulk Export
- if (HapiProperties.getBulkExportEnabled()) {
- registerProvider(appCtx.getBean(BulkDataExportProvider.class));
- }
-
- if (HapiProperties.getPartitioningEnabled()) {
- PartitionSettings partitionSettings = appCtx.getBean(PartitionSettings.class);
- partitionSettings.setPartitioningEnabled(true);
- PartitionSettings.CrossPartitionReferenceMode mode = PartitionSettings.CrossPartitionReferenceMode.valueOf(HapiProperties.getPartitioningCrossPartitionReferenceMode());
- partitionSettings.setAllowReferencesAcrossPartitions(mode);
- partitionSettings.setIncludePartitionInSearchHashes(HapiProperties.getIncludePartitionInSearchHashes());
- registerProvider(appCtx.getBean(PartitionManagementProvider.class));
-
- if (HapiProperties.getPartitioningMultitenancyEnabled()) {
- registerInterceptor(new RequestTenantPartitionInterceptor());
- setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy());
- }
-
- }
+ // Add your own customization here
}
-}
\ No newline at end of file
+}
diff --git a/src/main/resources/hapi.properties b/src/main/resources/hapi.properties
index edcbee1..b7ef0f8 100644
--- a/src/main/resources/hapi.properties
+++ b/src/main/resources/hapi.properties
@@ -124,7 +124,7 @@ cors.enabled=true
cors.allowCredentials=true
# Supports multiple, comma separated allowed origin entries
# cors.allowed_origin=http://localhost:8080,https://localhost:8080,https://fhirtest.uhn.ca
-cors.allow_origin=*
+cors.allowed_origin=*
##################################################
# Allowed Bundle Types for persistence (defaults are: COLLECTION,DOCUMENT,MESSAGE)
@@ -163,3 +163,5 @@ partitioning.cross_partition_reference_mode=NOT_ALLOWED
partitioning.partitioning_include_in_search_hashes=true
partitioning.multitenancy.enabled=false
+#daoconfig.client_id_strategy=ANY
+
diff --git a/src/test/java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java b/src/test/java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java
index 7e32013..a2a6148 100644
--- a/src/test/java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java
+++ b/src/test/java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java
@@ -17,7 +17,6 @@ import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.junit.AfterClass;
import org.junit.BeforeClass;
-import org.junit.Ignore;
import org.junit.Test;
import java.nio.file.Paths;