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