From e2eee4e4e58eefe83e8ff4e27cbc1e724802c7a6 Mon Sep 17 00:00:00 2001 From: Peter Micuch Date: Thu, 14 May 2020 11:44:46 +0200 Subject: [PATCH 01/10] Adding environment variables to override hapi properties file settings --- src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java | 1 + 1 file changed, 1 insertion(+) 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 5be55f4..4446d30 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java @@ -138,6 +138,7 @@ public class HapiProperties { if (overrideProps != null) { properties.putAll(overrideProps); } + properties.putAll(System.getenv()); return properties; } From c74d45059d554179959e9a12a708765a242c395e Mon Sep 17 00:00:00 2001 From: Vladimir Nemergut Date: Fri, 15 May 2020 11:44:38 +0200 Subject: [PATCH 02/10] Only override existing hapi properties with env ones --- src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 4446d30..d396880 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/HapiProperties.java @@ -17,6 +17,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; @@ -138,7 +139,10 @@ public class HapiProperties { if (overrideProps != null) { properties.putAll(overrideProps); } - properties.putAll(System.getenv()); + 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; } From 669f53bacf70a0704cbb75a9ac3f07715da0d077 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Tue, 16 Jun 2020 09:40:15 -0400 Subject: [PATCH 03/10] Bump to current release --- pom.xml | 2 +- .../java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 61f82de..c37158b 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.0.0 + 5.0.2 hapi-fhir-jpaserver-starter 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 23fb19a..6db05fb 100644 --- a/src/test/java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java +++ b/src/test/java/ca/uhn/fhir/jpa/starter/MultitenantServerR4IT.java @@ -10,7 +10,6 @@ import ca.uhn.fhir.rest.client.interceptor.UrlTenantSelectionInterceptor; import ca.uhn.fhir.test.utilities.JettyUtil; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.webapp.WebAppContext; -import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.IntegerType; @@ -18,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; @@ -70,7 +68,6 @@ public class MultitenantServerR4IT { } @Test - @Ignore public void testCreateAndReadInTenantB() { ourLog.info("Base URL is: " + HapiProperties.getServerAddress()); @@ -80,7 +77,7 @@ public class MultitenantServerR4IT { .operation() .onServer() .named(ProviderConstants.PARTITION_MANAGEMENT_CREATE_PARTITION) - .withParameter(Parameters.class, ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, new IntegerType(1)) + .withParameter(Parameters.class, ProviderConstants.PARTITION_MANAGEMENT_PARTITION_ID, new IntegerType(2)) .andParameter(ProviderConstants.PARTITION_MANAGEMENT_PARTITION_NAME, new CodeType("TENANT-B")) .execute(); From a5c1769b91c78dd9efe90f5b7a3f513c7ebe49c9 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Tue, 16 Jun 2020 09:57:49 -0400 Subject: [PATCH 04/10] Rework the JPA server class a bit --- .../jpa/starter/BaseJpaRestfulServer.java | 333 ++++++++++++++++++ .../fhir/jpa/starter/JpaRestfulServer.java | 320 +---------------- 2 files changed, 335 insertions(+), 318 deletions(-) create mode 100644 src/main/java/ca/uhn/fhir/jpa/starter/BaseJpaRestfulServer.java 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..b809cf0 --- /dev/null +++ b/src/main/java/ca/uhn/fhir/jpa/starter/BaseJpaRestfulServer.java @@ -0,0 +1,333 @@ +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) 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)); + } + + } + +} 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 b239233..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,332 +1,16 @@ 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 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) 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)); - } + // Add your own customization here } From 213bda7cfcc2d5f6f150b8781093b315a17a43c2 Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 29 Jun 2020 15:07:46 -0700 Subject: [PATCH 05/10] Updating Dockerfile to use context code instead of re-cloning the hapi-fhir-jpaserver-starter project. Still re-builds the base hapi libraries, though. --- Dockerfile | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 From 05daeff06a5d2ad84409cd5878ce068283960b0e Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Mon, 29 Jun 2020 15:40:58 -0700 Subject: [PATCH 06/10] Adding .dockerignore file to improve the efficiency of docker builds --- .dockerignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .dockerignore 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 From 25a3da1b83ad67b5bc5a133d4a7c4c250f483d7c Mon Sep 17 00:00:00 2001 From: jvi Date: Wed, 1 Jul 2020 12:16:09 +0200 Subject: [PATCH 07/10] Added support for configurable client ID strategy --- .../uhn/fhir/jpa/starter/BaseJpaRestfulServer.java | 6 +++++- .../java/ca/uhn/fhir/jpa/starter/HapiProperties.java | 12 ++++++++++++ src/main/resources/hapi.properties | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/BaseJpaRestfulServer.java b/src/main/java/ca/uhn/fhir/jpa/starter/BaseJpaRestfulServer.java index b809cf0..9eafd27 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/BaseJpaRestfulServer.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/BaseJpaRestfulServer.java @@ -311,7 +311,7 @@ public class BaseJpaRestfulServer extends RestfulServer { BundleType type = BundleType.valueOf(o); allowedBundleTypes.add(type.toCode()); }); - DaoConfig config = (DaoConfig) daoConfig; + DaoConfig config = daoConfig; config.setBundleTypesAllowedForStorage( Collections.unmodifiableSet(new TreeSet<>(allowedBundleTypes))); } @@ -328,6 +328,10 @@ public class BaseJpaRestfulServer extends RestfulServer { 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 6639ef5..544e452 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; @@ -74,6 +75,7 @@ public class HapiProperties { static final String EXPIRE_SEARCH_RESULTS_AFTER_MINS = "retain_cached_searches_mins"; static final String MAX_BINARY_SIZE = "max_binary_size"; static final String PARTITIONING_MULTITENANCY_ENABLED = "partitioning.multitenancy.enabled"; + static final String CLIENT_ID_STRATEGY = "daoconfig.client_id_strategy"; private static Properties ourProperties; @@ -240,6 +242,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); diff --git a/src/main/resources/hapi.properties b/src/main/resources/hapi.properties index dc656b9..303553f 100644 --- a/src/main/resources/hapi.properties +++ b/src/main/resources/hapi.properties @@ -154,3 +154,4 @@ subscription.websocket.enabled=false # Partitioning And Multitenancy ################################################### partitioning.multitenancy.enabled=false +#daoconfig.client_id_strategy=ANY \ No newline at end of file From 6fa569f030b25fceaf6a8ebbab7f589d9f07c938 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2020 20:58:36 +0000 Subject: [PATCH 08/10] Bump mysql-connector-java from 8.0.11 to 8.0.16 Bumps [mysql-connector-java](https://github.com/mysql/mysql-connector-j) from 8.0.11 to 8.0.16. - [Release notes](https://github.com/mysql/mysql-connector-j/releases) - [Changelog](https://github.com/mysql/mysql-connector-j/blob/release/8.0/CHANGES) - [Commits](https://github.com/mysql/mysql-connector-j/compare/8.0.11...8.0.16) Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c37158b..dca8630 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ mysql mysql-connector-java - 8.0.11 + 8.0.16 From 2e8c6a7082ee4a830bccfb37aa18520486b96c86 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Thu, 9 Jul 2020 11:23:25 -0700 Subject: [PATCH 09/10] Update property --- src/main/resources/hapi.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/hapi.properties b/src/main/resources/hapi.properties index 303553f..2d199d1 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) From a70fe5bfa0c7ce7f75d2f855fdd1ec4f316d713e Mon Sep 17 00:00:00 2001 From: Sean McIlvenna Date: Thu, 13 Aug 2020 08:26:53 -0700 Subject: [PATCH 10/10] Update README to include Docker info --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/README.md b/README.md index 877e579..a46218c 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: