From 5c69cba0064985c6260e39da1ae174dbfdf9a88d Mon Sep 17 00:00:00 2001 From: Quentin Ligier Date: Mon, 11 Nov 2024 10:56:49 +0100 Subject: [PATCH] Introduce parameter 'autoInstallMissingIgs' to automatically install IGs from the public registry Fixes #306 --- docs/changelog.md | 5 +++ docs/validation.md | 26 +++++++++------- .../ImplementationGuideProviderR4.java | 31 ++++++++++++++++--- .../ImplementationGuideProviderR4B.java | 21 +++++++++++++ .../ImplementationGuideProviderR5.java | 21 +++++++++++++ .../MatchboxImplementationGuideProvider.java | 10 ++++++ .../jpa/validation/ValidationProvider.java | 13 +++++++- .../java/ch/ahdis/matchbox/CliContext.java | 12 +++++++ .../ahdis/matchbox/MatchboxEngineSupport.java | 9 +++--- 9 files changed, 126 insertions(+), 22 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index cfb8bd353b9..1af17f02131 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,3 +1,8 @@ +Unreleased + +- Introduce parameter 'autoInstallMissingIgs' to automatically install IGs from the public registry + [#306](https://github.com/ahdis/matchbox/issues/306) + 2024/10/24 Release 3.9.6 - Remove lucene dependencies [#301](https://github.com/ahdis/matchbox/issues/301) diff --git a/docs/validation.md b/docs/validation.md index e523501d247..0858d36d024 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -66,23 +66,25 @@ matchbox: # onlyOneEngine: true # xVersion : false # igsPreloaded: ch.fhir.ig.ch-core#4.0.0-ballot + # autoInstallMissingIgs: true suppressWarnInfo: hl7.fhir.r4.core#4.0.1: #- "Constraint failed: dom-6:" ``` -| Parameter | Card | Description | -|----------------------|-------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| implementationguides | 0..\* | the Implementation Guide and version which with which matchbox will be configured, you can provide by classpath, file, http address, if none is specified the FHIR package servers will be used (need to be online) | -| txServer | 0..1 | txServer to use, n/a if none (default), http://localhost:8080/fhir for internal (if server.port in application.yaml is 8080), http://tx.fhir.org for hl7 validator | -| txUseEcosystem | 0..1 | boolean true if none (default), if true asks tx servers for which CodeSystem they are authorative | -| txServerCache | 0..1 | boolean if respones are cached, true if none (default) | -| txLog | 0..1 | string indicating file location of log (end either in .txt or .html, default no logging | -| igsPreloaded | 0..\* | For each mentioned ImplementationGuide (comma separated) an engine will be created, which will be cached in memory as long the application is running. Other IG's will created on demand and will be cached for an hour for subsequent calls. Tradeoff between memory consumption and first response time (creating of engine might have duration of half a minute). Default no igs are preloaded. | -| onlyOneEngine | 0..1 | Implementation Guides can have multiple versions with different dependencies. Matchbox creates for transformation and validation an own engine for each Implementation Guide and its dependencies (default setting). You can switch this behavior, e.g. if you are using it in development and want to create and update resources or transform maps. Set the setting for onlyOneEngine to true. The changes are however not persisted and will be lost if matchbox is restarted. | -| httpReadOnly | 0..1 | Whether to allow creating, modifying or deleting resources on the server via the HTTP API or not. If `true`, IGs can only be loaded through the configuration. | -| suppressWarnInfo | 0..\* | A list of warning message to ignore while validating resources, per Implementation Guide and version. | -| extensions | 0..1 | Extensions not defined by the ImplementationgGuides which are accepted, comma separted list by url patterns, defaults to 'any' | +| Parameter | Card | Description | +|-----------------------|-------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| implementationguides | 0..\* | the Implementation Guide and version which with which matchbox will be configured, you can provide by classpath, file, http address, if none is specified the FHIR package servers will be used (need to be online) | +| txServer | 0..1 | txServer to use, n/a if none (default), http://localhost:8080/fhir for internal (if server.port in application.yaml is 8080), http://tx.fhir.org for hl7 validator | +| txUseEcosystem | 0..1 | boolean true if none (default), if true asks tx servers for which CodeSystem they are authorative | +| txServerCache | 0..1 | boolean if respones are cached, true if none (default) | +| txLog | 0..1 | string indicating file location of log (end either in .txt or .html, default no logging | +| igsPreloaded | 0..\* | For each mentioned ImplementationGuide (comma separated) an engine will be created, which will be cached in memory as long the application is running. Other IG's will created on demand and will be cached for an hour for subsequent calls. Tradeoff between memory consumption and first response time (creating of engine might have duration of half a minute). Default no igs are preloaded. | +| onlyOneEngine | 0..1 | Implementation Guides can have multiple versions with different dependencies. Matchbox creates for transformation and validation an own engine for each Implementation Guide and its dependencies (default setting). You can switch this behavior, e.g. if you are using it in development and want to create and update resources or transform maps. Set the setting for onlyOneEngine to true. The changes are however not persisted and will be lost if matchbox is restarted. | +| httpReadOnly | 0..1 | Whether to allow creating, modifying or deleting resources on the server via the HTTP API or not. If `true`, IGs can only be loaded through the configuration. | +| suppressWarnInfo | 0..\* | A list of warning message to ignore while validating resources, per Implementation Guide and version. | +| extensions | 0..1 | Extensions not defined by the ImplementationgGuides which are accepted, comma separted list by url patterns, defaults to 'any' | +| autoInstallMissingIgs | 0..1 | Whether to automatically install IGs from the public registry if they are not installed. Default to `false`. | #### Suppress warning/information-level issues in validation diff --git a/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ImplementationGuideProviderR4.java b/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ImplementationGuideProviderR4.java index 396e8b442f9..41773ce7a84 100644 --- a/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ImplementationGuideProviderR4.java +++ b/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ImplementationGuideProviderR4.java @@ -176,8 +176,10 @@ public PackageInstallationSpec getPackageInstallationSpec() { public PackageInstallOutcomeJson load(ImplementationGuide theResource, PackageInstallOutcomeJson install) { PackageInstallOutcomeJson installOutcome = packageInstallerSvc - .install(this.getPackageInstallationSpec().setName(theResource.getName()) - .setPackageUrl(theResource.getUrl()).setVersion(theResource.getVersion())); + .install(this.getPackageInstallationSpec() + .setName(theResource.getName()) + .setPackageUrl(theResource.getUrl()) + .setVersion(theResource.getVersion())); if (install != null) { install.getMessage().addAll(installOutcome.getMessage()); return install; @@ -188,10 +190,8 @@ public PackageInstallOutcomeJson load(ImplementationGuide theResource, PackageIn public OperationOutcome load(ImplementationGuide theResource) { PackageInstallOutcomeJson installOutcome = packageInstallerSvc.install(this.getPackageInstallationSpec() .setPackageUrl(theResource.getUrl()) - .addInstallResourceTypes(MatchboxPackageInstallerImpl.DEFAULT_INSTALL_TYPES.toArray(new String[0])) .setName(theResource.getName()) - .setVersion(theResource.getVersion()) - .setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_ONLY)); + .setVersion(theResource.getVersion())); if (cliContext!=null && cliContext.getOnlyOneEngine()) { MatchboxEngine engine = matchboxEngineSupport.getMatchboxEngine(FHIRVersion._4_0_1.getDisplay(), this.cliContext, false, false); @@ -511,4 +511,25 @@ public ImplementationGuide read(HttpServletRequest theServletRequest, IIdType th } } + /** + * Returns whether the given ImplementationGuide is installed or not. + */ + @Override + public boolean has(final String packageId, final String packageVersion) { + return Boolean.TRUE.equals(new TransactionTemplate(this.myTxManager) + .execute(tx -> this.myPackageVersionDao.findByPackageIdAndVersion(packageId, packageVersion).isPresent())); + } + + /** + * Installs the given ImplementationGuide from the internet registry. + */ + @Override + public void installFromInternetRegistry(final String packageId, final String packageVersion) { + this.packageInstallerSvc.install( + this.getPackageInstallationSpec() + .setName(packageId) + .setPackageUrl("https://packages2.fhir.org/packages/%s/%s".formatted(packageId, packageVersion)) + .setVersion(packageVersion) + ); + } } diff --git a/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ImplementationGuideProviderR4B.java b/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ImplementationGuideProviderR4B.java index 7087354bb31..cea16873af7 100644 --- a/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ImplementationGuideProviderR4B.java +++ b/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ImplementationGuideProviderR4B.java @@ -515,4 +515,25 @@ public ImplementationGuide read(HttpServletRequest theServletRequest, IIdType th } } + /** + * Returns whether the given ImplementationGuide is installed or not. + */ + @Override + public boolean has(final String packageId, final String packageVersion) { + return Boolean.TRUE.equals(new TransactionTemplate(this.myTxManager) + .execute(tx -> this.myPackageVersionDao.findByPackageIdAndVersion(packageId, packageVersion).isPresent())); + } + + /** + * Installs the given ImplementationGuide from the internet registry. + */ + @Override + public void installFromInternetRegistry(final String packageId, final String packageVersion) { + this.packageInstallerSvc.install( + this.getPackageInstallationSpec() + .setName(packageId) + .setPackageUrl("https://packages2.fhir.org/packages/%s/%s".formatted(packageId, packageVersion)) + .setVersion(packageVersion) + ); + } } diff --git a/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ImplementationGuideProviderR5.java b/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ImplementationGuideProviderR5.java index 1953dbf5ca6..ac695eeef8f 100644 --- a/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ImplementationGuideProviderR5.java +++ b/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ImplementationGuideProviderR5.java @@ -495,4 +495,25 @@ public ImplementationGuide read(HttpServletRequest theServletRequest, IIdType th } } + /** + * Returns whether the given ImplementationGuide is installed or not. + */ + @Override + public boolean has(final String packageId, final String packageVersion) { + return Boolean.TRUE.equals(new TransactionTemplate(this.myTxManager) + .execute(tx -> this.myPackageVersionDao.findByPackageIdAndVersion(packageId, packageVersion).isPresent())); + } + + /** + * Installs the given ImplementationGuide from the internet registry. + */ + @Override + public void installFromInternetRegistry(final String packageId, final String packageVersion) { + this.packageInstallerSvc.install( + this.getPackageInstallationSpec() + .setName(packageId) + .setPackageUrl("https://packages2.fhir.org/packages/%s/%s".formatted(packageId, packageVersion)) + .setVersion(packageVersion) + ); + } } diff --git a/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/MatchboxImplementationGuideProvider.java b/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/MatchboxImplementationGuideProvider.java index 175b964fd97..f03ce8accb8 100644 --- a/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/MatchboxImplementationGuideProvider.java +++ b/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/MatchboxImplementationGuideProvider.java @@ -10,4 +10,14 @@ public interface MatchboxImplementationGuideProvider { PackageInstallOutcomeJson loadAll(boolean replace); + + /** + * Returns whether the given ImplementationGuide is installed or not. + */ + boolean has(final String packageId, final String packageVersion); + + /** + * Installs the given ImplementationGuide from the internet registry. + */ + void installFromInternetRegistry(final String packageId, final String packageVersion); } diff --git a/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ValidationProvider.java b/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ValidationProvider.java index 817cb3ccb3b..5205d0916bc 100644 --- a/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ValidationProvider.java +++ b/matchbox-server/src/main/java/ch/ahdis/fhir/hapi/jpa/validation/ValidationProvider.java @@ -38,7 +38,6 @@ import org.hl7.fhir.convertors.factory.VersionConvertorFactory_43_50; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; import org.hl7.fhir.r5.model.Duration; import org.hl7.fhir.r5.model.OperationOutcome; @@ -78,6 +77,9 @@ public class ValidationProvider { @Autowired private FhirContext myContext; + @Autowired + private MatchboxImplementationGuideProvider igProvider; + // @Operation(name = "$canonical", manualRequest = true, idempotent = true, returnParameters = { // @OperationParam(name = "return", type = IBase.class, min = 1, max = 1) }) // public IBaseResource canonical(HttpServletRequest theRequest) { @@ -132,6 +134,15 @@ public IBaseResource validate(final HttpServletRequest theRequest) { } } + // Check if the IG should be auto-installed + if (cliContext.isAutoInstallMissingIgs() && theRequest.getParameterMap().containsKey("ig")) { + final var parts = theRequest.getParameter("ig").split("#"); + if (!this.igProvider.has(parts[0], parts[1])) { + log.debug("Auto-installing the IG '{}' version '{}'", parts[0], parts[1]); + this.igProvider.installFromInternetRegistry(parts[0], parts[1]); + } + } + if (theRequest.getParameter("extensions") != null) { String extensions = theRequest.getParameter("extensions"); cliContext.setExtensions(new ArrayList(Arrays.asList(extensions.split(",")))); diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/CliContext.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/CliContext.java index 844637551b7..5d9ffb1c422 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/CliContext.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/CliContext.java @@ -193,6 +193,12 @@ public boolean isHttpReadOnly() { return this.httpReadOnly; } + private boolean autoInstallMissingIgs = false; + + public boolean isAutoInstallMissingIgs() { + return this.autoInstallMissingIgs; + } + private boolean xVersion = false; public boolean getXVersion() { @@ -225,6 +231,7 @@ public CliContext(Environment environment) { this.igsPreloaded = environment.getProperty("matchbox.fhir.context.igsPreloaded", String[].class); this.onlyOneEngine = environment.getProperty("matchbox.fhir.context.onlyOneEngine", Boolean.class, false); this.httpReadOnly = environment.getProperty("matchbox.fhir.context.httpReadOnly", Boolean.class, false); + this.autoInstallMissingIgs = environment.getProperty("matchbox.fhir.context.autoInstallMissingIgs", Boolean.class, false); this.extensions = Arrays.asList(environment.getProperty("matchbox.fhir.context.extensions", String[].class, new String[]{"any"})); this.xVersion = environment.getProperty("matchbox.fhir.context.xVersion", Boolean.class, false); } @@ -245,6 +252,7 @@ public CliContext(CliContext other) { this.igsPreloaded = other.igsPreloaded; this.onlyOneEngine = other.onlyOneEngine; this.httpReadOnly = other.httpReadOnly; + this.autoInstallMissingIgs = other.autoInstallMissingIgs; this.extensions = other.extensions; this.xVersion = other.xVersion; } @@ -675,6 +683,7 @@ public boolean equals(final Object o) { && onlyOneEngine == that.onlyOneEngine && xVersion == that.xVersion && httpReadOnly == that.httpReadOnly + && autoInstallMissingIgs == that.autoInstallMissingIgs && htmlInMarkdownCheck == that.htmlInMarkdownCheck && Objects.equals(extensions, that.extensions) && Objects.equals(txServer, that.txServer) @@ -717,6 +726,7 @@ public int hashCode() { showTerminologyRouting, clearTxCache, httpReadOnly, + autoInstallMissingIgs, allowExampleUrls, htmlInMarkdownCheck, txServer, @@ -782,6 +792,7 @@ public String toString() { ", onlyOneEngine=" + onlyOneEngine + ", xVersion=" + xVersion + ", httpReadOnly=" + httpReadOnly + + ", autoInstallMissingIgs=" + autoInstallMissingIgs + '}'; } @@ -817,6 +828,7 @@ public void addContextToExtension(final Extension ext) { addExtension(ext, "showTerminologyRouting", new BooleanType(this.showTerminologyRouting)); addExtension(ext, "clearTxCache", new BooleanType(this.clearTxCache)); addExtension(ext, "httpReadOnly", new BooleanType(this.httpReadOnly)); + addExtension(ext, "autoInstallMissingIgs", new BooleanType(this.autoInstallMissingIgs)); addExtension(ext, "allowExampleUrls", new BooleanType(this.allowExampleUrls)); addExtension(ext, "txServer", new UriType(this.txServer)); addExtension(ext, "txServerCache", new BooleanType(this.txServerCache)); diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/MatchboxEngineSupport.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/MatchboxEngineSupport.java index 7d01e5a090b..b148ee0bc0e 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/MatchboxEngineSupport.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/MatchboxEngineSupport.java @@ -1,9 +1,6 @@ package ch.ahdis.matchbox; -import static org.apache.commons.lang3.StringUtils.defaultString; - import java.io.File; -import java.security.MessageDigest; import java.util.*; import ch.ahdis.matchbox.config.MatchboxFhirContextProperties; @@ -432,7 +429,7 @@ public MatchboxEngine getMatchboxEngineNotSynchronized(final @Nullable String ca } } final var created = this.createMatchboxEngine(baseEngine, cliContext.getIg(), cliContext); - sessionCache.cacheSession("" + cliContext.hashCode(), created); + this.sessionCache.cacheSession("" + cliContext.hashCode(), created); return created; } return null; @@ -509,6 +506,10 @@ private void configureValidationEngine(final MatchboxEngine validator, } + if (cli.isHttpReadOnly() && cli.isAutoInstallMissingIgs()) { + throw new MatchboxEngineCreationException("httpReadOnly and autoInstallMissingIgs are mutually exclusive"); + } + validator.setDebug(cli.isDoDebug()); validator.getContext().setLogger(new EngineLoggingService());