diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 405c48a6001..7e522cf5e43 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -20,10 +20,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: java-version: 17 + distribution: zulu - name: Build with Maven run: mvn -B package --file pom.xml verify diff --git a/.github/workflows/smoke-tests.yml b/.github/workflows/smoke-tests.yml index 6fa35600ba5..31568a35ef2 100644 --- a/.github/workflows/smoke-tests.yml +++ b/.github/workflows/smoke-tests.yml @@ -21,11 +21,12 @@ jobs: steps: - name: Checkout project - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v1 + uses: actions/setup-java@v3 with: java-version: 17 + distribution: zulu - name: Build with Maven run: mvn -B package --file pom.xml -Dmaven.test.skip=true - name: Docker Pull HTTP client 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 019b3201fe8..b7fccac5348 100644 --- a/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java +++ b/src/main/java/ca/uhn/fhir/jpa/starter/AppProperties.java @@ -76,6 +76,18 @@ public class AppProperties { private Boolean reload_existing_implementationguides = false; private Map implementationGuides = null; private Boolean cr_enabled = false; + + private Boolean ig_runtime_upload_enabled = false; + + private Validation validation = new Validation(); + private Map tester = null; + private Logger logger = new Logger(); + private Subscription subscription = new Subscription(); + private Cors cors = null; + private Partitioning partitioning = null; + private Boolean install_transitive_ig_dependencies = true; + private Map implementationGuides = null; + private String staticLocation = null; private String staticLocationPrefix = "/static"; private Boolean lastn_enabled = false; @@ -579,6 +591,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 766870afdf6..c71a66600e4 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 a47434d0f94..49d12c87454 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -56,7 +56,7 @@ hapi: ### 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: true + enabled: false cdshooks: enabled: true @@ -66,6 +66,9 @@ 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. ### enable to use the ApacheProxyAddressStrategy which uses X-Forwarded-* headers ### to determine the FHIR server address