diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrBaseConfig.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrBaseConfig.java index 5baf37976..de4aec1db 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrBaseConfig.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrBaseConfig.java @@ -16,7 +16,8 @@ CrSettings settings( EvaluationSettings evaluationSettings, TerminologyServerClientSettings terminologyServerClientSettings) { return new CrSettings() .withEvaluationSettings(evaluationSettings) - .withTerminologyServerClientSettings(terminologyServerClientSettings); + .withTerminologyServerClientSettings(terminologyServerClientSettings) + .withValidatorPackage(new String[] {"hl7.fhir.us.ecr", "2.1.2"}); } @Bean diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrEcrValidateCondition.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrEcrValidateCondition.java new file mode 100644 index 000000000..809998731 --- /dev/null +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrEcrValidateCondition.java @@ -0,0 +1,35 @@ +package org.opencds.cqf.fhir.cr.hapi.config; + +import ca.uhn.fhir.validation.FhirValidator; +import org.opencds.cqf.fhir.cr.CrSettings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * Condition that gates the creation of a {@link FhirValidator} configured to load NPM packages provided by {@link CrSettings}. Based on the {@code cr.ecr.validate.enabled} + * property (or the equivalent {@code CR_ECR_VALIDATE_ENABLED} environment variable). + * + *

Set {@code cr.ecr.validate.enabled=true} (or export {@code CR_ECR_VALIDATE_ENABLED=true}) to + * enable the creation of this {@link FhirValidator}. + */ +public class CrEcrValidateCondition implements Condition { + private static final Logger ourLog = LoggerFactory.getLogger(CrEcrValidateCondition.class); + + static final String PROPERTY_NAME = "cr.ecr.validate.enabled"; + + @Override + public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { + var environment = conditionContext.getEnvironment(); + var enabled = environment.getProperty(PROPERTY_NAME, Boolean.class, false); + if (!enabled) { + ourLog.info( + "CrEcrValidateCondition not met: '{}' is not set to true. " + + "Custom FhirValidator will not be created.", + PROPERTY_NAME); + } + return enabled; + } +} diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrEcrValidateConfig.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrEcrValidateConfig.java new file mode 100644 index 000000000..7c0e0d67f --- /dev/null +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrEcrValidateConfig.java @@ -0,0 +1,113 @@ +package org.opencds.cqf.fhir.cr.hapi.config; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.validation.ValidatorResourceFetcher; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.validation.FhirValidator; +import java.io.IOException; +import java.io.InputStream; +import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService; +import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport; +import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain; +import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; +import org.hl7.fhir.utilities.npm.NpmPackage; +import org.opencds.cqf.fhir.cr.CrSettings; +import org.opencds.cqf.fhir.utility.repository.NpmRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** + * Configures a {@link FhirValidator} to be used by HAPI FHIR's built-in {@code $validate} operation. + * + *

This configuration is only activated when {@code cr.ecr.validate.enabled=true} is set (or the + * equivalent {@code CR_ECR_VALIDATE_ENABLED=true} environment variable). When disabled, the + * default HAPI FHIR {@code FhirValidator} is used. + */ +@Configuration +@Conditional(CrEcrValidateCondition.class) +public class CrEcrValidateConfig { + + private static final Logger logger = LoggerFactory.getLogger(CrEcrValidateConfig.class); + + /** + * Builds a {@link FhirInstanceValidator} using a {@link ValidationSupportChain} which contains + * a {@link PrePopulatedValidationSupport} that is populated with the contents of specified + * Npm Packages. + * + *

Marked {@link Primary} so it takes precedence when multiple {@link FhirInstanceValidator} beans + * are present in the application context. + */ + @Bean + @Primary + public FhirInstanceValidator crEcrFhirInstanceValidator( + FhirContext fhirContext, DaoRegistry daoRegistry, CrSettings crSettings) { + NpmRepository npmRepository = new NpmRepository(fhirContext, crSettings.getValidatorPackages()); + PrePopulatedValidationSupport prePopulatedValidationSupport = loadPrePopulatedValidationSupport(npmRepository); + var supportChain = new ValidationSupportChain( + new DefaultProfileValidationSupport(fhirContext), + new CommonCodeSystemsTerminologyService(fhirContext), + new InMemoryTerminologyServerValidationSupport(fhirContext), + new SnapshotGeneratingValidationSupport(fhirContext), + prePopulatedValidationSupport); + var instanceValidator = new FhirInstanceValidator(supportChain); + instanceValidator.setValidatorResourceFetcher( + new ValidatorResourceFetcher(fhirContext, supportChain, daoRegistry)); + return instanceValidator; + } + + /** + * This {@link FhirValidator} instance loads the above {@link FhirInstanceValidator} module. + * This instance which will conditionally be used by HAPI FHIR's built-in {@code $validate} operation. + * + *

Marked {@link Primary} so it takes precedence when multiple {@link FhirValidator} beans + * are present in the application context. + */ + @Bean + @Primary + public FhirValidator crEcrFhirValidator(FhirContext fhirContext, FhirInstanceValidator crEcrFhirInstanceValidator) { + var validator = fhirContext.newValidator().registerValidatorModule(crEcrFhirInstanceValidator); + validator.setValidateAgainstStandardSchema(false); + validator.setValidateAgainstStandardSchematron(false); + return validator; + } + + private PrePopulatedValidationSupport loadPrePopulatedValidationSupport(NpmRepository npmRepository) { + PrePopulatedValidationSupport prePopulatedValidationSupport = + new PrePopulatedValidationSupport(npmRepository.fhirContext()); + var parser = npmRepository.fhirContext().newJsonParser(); + for (var npmPackage : npmRepository.getLoadedPackages()) { + try { + var files = npmPackage.listResources("StructureDefinition", "ValueSet", "CodeSystem"); + for (var filename : files) { + logger.info("Loading pre-populated validation support from {}", filename); + addResource(npmPackage, filename, parser, prePopulatedValidationSupport); + } + + } catch (IOException e) { + logger.error("Error listing Resources from package {}", npmPackage.id(), e); + } + } + return prePopulatedValidationSupport; + } + + private void addResource( + NpmPackage npmPackage, + String filename, + IParser parser, + PrePopulatedValidationSupport prePopulatedValidationSupport) { + try (InputStream is = npmPackage.load("package", filename)) { + var resource = parser.parseResource(is); + prePopulatedValidationSupport.addResource(resource); + } catch (Exception e) { + logger.error("Error loading Resource from package {}: {}", npmPackage.id(), filename, e); + } + } +} diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java index a7d54ae12..937133d24 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/CrR4Config.java @@ -14,6 +14,7 @@ import org.opencds.cqf.fhir.cr.crmi.R4ReleaseService; import org.opencds.cqf.fhir.cr.ecr.r4.R4ERSDTransformService; import org.opencds.cqf.fhir.cr.hapi.common.StringTimePeriodHandler; +import org.opencds.cqf.fhir.cr.hapi.config.CrEcrValidateConfig; import org.opencds.cqf.fhir.cr.hapi.config.ProviderLoader; import org.opencds.cqf.fhir.cr.hapi.config.ProviderSelector; import org.opencds.cqf.fhir.cr.hapi.config.RepositoryConfig; @@ -69,7 +70,8 @@ WithdrawOperationConfig.class, ReviseOperationConfig.class, ArtifactDiffOperationConfig.class, - CreateChangelogOperationConfig.class + CreateChangelogOperationConfig.class, + CrEcrValidateConfig.class }) public class CrR4Config { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/CrSettings.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/CrSettings.java index f5ae66cde..470bdce11 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/CrSettings.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/CrSettings.java @@ -1,11 +1,14 @@ package org.opencds.cqf.fhir.cr; +import java.util.ArrayList; +import java.util.List; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.utility.client.TerminologyServerClientSettings; public class CrSettings { private EvaluationSettings evaluationSettings; private TerminologyServerClientSettings terminologyServerClientSettings; + private List validatorPackages; public static CrSettings getDefault() { return new CrSettings(); @@ -14,6 +17,7 @@ public static CrSettings getDefault() { public CrSettings() { evaluationSettings = EvaluationSettings.getDefault(); terminologyServerClientSettings = TerminologyServerClientSettings.getDefault(); + validatorPackages = new ArrayList<>(); } public EvaluationSettings getEvaluationSettings() { @@ -42,4 +46,17 @@ public CrSettings withTerminologyServerClientSettings( public void setTerminologyServerClientSettings(TerminologyServerClientSettings terminologyServerClientSettings) { this.terminologyServerClientSettings = terminologyServerClientSettings; } + + public List getValidatorPackages() { + return validatorPackages; + } + + public CrSettings withValidatorPackage(String[] validatorPackage) { + this.validatorPackages.add(validatorPackage); + return this; + } + + public void setValidatorPackages(List validatorPackages) { + this.validatorPackages = validatorPackages; + } }