From 1f7d25c5e57b1901fb8290151dc41a3d238933bf Mon Sep 17 00:00:00 2001 From: Jens Kristian Villadsen Date: Thu, 23 Nov 2023 22:25:32 +0100 Subject: [PATCH] Feat/ig install (#601) * Added IG operation providers for run time installation of IG's * Refactored conditions for enabling the provider * Refactoring * Disable it by default in config as well * document package install feature --------- Co-authored-by: Jose Costa Teixeira --- README.md | 26 ++++++++++++++ .../uhn/fhir/jpa/starter/AppProperties.java | 10 ++++++ .../jpa/starter/common/StarterJpaConfig.java | 5 ++- ...IImplementationGuideOperationProvider.java | 18 ++++++++++ .../jpa/starter/ig/IgConfigCondition.java | 14 ++++++++ ...mplementationGuideR4OperationProvider.java | 35 ++++++++++++++++++ ...mplementationGuideR5OperationProvider.java | 36 +++++++++++++++++++ src/main/resources/application.yaml | 2 ++ 8 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ca/uhn/fhir/jpa/starter/ig/IImplementationGuideOperationProvider.java create mode 100644 src/main/java/ca/uhn/fhir/jpa/starter/ig/IgConfigCondition.java create mode 100644 src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR4OperationProvider.java create mode 100644 src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR5OperationProvider.java diff --git a/README.md b/README.md index 0184cc5b271..75f18c2ba4c 100644 --- a/README.md +++ b/README.md @@ -515,6 +515,32 @@ To add a custom operation, refer to the documentation in the core hapi-fhir libr Within `hapi-fhir-jpaserver-starter`, create a generic class (that does not extend or implement any classes or interfaces), add the `@Operation` as a method within the generic class, and then register the class as a provider using `RestfulServer.registerProvider()`. +## Runtime package install + +It's possible to install a FHIR Implementation Guide package (`package.tgz`) either from a published package or from a local package with the `$install` operation, without having to restart the server. This is available for R4 and R5. + +This feature must be enabled in the application.yaml (or docker command line): + +```yaml +hapi: + fhir: + ig_runtime_upload_enabled: true +``` + +The `$install` operation is triggered with a POST to `[server]/ImplementationGuide/$install`, with the payload below: + +```json +{ + "resourceType": "Parameters", + "parameter": [ + { + "name": "npmContent", + "valueBase64Binary": "[BASE64_ENCODED_NPM_PACKAGE_DATA]" + } + ] +} +``` + ## Enable OpenTelemetry auto-instrumentation The container image includes the [OpenTelemetry Java auto-instrumentation](https://github.com/open-telemetry/opentelemetry-java-instrumentation) diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java b/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java index 8528e25a5ee..6c93d150770 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java @@ -67,6 +67,8 @@ public class AppProperties { private List allowed_bundle_types = null; private Boolean narrative_enabled = true; + private Boolean ig_runtime_upload_enabled = false; + private Validation validation = new Validation(); private Map tester = null; private Logger logger = new Logger(); @@ -584,6 +586,14 @@ public Set getLocal_base_urls() { return local_base_urls; } + public Boolean getIg_runtime_upload_enabled() { + return ig_runtime_upload_enabled; + } + + public void setIg_runtime_upload_enabled(Boolean ig_runtime_upload_enabled) { + this.ig_runtime_upload_enabled = ig_runtime_upload_enabled; + } + public static class Cors { private Boolean allow_Credentials = true; private List allowed_origin = List.of("*"); diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java b/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java index 7276777b13d..30c26fe8990 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/common/StarterJpaConfig.java @@ -44,6 +44,7 @@ import ca.uhn.fhir.jpa.starter.annotations.OnImplementationGuidesPresent; import ca.uhn.fhir.jpa.starter.common.validation.IRepositoryValidationInterceptorFactory; import ca.uhn.fhir.jpa.starter.util.EnvironmentHelper; +import ca.uhn.fhir.jpa.starter.ig.IImplementationGuideOperationProvider; import ca.uhn.fhir.jpa.subscription.util.SubscriptionDebugLogInterceptor; import ca.uhn.fhir.jpa.util.ResourceCountCache; import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain; @@ -241,7 +242,7 @@ public CorsInterceptor corsInterceptor(AppProperties appProperties) { } @Bean - public RestfulServer restfulServer(IFhirSystemDao fhirSystemDao, AppProperties appProperties, DaoRegistry daoRegistry, Optional mdmProviderProvider, IJpaSystemProvider jpaSystemProvider, ResourceProviderFactory resourceProviderFactory, JpaStorageSettings jpaStorageSettings, ISearchParamRegistry searchParamRegistry, IValidationSupport theValidationSupport, DatabaseBackedPagingProvider databaseBackedPagingProvider, LoggingInterceptor loggingInterceptor, Optional terminologyUploaderProvider, Optional subscriptionTriggeringProvider, Optional corsInterceptor, IInterceptorBroadcaster interceptorBroadcaster, Optional binaryAccessProvider, BinaryStorageInterceptor binaryStorageInterceptor, IValidatorModule validatorModule, Optional graphQLProvider, BulkDataExportProvider bulkDataExportProvider, BulkDataImportProvider bulkDataImportProvider, ValueSetOperationProvider theValueSetOperationProvider, ReindexProvider reindexProvider, PartitionManagementProvider partitionManagementProvider, Optional repositoryValidatingInterceptor, IPackageInstallerSvc packageInstallerSvc, ThreadSafeResourceDeleterSvc theThreadSafeResourceDeleterSvc, ApplicationContext appContext, Optional theIpsOperationProvider) { + public RestfulServer restfulServer(IFhirSystemDao fhirSystemDao, AppProperties appProperties, DaoRegistry daoRegistry, Optional mdmProviderProvider, IJpaSystemProvider jpaSystemProvider, ResourceProviderFactory resourceProviderFactory, JpaStorageSettings jpaStorageSettings, ISearchParamRegistry searchParamRegistry, IValidationSupport theValidationSupport, DatabaseBackedPagingProvider databaseBackedPagingProvider, LoggingInterceptor loggingInterceptor, Optional terminologyUploaderProvider, Optional subscriptionTriggeringProvider, Optional corsInterceptor, IInterceptorBroadcaster interceptorBroadcaster, Optional binaryAccessProvider, BinaryStorageInterceptor binaryStorageInterceptor, IValidatorModule validatorModule, Optional graphQLProvider, BulkDataExportProvider bulkDataExportProvider, BulkDataImportProvider bulkDataImportProvider, ValueSetOperationProvider theValueSetOperationProvider, ReindexProvider reindexProvider, PartitionManagementProvider partitionManagementProvider, Optional repositoryValidatingInterceptor, IPackageInstallerSvc packageInstallerSvc, ThreadSafeResourceDeleterSvc theThreadSafeResourceDeleterSvc, ApplicationContext appContext, Optional theIpsOperationProvider, Optional implementationGuideOperationProvider) { RestfulServer fhirServer = new RestfulServer(fhirSystemDao.getContext()); List supportedResourceTypes = appProperties.getSupported_resource_types(); @@ -304,6 +305,8 @@ public RestfulServer restfulServer(IFhirSystemDao fhirSystemDao, AppProper fhirServer.registerInterceptor(loggingInterceptor); + implementationGuideOperationProvider.ifPresent(fhirServer::registerProvider); + /* * 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 diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/ig/IImplementationGuideOperationProvider.java b/src/main/java/ca/uhn/fhir/jpa/starter/ig/IImplementationGuideOperationProvider.java new file mode 100644 index 00000000000..617f7620f56 --- /dev/null +++ b/src/main/java/ca/uhn/fhir/jpa/starter/ig/IImplementationGuideOperationProvider.java @@ -0,0 +1,18 @@ +package ca.uhn.fhir.jpa.starter.ig; + +import ca.uhn.fhir.jpa.packages.PackageInstallationSpec; +import org.hl7.fhir.utilities.npm.NpmPackage; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public interface IImplementationGuideOperationProvider { + static PackageInstallationSpec toPackageInstallationSpec(byte[] npmPackageAsByteArray) throws IOException { + NpmPackage npmPackage = NpmPackage.fromPackage(new ByteArrayInputStream(npmPackageAsByteArray)); + return new PackageInstallationSpec().setName(npmPackage.name()).setPackageContents(npmPackageAsByteArray).setVersion(npmPackage.version()).setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL).setFetchDependencies(false); + } + + //The following declaration is the one that counts but cannot be used across different versions as stating Base64BinaryType would bind to a separate version + //@Operation(name = "$install", typeName = "ImplementationGuide") + //Parameters install(@OperationParam(name = "npmContent",min = 1, max = 1) Base64BinaryType implementationGuide); +} diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/ig/IgConfigCondition.java b/src/main/java/ca/uhn/fhir/jpa/starter/ig/IgConfigCondition.java new file mode 100644 index 00000000000..a93736b0cd2 --- /dev/null +++ b/src/main/java/ca/uhn/fhir/jpa/starter/ig/IgConfigCondition.java @@ -0,0 +1,14 @@ +package ca.uhn.fhir.jpa.starter.ig; + +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +public class IgConfigCondition implements Condition { + + @Override + public boolean matches(ConditionContext theConditionContext, AnnotatedTypeMetadata theAnnotatedTypeMetadata) { + String property = theConditionContext.getEnvironment().getProperty("hapi.fhir.ig_runtime_upload_enabled"); + return Boolean.parseBoolean(property); + } +} diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR4OperationProvider.java b/src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR4OperationProvider.java new file mode 100644 index 00000000000..75dc4de80be --- /dev/null +++ b/src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR4OperationProvider.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.jpa.starter.ig; + +import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc; +import ca.uhn.fhir.jpa.starter.annotations.OnR4Condition; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import org.hl7.fhir.r4.model.Base64BinaryType; +import org.hl7.fhir.r4.model.Parameters; +import org.springframework.context.annotation.Conditional; +import org.springframework.stereotype.Service; + +import java.io.IOException; + +@Conditional({OnR4Condition.class, IgConfigCondition.class}) +@Service +public class ImplementationGuideR4OperationProvider implements IImplementationGuideOperationProvider { + + IPackageInstallerSvc packageInstallerSvc; + + public ImplementationGuideR4OperationProvider(IPackageInstallerSvc packageInstallerSvc) { + this.packageInstallerSvc = packageInstallerSvc; + } + + @Operation(name = "$install", typeName = "ImplementationGuide") + public Parameters install(@OperationParam(name = "npmContent", min = 1, max = 1) Base64BinaryType implementationGuide) { + try { + + packageInstallerSvc.install(IImplementationGuideOperationProvider.toPackageInstallationSpec(implementationGuide.getValue())); + } catch (IOException e) { + throw new RuntimeException(e); + } + return new Parameters(); + } + +} diff --git a/src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR5OperationProvider.java b/src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR5OperationProvider.java new file mode 100644 index 00000000000..233789dfb16 --- /dev/null +++ b/src/main/java/ca/uhn/fhir/jpa/starter/ig/ImplementationGuideR5OperationProvider.java @@ -0,0 +1,36 @@ +package ca.uhn.fhir.jpa.starter.ig; + +import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc; +import ca.uhn.fhir.jpa.starter.annotations.OnR5Condition; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import org.hl7.fhir.r5.model.Base64BinaryType; +import org.hl7.fhir.r5.model.Parameters; +import org.springframework.context.annotation.Conditional; +import org.springframework.stereotype.Service; + +import java.io.IOException; + +@Conditional({OnR5Condition.class, IgConfigCondition.class}) +@Service +public class ImplementationGuideR5OperationProvider { + + IPackageInstallerSvc packageInstallerSvc; + + public ImplementationGuideR5OperationProvider(IPackageInstallerSvc packageInstallerSvc) { + this.packageInstallerSvc = packageInstallerSvc; + } + + @Operation(name = "$install", typeName = "ImplementationGuide") + public Parameters install(@OperationParam(name = "npmContent", min = 1, max = 1) Base64BinaryType implementationGuide) { + try { + + packageInstallerSvc.install(IImplementationGuideOperationProvider.toPackageInstallationSpec(implementationGuide.getValue())); + } catch (IOException e) { + throw new RuntimeException(e); + } + return new Parameters(); + } + + +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 25b8989467f..4a994dc8e1e 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -58,6 +58,8 @@ hapi: openapi_enabled: true ### This is the FHIR version. Choose between, DSTU2, DSTU3, R4 or R5 fhir_version: R4 + ### Flag is false by default. This flag enables runtime installation of IG's. + ig_runtime_upload_enabled: false ### This flag when enabled to true, will avail evaluate measure operations from CR Module. ### Flag is false by default, can be passed as command line argument to override. cr_enabled: "${CR_ENABLED: false}"