From 5fde9548e973a0efc067b05deffe2ef6a355dd17 Mon Sep 17 00:00:00 2001 From: Quentin Ligier Date: Thu, 17 Apr 2025 19:18:27 +0200 Subject: [PATCH 1/2] Save installed StructureDefinitions in database --- .../ca/uhn/fhir/jpa/config/JpaConfig.java | 6 +- ...nstalledStructureDefinitionRepository.java | 14 ++ .../MbInstalledStructureDefinitionEntity.java | 197 ++++++++++++++++++ .../fhir/jpa/packages/JpaPackageCache.java | 5 +- .../ca/uhn/fhir/jpa/starter/Application.java | 1 + .../matchbox/config/MatchboxJpaConfig.java | 17 +- .../packages/MatchboxJpaPackageCache.java | 132 ++++++------ .../ConformancePackageResourceProvider.java | 67 ------ .../MatchboxCapabilityStatementProvider.java | 41 ++-- .../validation/ValidationProvider.java | 1 - .../gazelle/GazelleValidationWs.java | 38 ++-- 11 files changed, 342 insertions(+), 177 deletions(-) create mode 100644 matchbox-server/src/main/java/ca/uhn/fhir/jpa/dao/data/MbInstalledStructureDefinitionRepository.java create mode 100644 matchbox-server/src/main/java/ca/uhn/fhir/jpa/model/entity/MbInstalledStructureDefinitionEntity.java diff --git a/matchbox-server/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/matchbox-server/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index ae5a5acfaa..edd3453330 100644 --- a/matchbox-server/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/matchbox-server/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -192,11 +192,13 @@ import ca.uhn.fhir.util.IMetaTagSorter; import ca.uhn.fhir.util.MetaTagSorterAlphabetical; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; +import ca.uhn.fhir.jpa.model.entity.MbInstalledStructureDefinitionEntity; +import ca.uhn.fhir.jpa.dao.data.MbInstalledStructureDefinitionRepository; import jakarta.annotation.Nullable; import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport; -import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -216,7 +218,7 @@ @Configuration // repositoryFactoryBeanClass: EnversRevisionRepositoryFactoryBean is needed primarily for unit testing @EnableJpaRepositories( - basePackages = "ca.uhn.fhir.jpa.dao.data", + basePackages = {"ca.uhn.fhir.jpa.dao.data",}, repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class) @Import({ BeanPostProcessorConfig.class, diff --git a/matchbox-server/src/main/java/ca/uhn/fhir/jpa/dao/data/MbInstalledStructureDefinitionRepository.java b/matchbox-server/src/main/java/ca/uhn/fhir/jpa/dao/data/MbInstalledStructureDefinitionRepository.java new file mode 100644 index 0000000000..5d1819b971 --- /dev/null +++ b/matchbox-server/src/main/java/ca/uhn/fhir/jpa/dao/data/MbInstalledStructureDefinitionRepository.java @@ -0,0 +1,14 @@ +package ca.uhn.fhir.jpa.dao.data; + +import ca.uhn.fhir.jpa.model.entity.MbInstalledStructureDefinitionEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface MbInstalledStructureDefinitionRepository + extends JpaRepository { + + @Query("SELECT e FROM MbInstalledStructureDefinitionEntity e WHERE e.isValidatable = TRUE") + List findAllValidatable(); +} diff --git a/matchbox-server/src/main/java/ca/uhn/fhir/jpa/model/entity/MbInstalledStructureDefinitionEntity.java b/matchbox-server/src/main/java/ca/uhn/fhir/jpa/model/entity/MbInstalledStructureDefinitionEntity.java new file mode 100644 index 0000000000..f045623ed9 --- /dev/null +++ b/matchbox-server/src/main/java/ca/uhn/fhir/jpa/model/entity/MbInstalledStructureDefinitionEntity.java @@ -0,0 +1,197 @@ +package ca.uhn.fhir.jpa.model.entity; + +import jakarta.persistence.*; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import java.io.Serializable; +import java.util.Objects; + +/** + * This table contains the list of StructureDefinitions currently installed in Matchbox. + */ +@Entity() +@Table( + name = "MB_INSTALLED_STRUCT_DEF", + indexes = { + @Index(name = "IDX_IS_VALIDATABLE", columnList = "IS_VALIDATABLE"), + }) +public class MbInstalledStructureDefinitionEntity implements Serializable { + + /** + * A primary key for the table. + */ + @Id + @SequenceGenerator(name = "SEQ_MB_INSTSTRUCTDEF", sequenceName = "SEQ_MB_INSTSTRUCTDEF") + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_MB_INSTSTRUCTDEF") + @Column(name = "PID") + private Long id; + + /** + * StructureDefinition.url + */ + @Column(name = "CANONICAL_URL", length = 200, nullable = false) + private String canonicalUrl; + + /** + * StructureDefinition.title or StructureDefinition.name + */ + @Column(name = "TITLE", length = 200, nullable = false) + private String title; + + /** + * ImplementationGuide.packageId + */ + @Column(name = "PACKAGE_ID", length = 200, nullable = false) + private String packageId; + + /** + * ImplementationGuide.version + */ + @Column(name = "PACKAGE_VERSION", length = 200, nullable = false) + private String packageVersion; + + /** + * StructureDefinition.type + */ + @Column(name = "TYPE", length = 100, nullable = false) + private String type; + + /** + * StructureDefinition.kind: primitive-type | complex-type | resource | logical + */ + @Column(name = "KIND", length = 20, nullable = false) + private String kind; + + /** + * Whether the package version is the current one (i.e. the most recent one) or not. + */ + @Column(name = "IS_CURRENT", nullable = false) + private Boolean isCurrent; + + /** + * Whether that StructureDefinition can be used for validation or not. + */ + @Column(name = "IS_VALIDATABLE", nullable = false) + private Boolean isValidatable; + + /** + * We keep a link to the original entity and cascade changes. + * Like that, if it gets removed, this entity will also be removed. + */ + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "NPM_PACKAGE_VER_RES_ID", referencedColumnName = "PID") + @OnDelete(action = OnDeleteAction.CASCADE) + private NpmPackageVersionResourceEntity npmPackageVersionResourceEntity; + + public Long getId() { + return this.id; + } + + public void setId(final Long id) { + this.id = id; + } + + public String getCanonicalUrl() { + return this.canonicalUrl; + } + + public void setCanonicalUrl(final String canonicalUrl) { + this.canonicalUrl = canonicalUrl; + } + + public String getTitle() { + return this.title; + } + + public void setTitle(final String title) { + this.title = title; + } + + public String getPackageId() { + return this.packageId; + } + + public void setPackageId(final String packageId) { + this.packageId = packageId; + } + + public String getPackageVersion() { + return this.packageVersion; + } + + public void setPackageVersion(final String packageVersion) { + this.packageVersion = packageVersion; + } + + public String getType() { + return this.type; + } + + public void setType(final String type) { + this.type = type; + } + + public String getKind() { + return this.kind; + } + + public void setKind(final String kind) { + this.kind = kind; + } + + public Boolean isCurrent() { + return this.isCurrent; + } + + public void setCurrent(final Boolean current) { + isCurrent = current; + } + + public Boolean isValidatable() { + return this.isValidatable; + } + + public void setValidatable(final Boolean validatable) { + isValidatable = validatable; + } + + public void setNpmPackageVersionResourceEntity(final NpmPackageVersionResourceEntity npmPackageVersionResourceEntity) { + this.npmPackageVersionResourceEntity = npmPackageVersionResourceEntity; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof final MbInstalledStructureDefinitionEntity that)) return false; + return id.equals(that.id) + && canonicalUrl.equals(that.canonicalUrl) + && title.equals(that.title) + && packageId.equals(that.packageId) + && packageVersion.equals(that.packageVersion) + && type.equals(that.type) + && kind.equals(that.kind) + && isCurrent.equals(that.isCurrent) + && isValidatable.equals(that.isValidatable); + } + + @Override + public int hashCode() { + return Objects.hash(id, canonicalUrl, title, packageId, packageVersion, type, kind, isCurrent, isValidatable); + } + + @Override + public String toString() { + return "MbInstalledStructureDefinitionEntity{" + + "id=" + id + + ", canonicalUrl='" + canonicalUrl + '\'' + + ", title='" + title + '\'' + + ", packageId='" + packageId + '\'' + + ", packageVersion='" + packageVersion + '\'' + + ", type='" + type + '\'' + + ", kind='" + kind + '\'' + + ", isCurrent=" + isCurrent + + ", isValidatable=" + isValidatable + + '}'; + } +} diff --git a/matchbox-server/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java b/matchbox-server/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java index 50c85bbe6d..572cb10aba 100644 --- a/matchbox-server/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java +++ b/matchbox-server/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java @@ -138,6 +138,9 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac @Autowired(required = false) // It is possible that some implementers will not create such a bean. private IBinaryStorageSvc myBinaryStorageSvc; + + @Autowired + private MatchboxJpaPackageCache matchboxJpaPackageCache; @Override public void addPackageServer(@Nonnull PackageServer thePackageServer) { @@ -416,7 +419,7 @@ private NpmPackage addPackageToCacheInternal(NpmPackageData thePackageData) { resourceEntity.setCanonicalVersion(version); } // PATCH MATCHBOX: the next line is our customization hook: https://github.com/ahdis/matchbox/issues/341 - MatchboxJpaPackageCache.customizeNpmPackageVersionResourceEntity(resourceEntity, resource); + matchboxJpaPackageCache.interceptEntityBeforeSaving(resourceEntity, resource); myPackageVersionResourceDao.save(resourceEntity); String resType = packageContext.getResourceType(resource); diff --git a/matchbox-server/src/main/java/ca/uhn/fhir/jpa/starter/Application.java b/matchbox-server/src/main/java/ca/uhn/fhir/jpa/starter/Application.java index 459f86886b..a012a05713 100644 --- a/matchbox-server/src/main/java/ca/uhn/fhir/jpa/starter/Application.java +++ b/matchbox-server/src/main/java/ca/uhn/fhir/jpa/starter/Application.java @@ -15,6 +15,7 @@ import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Import; diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/config/MatchboxJpaConfig.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/config/MatchboxJpaConfig.java index b26677db25..d9194b1c01 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/config/MatchboxJpaConfig.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/config/MatchboxJpaConfig.java @@ -9,6 +9,8 @@ import ca.uhn.fhir.jpa.config.JpaConfig; import ch.ahdis.matchbox.CliContext; import ch.ahdis.matchbox.interceptors.*; +import ch.ahdis.matchbox.packages.MatchboxJpaPackageCache; +import ca.uhn.fhir.jpa.dao.data.MbInstalledStructureDefinitionRepository; import ch.ahdis.matchbox.mappinglanguage.StructureMapListProvider; import ch.ahdis.matchbox.packages.*; import ch.ahdis.matchbox.providers.*; @@ -66,7 +68,6 @@ import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.starter.AppProperties; import ca.uhn.fhir.jpa.starter.common.StarterJpaConfig; -import ca.uhn.fhir.jpa.validation.JpaValidationSupportChain; import ca.uhn.fhir.mdm.provider.MdmProviderLoader; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; @@ -166,6 +167,9 @@ public class MatchboxJpaConfig extends StarterJpaConfig { @Autowired private ValueSetCodeValidationProvider valueSetCodeValidationProvider; + @Autowired + private MbInstalledStructureDefinitionRepository installedStructureDefinitionRepository; + // removed GraphQlProvider // removed IVAldiationSupport @@ -246,7 +250,11 @@ public RestfulServer restfulServer(IFhirSystemDao fhirSystemDao, AppProper "implementationGuideResourceProvider"); } - fhirServer.setServerConformanceProvider(new MatchboxCapabilityStatementProvider(this.myFhirContext,fhirServer, structureDefinitionProvider, getCliContext())); + fhirServer.setServerConformanceProvider(new MatchboxCapabilityStatementProvider(this.myFhirContext, + fhirServer, + this.structureDefinitionProvider, + getCliContext(), + this.installedStructureDefinitionRepository)); return fhirServer; } @@ -421,4 +429,9 @@ public IJobPersistence batch2JobInstancePersister( public InstallNpmPackageProvider installNpmPackageOperationProvider(final MatchboxPackageInstallerImpl packageInstallerSvc) { return new InstallNpmPackageProvider(packageInstallerSvc); } + + @Bean + public MatchboxJpaPackageCache matchboxJpaPackageCache(final MbInstalledStructureDefinitionRepository installedStructureDefinitionRepository) { + return new MatchboxJpaPackageCache(installedStructureDefinitionRepository); + } } diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/MatchboxJpaPackageCache.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/MatchboxJpaPackageCache.java index 4e6873ebc0..a55bbcb4e7 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/MatchboxJpaPackageCache.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/MatchboxJpaPackageCache.java @@ -1,90 +1,102 @@ package ch.ahdis.matchbox.packages; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.data.MbInstalledStructureDefinitionRepository; +import ca.uhn.fhir.jpa.model.entity.MbInstalledStructureDefinitionEntity; import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionResourceEntity; import ca.uhn.fhir.util.FhirTerser; import org.checkerframework.checker.nullness.qual.Nullable; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; /** - * matchbox + * A service to help Matchbox customizing the JPA * * @author Quentin Ligier * @see The NpmPackageVersionResourceEntity update is costly **/ +@Service public class MatchboxJpaPackageCache { - private static final Logger ourLog = LoggerFactory.getLogger(MatchboxJpaPackageCache.class); - public static final String SD_EXTENSION_TITLE_PREFIX = "[Extension] "; - public static final String SD_PRIMITIVE_DT_TITLE_PREFIX = "[Primitive Datatype] "; - public static final String SD_COMPLEX_DT_TITLE_PREFIX = "[Complex Datatype] "; - public static final String SD_LOGICAL_TITLE_PREFIX = "[Logical] "; + private final MbInstalledStructureDefinitionRepository installedStructureDefinitionRepository; + + public MatchboxJpaPackageCache(final MbInstalledStructureDefinitionRepository installedStructureDefinitionRepository) { + this.installedStructureDefinitionRepository = installedStructureDefinitionRepository; + } /** - * This class is not instantiable. + * This is Matchbox's hook to intercept the {@link NpmPackageVersionResourceEntity}s being generated when loading a + * package right before it gets saved in the database. + *

+ * This will check the resource type and delegate to the appropriate method to customize the entity. */ - private MatchboxJpaPackageCache() { + public void interceptEntityBeforeSaving(final NpmPackageVersionResourceEntity entity, + final IBaseResource res) { + switch (res) { + case org.hl7.fhir.r4.model.StructureDefinition sdR4 -> + this.interceptStructureDefinition(entity, sdR4, null, null); + case org.hl7.fhir.r4b.model.StructureDefinition sdR4b -> + this.interceptStructureDefinition(entity, null, sdR4b, null); + case org.hl7.fhir.r5.model.StructureDefinition sdR5 -> + this.interceptStructureDefinition(entity, null, null, sdR5); + case org.hl7.fhir.r4.model.StructureMap smR4 -> this.updateStructureMap(entity, smR4, null, null); + case org.hl7.fhir.r4b.model.StructureMap smR4b -> this.updateStructureMap(entity, null, smR4b, null); + case org.hl7.fhir.r5.model.StructureMap smR5 -> this.updateStructureMap(entity, null, null, smR5); + default -> { /* do nothing */ } + } } - /** - * This is Matchbox's hook to customize the {@link NpmPackageVersionResourceEntity}s being generated when loading a - * package. + * This is Matchbox's hook to intercept the {@link NpmPackageVersionResourceEntity}s being generated when loading a + * package right after it gets saved in the database. *

* This will check the resource type and delegate to the appropriate method to customize the entity. */ - public static void customizeNpmPackageVersionResourceEntity(final NpmPackageVersionResourceEntity entity, - final IBaseResource res) { + public void interceptEntityAfterSaving(final NpmPackageVersionResourceEntity entity, + final IBaseResource res) { switch (res) { - case org.hl7.fhir.r4.model.StructureDefinition sdR4 -> customizeStructureDefinition(entity, sdR4, null, null); + case org.hl7.fhir.r4.model.StructureDefinition sdR4 -> + this.interceptStructureDefinition(entity, sdR4, null, null); case org.hl7.fhir.r4b.model.StructureDefinition sdR4b -> - customizeStructureDefinition(entity, null, sdR4b, null); - case org.hl7.fhir.r5.model.StructureDefinition sdR5 -> customizeStructureDefinition(entity, null, null, sdR5); - case org.hl7.fhir.r4.model.StructureMap smR4 -> customizeStructureMap(entity, smR4, null, null); - case org.hl7.fhir.r4b.model.StructureMap smR4b -> customizeStructureMap(entity, null, smR4b, null); - case org.hl7.fhir.r5.model.StructureMap smR5 -> customizeStructureMap(entity, null, null, smR5); + this.interceptStructureDefinition(entity, null, sdR4b, null); + case org.hl7.fhir.r5.model.StructureDefinition sdR5 -> + this.interceptStructureDefinition(entity, null, null, sdR5); default -> { /* do nothing */ } } } /** - * Updates the NpmPackageVersionResourceEntity of a StructureDefinition: - *

    - *
  1. entity.myFilename now contains the StructureDefinition.title or StructureDefinition.name
  2. - *
  3. entity.myCanonicalVersion now contains the StructureDefinition package version
  4. - *
+ * Intercept a StructureDefinition before it gets saved in the database. Create our + * MbInstalledStructureDefinitionEntity to store it in an optimized way. */ - private static void customizeStructureDefinition(final NpmPackageVersionResourceEntity npmPackageVersionResourceEntity, - final org.hl7.fhir.r4.model.@Nullable StructureDefinition sdR4, - final org.hl7.fhir.r4b.model.@Nullable StructureDefinition sdR4b, - final org.hl7.fhir.r5.model.@Nullable StructureDefinition sdR5) { - // we update the canonical version to the package version for StructureDefinitions - // https://github.com/ahdis/matchbox/issues/225 - npmPackageVersionResourceEntity.setCanonicalVersion(npmPackageVersionResourceEntity.getPackageVersion().getVersionId()); - + private void interceptStructureDefinition(final NpmPackageVersionResourceEntity npmPackageVersionResourceEntity, + final org.hl7.fhir.r4.model.@Nullable StructureDefinition sdR4, + final org.hl7.fhir.r4b.model.@Nullable StructureDefinition sdR4b, + final org.hl7.fhir.r5.model.@Nullable StructureDefinition sdR5) { final var terser = new FhirTerserWrapper(sdR4, sdR4b, sdR5); - - final var type = terser.getSinglePrimitiveValueOrNull("type"); - final var kind = terser.getSinglePrimitiveValueOrNull("kind"); - var title = terser.getSinglePrimitiveValueOrNull("title"); if (title == null) { title = terser.getSinglePrimitiveValueOrNull("name"); } - if ("primitive-type".equals(kind)) { - title = SD_PRIMITIVE_DT_TITLE_PREFIX + title; - } else if ("complex-type".equals(kind)) { - title = SD_COMPLEX_DT_TITLE_PREFIX + title; - } else if ("logical".equals(kind)) { - title = SD_LOGICAL_TITLE_PREFIX + title; - } else if ("Extension".equals(type)) { - title = SD_EXTENSION_TITLE_PREFIX + title; - } - - // Change the filename for the StructureDefinition title - npmPackageVersionResourceEntity.setFilename(title); + final var type = terser.getSinglePrimitiveValueOrNull("type"); + final var kind = terser.getSinglePrimitiveValueOrNull("kind"); + final var isValidatable = !"primitive-type".equals(kind) + && !"complex-type".equals(kind) + && !"logical".equals(kind) + && !"Extension".equals(type); + + final var entity = new MbInstalledStructureDefinitionEntity(); + entity.setCanonicalUrl(npmPackageVersionResourceEntity.getCanonicalUrl()); + entity.setTitle(title); + entity.setPackageId(npmPackageVersionResourceEntity.getPackageVersion().getPackageId()); + entity.setPackageVersion(npmPackageVersionResourceEntity.getPackageVersion().getVersionId()); + entity.setType(type); + entity.setKind(kind); + entity.setCurrent(npmPackageVersionResourceEntity.getPackageVersion().isCurrentVersion()); + entity.setValidatable(isValidatable); + entity.setNpmPackageVersionResourceEntity(npmPackageVersionResourceEntity); + + this.installedStructureDefinitionRepository.save(entity); } /** @@ -93,28 +105,16 @@ private static void customizeStructureDefinition(final NpmPackageVersionResource *
  • entity.myFilename now contains the StructureMap.title or StructureMap.name
  • * */ - private static void customizeStructureMap(final NpmPackageVersionResourceEntity npmPackageVersionResourceEntity, - final org.hl7.fhir.r4.model.@Nullable StructureMap smR4, - final org.hl7.fhir.r4b.model.@Nullable StructureMap smR4b, - final org.hl7.fhir.r5.model.@Nullable StructureMap smR5) { + private void updateStructureMap(final NpmPackageVersionResourceEntity npmPackageVersionResourceEntity, + final org.hl7.fhir.r4.model.@Nullable StructureMap smR4, + final org.hl7.fhir.r4b.model.@Nullable StructureMap smR4b, + final org.hl7.fhir.r5.model.@Nullable StructureMap smR5) { final var terser = new FhirTerserWrapper(smR4, smR4b, smR5); // Change the filename for the StructureDefinition title npmPackageVersionResourceEntity.setFilename(terser.getSinglePrimitiveValueOrNull("title")); } - /** - * Checks if a StructureDefinition is validatable, i.e. if it is returned in the list of profiles supported - * by the server in the $validate OperationDefinition and in the Gazelle Webservice. - */ - public static boolean structureDefinitionIsValidatable(final String title) { - // All those prefixes are added when loading the IGs, so we can filter out the profiles - return !title.startsWith(SD_EXTENSION_TITLE_PREFIX) - && !title.startsWith(SD_PRIMITIVE_DT_TITLE_PREFIX) - && !title.startsWith(SD_COMPLEX_DT_TITLE_PREFIX) - && !title.startsWith(SD_LOGICAL_TITLE_PREFIX); - } - // A small wrapper around FhirTerser to handle the different FHIR versions of a resource private static class FhirTerserWrapper { private final IBase resource; diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/providers/ConformancePackageResourceProvider.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/providers/ConformancePackageResourceProvider.java index 123a6cca87..9cc9ee860b 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/providers/ConformancePackageResourceProvider.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/providers/ConformancePackageResourceProvider.java @@ -29,7 +29,6 @@ import ca.uhn.fhir.jpa.binary.api.IBinaryStorageSvc; import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionResourceDao; import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionResourceEntity; -import ca.uhn.fhir.jpa.starter.AppProperties; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; @@ -57,17 +56,12 @@ import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.server.IResourceProvider; -import static ch.ahdis.matchbox.util.MatchboxServerUtils.addExtension; - @DisallowConcurrentExecution public class ConformancePackageResourceProvider implements IResourceProvider { @Autowired protected MatchboxEngineSupport matchboxEngineSupport; - @Autowired - AppProperties appProperties; - @Autowired private INpmPackageVersionResourceDao myPackageVersionResourceDao; @@ -200,67 +194,6 @@ public ca.uhn.fhir.rest.api.server.IBundleProvider search(jakarta.servlet.http.H return null; } - /** - * Returns the list of installed StructureDefinitions, as a list of R5 CanonicalTypes. - */ - public List getCanonicalsR5() { - return new TransactionTemplate(myTxManager).execute(tx -> { - final var page = PageRequest.of(0, 2147483646); - - // Find the IDs of the current StructureDefinitions. - final var currentEntityIds = - this.myPackageVersionResourceDao.findCurrentByResourceType(page, this.resourceType) - .stream() - .map(NpmPackageVersionResourceEntity::getId) - .collect(Collectors.toUnmodifiableSet()); - - return this.myPackageVersionResourceDao.findByResourceType(page, this.resourceType) - .stream() - .peek(entity -> { - // NB: getCanonicalVersion() may be null is rare cases, but getPackageVersion().getVersionId() should not - if (entity.getCanonicalVersion() == null) { - entity.setCanonicalVersion(entity.getPackageVersion().getVersionId()); - } - }) - // Sort the StructureDefinitions by canonical URL first, and then by version - .sorted(Comparator - .comparing(NpmPackageVersionResourceEntity::getCanonicalUrl) - .thenComparing(NpmPackageVersionResourceEntity::getCanonicalVersion)) - .map(entity -> { - final var canonical = new org.hl7.fhir.r5.model.CanonicalType(entity.getCanonicalUrl()); - // Add custom extensions to the CanonicalType to store additional information - addExtension(canonical, "ig-id", - new org.hl7.fhir.r5.model.StringType(entity.getPackageVersion().getPackageId())); - addExtension(canonical, "ig-version", - new org.hl7.fhir.r5.model.StringType(entity.getCanonicalVersion())); - addExtension(canonical, "ig-current", - new org.hl7.fhir.r5.model.BooleanType(currentEntityIds.contains(entity.getId()))); - addExtension(canonical, "sd-canonical", new org.hl7.fhir.r5.model.StringType(entity.getCanonicalUrl())); - if (entity.getFilename() != null && !entity.getFilename().isBlank()) { - addExtension(canonical, "sd-title", new org.hl7.fhir.r5.model.StringType(entity.getFilename())); - } else { - addExtension(canonical, "sd-title", new org.hl7.fhir.r5.model.StringType(entity.getCanonicalUrl())); - } - return canonical; - }) - .toList(); - }); - } - - public List getPackageResources() { - return new TransactionTemplate(this.myTxManager).execute(tx -> { - return myPackageVersionResourceDao - .findByResourceType(PageRequest.of(0, 2147483646), resourceType).stream().toList(); - }); - } - - public List getCurrentPackageResources() { - return new TransactionTemplate(this.myTxManager).execute(tx -> { - return myPackageVersionResourceDao - .findCurrentByResourceType(PageRequest.of(0, 2147483646), resourceType).stream().toList(); - }); - } - protected IBaseResource loadPackageEntityAdjustId(NpmPackageVersionResourceEntity contents) { IBaseResource resource = loadPackageEntity(contents); if (resource != null) { diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/providers/MatchboxCapabilityStatementProvider.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/providers/MatchboxCapabilityStatementProvider.java index f0659e567a..6ac238d323 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/providers/MatchboxCapabilityStatementProvider.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/providers/MatchboxCapabilityStatementProvider.java @@ -3,6 +3,7 @@ import ca.uhn.fhir.context.BaseRuntimeChildDefinition; import ca.uhn.fhir.context.BaseRuntimeElementDefinition; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.data.MbInstalledStructureDefinitionRepository; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -20,29 +21,33 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r5.model.BooleanType; +import org.hl7.fhir.r5.model.CanonicalType; import org.hl7.fhir.r5.model.CapabilityStatement; -import org.hl7.fhir.r5.model.DataType; import org.hl7.fhir.r5.model.Enumerations; import org.hl7.fhir.r5.model.OperationDefinition; import org.hl7.fhir.r5.model.StringType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.lang.reflect.Field; -import static ch.ahdis.matchbox.packages.MatchboxJpaPackageCache.structureDefinitionIsValidatable; - /** * A provider of CapabilityStatement customized for Matchbox. */ public class MatchboxCapabilityStatementProvider extends ServerCapabilityStatementProvider { + private static final Logger log = LoggerFactory.getLogger(MatchboxCapabilityStatementProvider.class); + private static final String VALIDATE_OPERATION_NAME = "Validate"; private final StructureDefinitionResourceProvider structureDefinitionProvider; protected final CliContext cliContext; protected final FhirContext myFhirContext; + private final MbInstalledStructureDefinitionRepository installedStructureDefinitionRepository; public MatchboxCapabilityStatementProvider(final FhirContext fhirContext, final RestfulServer theServerConfiguration, final StructureDefinitionResourceProvider structureDefinitionProvider, - final CliContext cliContext) { + final CliContext cliContext, + final MbInstalledStructureDefinitionRepository installedStructureDefinitionRepository) { super(theServerConfiguration, null, null); this.structureDefinitionProvider = structureDefinitionProvider; this.cliContext = cliContext; @@ -52,6 +57,7 @@ public MatchboxCapabilityStatementProvider(final FhirContext fhirContext, theServerConfiguration.setImplementationDescription("Development mode"); } this.myFhirContext = fhirContext; + this.installedStructureDefinitionRepository = installedStructureDefinitionRepository; } protected void postProcessRestResource(FhirTerser theTerser, IBase theResource, String theResourceName) { @@ -180,9 +186,16 @@ private void updateValidateOperationDefinition(final OperationDefinition validat .setMax("1") .setType(Enumerations.FHIRTypes.CODE); - final var profiles = this.structureDefinitionProvider.getCanonicalsR5().stream() - .filter(sd -> structureDefinitionIsValidatable(sd.getExtensionByUrl("sd-title").getValueStringType().getValue())) - .toList(); + final var profiles = this.installedStructureDefinitionRepository.findAllValidatable().stream() + .map(entity -> { + final var canonical = new CanonicalType(entity.getCanonicalUrl()); + canonical.addExtension("ig-id", new StringType(entity.getPackageId())); + canonical.addExtension("ig-version", new StringType(entity.getPackageVersion())); + canonical.addExtension("ig-current", new BooleanType(entity.isCurrent())); + canonical.addExtension("sd-canonical", new StringType(entity.getCanonicalUrl())); + canonical.addExtension("sd-title", new StringType(entity.getTitle())); + return canonical; + }).toList(); validateOperationDefinition.addParameter() .setName("profile") .setUse(Enumerations.OperationParameterUse.IN) @@ -200,20 +213,17 @@ private void updateValidateOperationDefinition(final OperationDefinition validat final var cliContextProperties = this.cliContext.getValidateEngineParameters(); for (final Field field : cliContextProperties) { field.setAccessible(true); + final var isBoolean = field.getType().equals(boolean.class) || field.getType().equals(Boolean.class); try { validateOperationDefinition.addParameter() .setName(field.getName()) .setUse(Enumerations.OperationParameterUse.IN) .setMin(0) .setMax("1") - .setType(field.getType().equals(boolean.class) || field.getType().equals(Boolean.class) ? Enumerations.FHIRTypes.BOOLEAN : Enumerations.FHIRTypes.STRING) - .addExtension("http://matchbox.health/validationDefaultValue", field.getType().equals(boolean.class) || field.getType().equals(Boolean.class) ? new BooleanType((Boolean) field.get(cliContext)) : new StringType((String) field.get(cliContext))); - } catch (IllegalArgumentException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IllegalAccessException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + .setType(isBoolean ? Enumerations.FHIRTypes.BOOLEAN : Enumerations.FHIRTypes.STRING) + .addExtension("http://matchbox.health/validationDefaultValue", isBoolean ? new BooleanType((Boolean) field.get(cliContext)) : new StringType((String) field.get(cliContext))); + } catch (final Exception e) { + log.warn("Unable to inspect field", e); } } @@ -223,6 +233,5 @@ private void updateValidateOperationDefinition(final OperationDefinition validat .setMin(0) .setMax("1") .setType(Enumerations.FHIRTypes.STRING); - } } diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/ValidationProvider.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/ValidationProvider.java index 05a59d7085..daca988d80 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/ValidationProvider.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/ValidationProvider.java @@ -55,7 +55,6 @@ import org.hl7.fhir.r5.model.UriType; import org.hl7.fhir.r5.utils.OperationOutcomeUtilities; import org.hl7.fhir.r5.utils.ToolingExtensions; -import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; import org.hl7.fhir.utilities.validation.ValidationMessage; import org.springframework.beans.factory.annotation.Autowired; diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/gazelle/GazelleValidationWs.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/gazelle/GazelleValidationWs.java index 5763c45a1e..61a5af09ef 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/gazelle/GazelleValidationWs.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/gazelle/GazelleValidationWs.java @@ -1,12 +1,12 @@ package ch.ahdis.matchbox.validation.gazelle; -import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionResourceEntity; +import ca.uhn.fhir.jpa.dao.data.MbInstalledStructureDefinitionRepository; +import ca.uhn.fhir.jpa.model.entity.MbInstalledStructureDefinitionEntity; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.util.StopWatch; import ch.ahdis.matchbox.validation.ValidationProvider; import ch.ahdis.matchbox.CliContext; import ch.ahdis.matchbox.util.MatchboxEngineSupport; -import ch.ahdis.matchbox.providers.StructureDefinitionResourceProvider; import ch.ahdis.matchbox.engine.MatchboxEngine; import ch.ahdis.matchbox.engine.cli.VersionUtil; import ch.ahdis.matchbox.engine.exception.MatchboxEngineCreationException; @@ -28,8 +28,6 @@ import java.util.List; import java.util.Objects; -import static ch.ahdis.matchbox.packages.MatchboxJpaPackageCache.structureDefinitionIsValidatable; - /** * The WebService for validation with the new Gazelle Validation API. * @@ -49,17 +47,17 @@ public class GazelleValidationWs { private final MatchboxEngineSupport matchboxEngineSupport; - private final StructureDefinitionResourceProvider structureDefinitionProvider; + private final MbInstalledStructureDefinitionRepository installedStructureDefinitionRepository; // The base CLI context, with the default parameters private final CliContext baseCliContext; public GazelleValidationWs(final MatchboxEngineSupport matchboxEngineSupport, final CliContext baseCliContext, - final StructureDefinitionResourceProvider structureDefinitionProvider) { + final MbInstalledStructureDefinitionRepository installedStructureDefinitionRepository) { this.matchboxEngineSupport = Objects.requireNonNull(matchboxEngineSupport); this.baseCliContext = Objects.requireNonNull(baseCliContext); - this.structureDefinitionProvider = Objects.requireNonNull(structureDefinitionProvider); + this.installedStructureDefinitionRepository = Objects.requireNonNull(installedStructureDefinitionRepository); } /** @@ -97,28 +95,24 @@ public Service getMetadata(final HttpServletRequest request) { @GetMapping(path = PROFILES_PATH, produces = MediaType.APPLICATION_JSON_VALUE) public List getProfiles() { // Filter the extensions, because they won't be validated directly - final List entities = - this.structureDefinitionProvider.getPackageResources().stream() - .filter(packageVersionResource -> structureDefinitionIsValidatable(packageVersionResource.getFilename())) - .toList(); + final List entities = + this.installedStructureDefinitionRepository.findAllValidatable(); final var profiles = new ArrayList(entities.size()*2); - entities.forEach(packageVersionResource -> { + entities.forEach(installedStructDef -> { final var profile = new ValidationProfile(); - final var version = packageVersionResource.getCanonicalVersion(); - profile.setProfileID("%s|%s".formatted(packageVersionResource.getCanonicalUrl(), version)); - // PATCHed: filename contains the StructureDefinition title. - profile.setProfileName("%s (%s)".formatted(packageVersionResource.getFilename(), version)); - profile.setDomain(packageVersionResource.getPackageVersion().getPackageId()); + final var version = installedStructDef.getPackageVersion(); + profile.setProfileID("%s|%s".formatted(installedStructDef.getCanonicalUrl(), version)); + profile.setProfileName("%s (%s)".formatted(installedStructDef.getTitle(), version)); + profile.setDomain(installedStructDef.getPackageId()); profiles.add(profile); // If the package is current, we also add it version-less - if (packageVersionResource.getPackageVersion().isCurrentVersion()) { + if (installedStructDef.isCurrent()) { final var profile2 = new ValidationProfile(); - profile2.setProfileID(packageVersionResource.getCanonicalUrl()); - // PATCHed: filename contains the StructureDefinition title. - profile2.setProfileName(packageVersionResource.getFilename()); - profile2.setDomain(packageVersionResource.getPackageVersion().getPackageId()); + profile2.setProfileID(installedStructDef.getCanonicalUrl()); + profile2.setProfileName(installedStructDef.getTitle()); + profile2.setDomain(installedStructDef.getPackageId()); profiles.add(profile2); } }); From 26ce6fcead76e71811896313ca1eb3e776666500 Mon Sep 17 00:00:00 2001 From: Quentin Ligier Date: Fri, 18 Apr 2025 09:36:40 +0200 Subject: [PATCH 2/2] Restore patch for #225 --- .../ahdis/matchbox/packages/MatchboxJpaPackageCache.java | 8 +++++++- .../ch/ahdis/matchbox/util/MatchboxEngineSupport.java | 4 ++-- .../ch/ahdis/matchbox/validation/ValidationProvider.java | 3 +-- .../ch/ahdis/matchbox/gazelle/AbstractGazelleTest.java | 3 +++ 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/MatchboxJpaPackageCache.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/MatchboxJpaPackageCache.java index a55bbcb4e7..6bd3f54148 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/MatchboxJpaPackageCache.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/packages/MatchboxJpaPackageCache.java @@ -73,6 +73,12 @@ private void interceptStructureDefinition(final NpmPackageVersionResourceEntity final org.hl7.fhir.r4.model.@Nullable StructureDefinition sdR4, final org.hl7.fhir.r4b.model.@Nullable StructureDefinition sdR4b, final org.hl7.fhir.r5.model.@Nullable StructureDefinition sdR5) { + // 1. Modify the original entity + // We update the canonical version to the package version for StructureDefinitions + // https://github.com/ahdis/matchbox/issues/225 + npmPackageVersionResourceEntity.setCanonicalVersion(npmPackageVersionResourceEntity.getPackageVersion().getVersionId()); + + // 2. Extract interesting info final var terser = new FhirTerserWrapper(sdR4, sdR4b, sdR5); var title = terser.getSinglePrimitiveValueOrNull("title"); if (title == null) { @@ -85,6 +91,7 @@ private void interceptStructureDefinition(final NpmPackageVersionResourceEntity && !"logical".equals(kind) && !"Extension".equals(type); + // 3. Create our own entity for the StructureDefinition final var entity = new MbInstalledStructureDefinitionEntity(); entity.setCanonicalUrl(npmPackageVersionResourceEntity.getCanonicalUrl()); entity.setTitle(title); @@ -95,7 +102,6 @@ private void interceptStructureDefinition(final NpmPackageVersionResourceEntity entity.setCurrent(npmPackageVersionResourceEntity.getPackageVersion().isCurrentVersion()); entity.setValidatable(isValidatable); entity.setNpmPackageVersionResourceEntity(npmPackageVersionResourceEntity); - this.installedStructureDefinitionRepository.save(entity); } diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/util/MatchboxEngineSupport.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/util/MatchboxEngineSupport.java index 1a45688d74..e6a7d43e87 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/util/MatchboxEngineSupport.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/util/MatchboxEngineSupport.java @@ -294,7 +294,7 @@ public MatchboxEngine getMatchboxEngineNotSynchronized(final @Nullable String ca this.configureValidationEngine(mainEngine, cliContextMain); } else if (cliContextMain.getFhirVersion().equals("4.3.0")) { log.debug("Preconfigure FHIR R4B"); - mainEngine = new MatchboxEngineBuilder().withXVersion(cliContextMain.getXVersion()).getEngineR4B(); + mainEngine = new MatchboxEngineBuilder().withXVersion(cliContextMain.getXVersion()).getEngineR4B(); mainEngine.setIgLoader(new IgLoaderFromJpaPackageCache(mainEngine.getPcm(), mainEngine.getContext(), mainEngine.getVersion(), @@ -307,7 +307,7 @@ public MatchboxEngine getMatchboxEngineNotSynchronized(final @Nullable String ca this.configureValidationEngine(mainEngine, cliContextMain); } else if (cliContextMain.getFhirVersion().equals("5.0.0")) { log.debug("Preconfigure FHIR R5"); - mainEngine = new MatchboxEngineBuilder().withXVersion(cliContextMain.getXVersion()).getEngineR5(); + mainEngine = new MatchboxEngineBuilder().withXVersion(cliContextMain.getXVersion()).getEngineR5(); mainEngine.setIgLoader(new IgLoaderFromJpaPackageCache(mainEngine.getPcm(), mainEngine.getContext(), mainEngine.getVersion(), diff --git a/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/ValidationProvider.java b/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/ValidationProvider.java index daca988d80..f794828246 100644 --- a/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/ValidationProvider.java +++ b/matchbox-server/src/main/java/ch/ahdis/matchbox/validation/ValidationProvider.java @@ -195,8 +195,7 @@ public IBaseResource validate(final HttpServletRequest theRequest) { } if (engine == null) { return this.getOoForError( - "Matchbox engine for profile '%s' could not be created, check the installed IGs".formatted( - profile)); + "Matchbox engine for profile '%s' could not be created, check the installed IGs".formatted(profile)); } int versionSeparator = profile.lastIndexOf('|'); if (versionSeparator != -1) { diff --git a/matchbox-server/src/test/java/ch/ahdis/matchbox/gazelle/AbstractGazelleTest.java b/matchbox-server/src/test/java/ch/ahdis/matchbox/gazelle/AbstractGazelleTest.java index 0048098269..ae0d215801 100644 --- a/matchbox-server/src/test/java/ch/ahdis/matchbox/gazelle/AbstractGazelleTest.java +++ b/matchbox-server/src/test/java/ch/ahdis/matchbox/gazelle/AbstractGazelleTest.java @@ -20,6 +20,9 @@ public static int countValidationFailures(final ValidationReport report) { } public static String getMetadata(final ValidationReport report, final String metadataName) { + if (report.getAdditionalMetadata() == null) { + return null; + } for (final Metadata metadata : report.getAdditionalMetadata()) { if (metadataName.equals(metadata.getName())) { return metadata.getValue();