diff --git a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/ActivityDefinitionProcessorFactory.java b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/ActivityDefinitionProcessorFactory.java index 74208193e9..4d51cfe2ab 100644 --- a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/ActivityDefinitionProcessorFactory.java +++ b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/ActivityDefinitionProcessorFactory.java @@ -1,14 +1,28 @@ package org.opencds.cqf.fhir.benchmark; import ca.uhn.fhir.repository.IRepository; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; +import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cr.activitydefinition.ActivityDefinitionProcessor; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.operations.IActivityDefinitionProcessor; import org.opencds.cqf.fhir.utility.repository.operations.IActivityDefinitionProcessorFactory; public class ActivityDefinitionProcessorFactory implements IActivityDefinitionProcessorFactory { + private final NpmPackageLoader npmPackageLoader; + private final EvaluationSettings evaluationSettings; + + public ActivityDefinitionProcessorFactory( + NpmPackageLoader npmPackageLoader, EvaluationSettings evaluationSettings) { + this.npmPackageLoader = npmPackageLoader; + this.evaluationSettings = evaluationSettings; + } + @Override public IActivityDefinitionProcessor create(IRepository repository) { - return new ActivityDefinitionProcessor(repository); + var engineInitializationContext = + new EngineInitializationContext(repository, npmPackageLoader, evaluationSettings); + return new ActivityDefinitionProcessor(repository, engineInitializationContext); } } diff --git a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/TestOperationProvider.java b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/TestOperationProvider.java index 759d2e6267..cddd1fa157 100644 --- a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/TestOperationProvider.java +++ b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/TestOperationProvider.java @@ -1,10 +1,25 @@ package org.opencds.cqf.fhir.benchmark; import ca.uhn.fhir.context.FhirContext; +import javax.annotation.Nonnull; +import org.opencds.cqf.fhir.cql.EvaluationSettings; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.operations.RepositoryOperationProvider; public class TestOperationProvider { - public static RepositoryOperationProvider newProvider(FhirContext fhirContext) { - return new RepositoryOperationProvider(fhirContext, new ActivityDefinitionProcessorFactory(), null, null, null); + public static RepositoryOperationProvider newProvider( + FhirContext fhirContext, NpmPackageLoader npmPackageLoader, EvaluationSettings evaluationSettings) { + return new RepositoryOperationProvider( + fhirContext, + newActivityDefinitionProcessorFactory(npmPackageLoader, evaluationSettings), + null, + null, + null); + } + + @Nonnull + private static ActivityDefinitionProcessorFactory newActivityDefinitionProcessorFactory( + NpmPackageLoader npmPackageLoader, EvaluationSettings evaluationSettings) { + return new ActivityDefinitionProcessorFactory(npmPackageLoader, evaluationSettings); } } diff --git a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/measure/r4/Measure.java b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/measure/r4/Measure.java index 38cb2cb54c..29911f1eb8 100644 --- a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/measure/r4/Measure.java +++ b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/measure/r4/Measure.java @@ -6,18 +6,23 @@ import ca.uhn.fhir.repository.IRepository; import java.nio.file.Path; import java.time.ZonedDateTime; +import java.util.Optional; import java.util.function.Supplier; +import javax.annotation.Nonnull; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.MeasureReport; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; +import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.MeasurePeriodValidator; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureService; -import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; public class Measure { @@ -46,9 +51,10 @@ public static Given given() { public static class Given { private IRepository repository; + private EngineInitializationContext engineInitializationContext; private MeasureEvaluationOptions evaluationOptions; private final MeasurePeriodValidator measurePeriodValidator; - private final R4MeasureServiceUtils measureServiceUtils; + private final R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider; public Given() { this.evaluationOptions = MeasureEvaluationOptions.defaultOptions(); @@ -65,14 +71,16 @@ public Given() { this.measurePeriodValidator = new MeasurePeriodValidator(); - this.measureServiceUtils = new R4MeasureServiceUtils(repository); + var npmPackageLoader = NpmPackageLoader.DEFAULT; + this.r4RepositoryOrNpmResourceProvider = new R4RepositoryOrNpmResourceProvider( + repository, npmPackageLoader, evaluationOptions.getEvaluationSettings()); } public Given repositoryFor(String repositoryPath) { this.repository = new IgRepository( FhirContext.forR4Cached(), Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); - + this.engineInitializationContext = getEngineInitializationContext(); return this; } @@ -82,12 +90,27 @@ public Given evaluationOptions(MeasureEvaluationOptions evaluationOptions) { } private R4MeasureService buildMeasureService() { - return new R4MeasureService(repository, evaluationOptions, measurePeriodValidator); + return new R4MeasureService( + repository, + engineInitializationContext, + evaluationOptions, + measurePeriodValidator, + r4RepositoryOrNpmResourceProvider); } public When when() { return new When(buildMeasureService()); } + + @Nonnull + private EngineInitializationContext getEngineInitializationContext() { + return new EngineInitializationContext( + this.repository, + NpmPackageLoader.DEFAULT, + Optional.ofNullable(evaluationOptions) + .map(MeasureEvaluationOptions::getEvaluationSettings) + .orElse(EvaluationSettings.getDefault())); + } } public static class When { diff --git a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/plandefinition/TestPlanDefinition.java b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/plandefinition/TestPlanDefinition.java index e083d685b7..c4cf5287a1 100644 --- a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/plandefinition/TestPlanDefinition.java +++ b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/plandefinition/TestPlanDefinition.java @@ -40,6 +40,7 @@ import org.opencds.cqf.fhir.benchmark.TestOperationProvider; import org.opencds.cqf.fhir.benchmark.helpers.DataRequirementsLibrary; import org.opencds.cqf.fhir.benchmark.helpers.GeneratedPackage; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; @@ -50,6 +51,7 @@ import org.opencds.cqf.fhir.utility.adapter.IParametersAdapter; import org.opencds.cqf.fhir.utility.model.FhirModelResolverCache; import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; import org.skyscreamer.jsonassert.JSONAssert; @@ -100,10 +102,12 @@ public static Given given() { public static class Given { private IRepository repository; + private NpmPackageLoader npmPackageLoader; private EvaluationSettings evaluationSettings; public Given repository(IRepository repository) { this.repository = repository; + this.npmPackageLoader = NpmPackageLoader.DEFAULT; return this; } @@ -111,6 +115,7 @@ public Given repositoryFor(FhirContext fhirContext, String repositoryPath) { this.repository = new IgRepository( fhirContext, Path.of("%s/%s/%s".formatted(getResourcePath(this.getClass()), CLASS_PATH, repositoryPath))); + this.npmPackageLoader = NpmPackageLoader.DEFAULT; return this; } @@ -121,7 +126,8 @@ public Given evaluationSettings(EvaluationSettings evaluationSettings) { public PlanDefinitionProcessor buildProcessor(IRepository repository) { if (repository instanceof IgRepository igRepository) { - igRepository.setOperationProvider(TestOperationProvider.newProvider(repository.fhirContext())); + igRepository.setOperationProvider(TestOperationProvider.newProvider( + repository.fhirContext(), npmPackageLoader, evaluationSettings)); } if (evaluationSettings == null) { evaluationSettings = EvaluationSettings.getDefault(); @@ -134,7 +140,9 @@ public PlanDefinitionProcessor buildProcessor(IRepository repository) { .getTerminologySettings() .setValuesetExpansionMode(VALUESET_EXPANSION_MODE.PERFORM_NAIVE_EXPANSION); } - return new PlanDefinitionProcessor(repository, evaluationSettings, null); + var engineInitializationContext = + new EngineInitializationContext(repository, NpmPackageLoader.DEFAULT, evaluationSettings); + return new PlanDefinitionProcessor(repository, evaluationSettings, engineInitializationContext, null); } public When when() { diff --git a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/questionnaire/TestQuestionnaire.java b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/questionnaire/TestQuestionnaire.java index 8d0adb6fda..2370fdc6a4 100644 --- a/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/questionnaire/TestQuestionnaire.java +++ b/cqf-fhir-benchmark/src/test/java/org/opencds/cqf/fhir/benchmark/questionnaire/TestQuestionnaire.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.repository.IRepository; import java.nio.file.Path; import java.util.List; +import java.util.Optional; import org.hl7.fhir.instance.model.api.IBaseBackboneElement; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseExtension; @@ -13,12 +14,14 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE; import org.opencds.cqf.fhir.cr.questionnaire.QuestionnaireProcessor; import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; public class TestQuestionnaire { @@ -50,7 +53,13 @@ public QuestionnaireProcessor buildProcessor(IRepository repository) { .getTerminologySettings() .setValuesetExpansionMode(VALUESET_EXPANSION_MODE.PERFORM_NAIVE_EXPANSION); } - return new QuestionnaireProcessor(repository, evaluationSettings, null, null, null, null); + var engineInitializationContext = new EngineInitializationContext( + this.repository, + NpmPackageLoader.DEFAULT, + Optional.ofNullable(this.evaluationSettings).orElse(EvaluationSettings.getDefault())); + + return new QuestionnaireProcessor( + repository, evaluationSettings, engineInitializationContext, null, null, null, null); } public When when() { diff --git a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/Engines.java b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/Engines.java index 72cfdecc99..ef8f0ac9bf 100644 --- a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/Engines.java +++ b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/Engines.java @@ -32,37 +32,39 @@ import org.opencds.cqf.fhir.cql.engine.retrieve.RepositoryRetrieveProvider; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings; import org.opencds.cqf.fhir.cql.engine.terminology.RepositoryTerminologyProvider; +import org.opencds.cqf.fhir.cql.npm.EnginesNpmLibraryHandler; import org.opencds.cqf.fhir.utility.Constants; import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; import org.opencds.cqf.fhir.utility.model.FhirModelResolverCache; +import org.opencds.cqf.fhir.utility.npm.MeasureOrNpmResourceHolder; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoaderWithCache; import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Engines { - private static Logger logger = LoggerFactory.getLogger(Engines.class); + private static final Logger logger = LoggerFactory.getLogger(Engines.class); private Engines() {} - public static CqlEngine forRepository(IRepository repository) { - return forRepository(repository, EvaluationSettings.getDefault()); + public static CqlEngine forContext(EngineInitializationContext initializationContext) { + return forContext(initializationContext, null); } - public static CqlEngine forRepository(IRepository repository, EvaluationSettings settings) { - return forRepository(repository, settings, null); - } - - public static CqlEngine forRepository( - IRepository repository, EvaluationSettings settings, IBaseBundle additionalData) { - checkNotNull(settings); - checkNotNull(repository); - + public static CqlEngine forContext(EngineInitializationContext initializationContext, IBaseBundle additionalData) { + var repository = initializationContext.repository; + var settings = initializationContext.evaluationSettings; var terminologyProvider = new RepositoryTerminologyProvider( - repository, settings.getValueSetCache(), settings.getTerminologySettings()); + repository, + settings.getValueSetCache(), + initializationContext.evaluationSettings.getTerminologySettings()); + var dataProviders = buildDataProviders(repository, additionalData, terminologyProvider, settings.getRetrieveSettings()); - var environment = buildEnvironment(repository, settings, terminologyProvider, dataProviders); + var environment = buildEnvironment( + repository, settings, terminologyProvider, dataProviders, initializationContext.npmPackageLoader); return createEngine(environment, settings); } @@ -70,7 +72,8 @@ private static Environment buildEnvironment( IRepository repository, EvaluationSettings settings, TerminologyProvider terminologyProvider, - Map dataProviders) { + Map dataProviders, + NpmPackageLoader npmPackageLoader) { var modelManager = settings.getModelCache() != null ? new ModelManager(settings.getModelCache()) : new ModelManager(); @@ -80,6 +83,7 @@ private static Environment buildEnvironment( registerLibrarySourceProviders(settings, libraryManager, repository); registerNpmSupport(settings, libraryManager, modelManager); + EnginesNpmLibraryHandler.registerNpmPackageLoader(libraryManager, modelManager, npmPackageLoader); return new Environment(libraryManager, dataProviders, terminologyProvider); } @@ -123,8 +127,8 @@ private static void registerNpmSupport( // list, and b) there are packages with different package ids but the same base canonical (e.g. // fhir.r4.examples has the same base canonical as fhir.r4) // NOTE: Using ensureNamespaceRegistered works around a but not b - Set keys = new HashSet(); - Set uris = new HashSet(); + Set keys = new HashSet<>(); + Set uris = new HashSet<>(); for (var n : npmProcessor.getNamespaces()) { if (!keys.contains(n.getName()) && !uris.contains(n.getUri())) { libraryManager.getNamespaceManager().addNamespace(n); @@ -182,4 +186,52 @@ public static CqlFhirParametersConverter getCqlFhirParametersConverter(FhirConte return new CqlFhirParametersConverter( fhirContext, IAdapterFactory.forFhirContext(fhirContext), fhirTypeConverter); } + + /** + * Maintain context needed to initialize a CqlEngine. This will initially contain both the + * Repository and NpmPackageLoader, but will eventually drop the Repository as qualifying + * clinical intelligence resources (starting with Library and Measure but later including + * ValueSets, PlanDefinitions, etc) are stored in the NpmPackageLoader, the Repository will + * eventually be dropped from this context and from initialization of the CqlEngine. + *

+ * This context also has convenience methods to create modified copies of itself when + * either the Repository or EvaluationSettings need to be changed for a specific evaluation. + */ + public static class EngineInitializationContext { + + private final IRepository repository; + private final NpmPackageLoader npmPackageLoader; + private final EvaluationSettings evaluationSettings; + + public EngineInitializationContext( + IRepository repository, NpmPackageLoader npmPackageLoader, EvaluationSettings evaluationSettings) { + this.repository = checkNotNull(repository); + this.npmPackageLoader = checkNotNull(npmPackageLoader); + this.evaluationSettings = checkNotNull(evaluationSettings); + } + + // For when a request builds a proxy or federated repository and needs to pass that to the + // Engine + public EngineInitializationContext withRepository(IRepository repository) { + return new EngineInitializationContext(repository, npmPackageLoader, evaluationSettings); + } + + public EngineInitializationContext withNpmPackageLoader(NpmPackageLoader npmPackageLoader) { + return new EngineInitializationContext(repository, npmPackageLoader, evaluationSettings); + } + + // For when a request evaluates evaluation settings + EngineInitializationContext withEvaluationSettings(EvaluationSettings evaluationSettings) { + return new EngineInitializationContext(repository, npmPackageLoader, evaluationSettings); + } + + public EngineInitializationContext withRepositoryAndCachedPackageLoader( + IRepository proxyRepoForMeasureProcessor, MeasureOrNpmResourceHolder measureOrNpmResourceHolder) { + + return new EngineInitializationContext( + proxyRepoForMeasureProcessor, + NpmPackageLoaderWithCache.of(measureOrNpmResourceHolder.npmResourceHolder(), npmPackageLoader), + evaluationSettings); + } + } } diff --git a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/EvaluationSettings.java b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/EvaluationSettings.java index 752cd3dbf6..cfa12277f6 100644 --- a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/EvaluationSettings.java +++ b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/EvaluationSettings.java @@ -1,5 +1,6 @@ package org.opencds.cqf.fhir.cql; +import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -26,6 +27,7 @@ public class EvaluationSettings { private RetrieveSettings retrieveSettings; private TerminologySettings terminologySettings; private NpmProcessor npmProcessor; + private boolean isUseNpmForQualifyingResources = false; public static EvaluationSettings getDefault() { return new EvaluationSettings(); @@ -161,4 +163,14 @@ public EvaluationSettings withNpmProcessor(NpmProcessor npmProcessor) { setNpmProcessor(npmProcessor); return this; } + + public boolean isUseNpmForQualifyingResources() { + return isUseNpmForQualifyingResources; + } + + @VisibleForTesting + public EvaluationSettings setUseNpmForQualifyingResources(boolean useNpmForQualifyingResources) { + isUseNpmForQualifyingResources = useNpmForQualifyingResources; + return this; + } } diff --git a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/LibraryEngine.java b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/LibraryEngine.java index 55d3540f4d..d8ee3f6975 100644 --- a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/LibraryEngine.java +++ b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/LibraryEngine.java @@ -27,6 +27,7 @@ import org.opencds.cqf.cql.engine.execution.CqlEngine; import org.opencds.cqf.cql.engine.execution.EvaluationResult; import org.opencds.cqf.cql.engine.execution.EvaluationResultsForMultiLib; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.engine.parameters.CqlFhirParametersConverter; import org.opencds.cqf.fhir.cql.engine.parameters.CqlParameterDefinition; import org.opencds.cqf.fhir.utility.CqfExpression; @@ -40,11 +41,16 @@ public class LibraryEngine { protected final IRepository repository; protected final FhirContext fhirContext; protected final EvaluationSettings settings; + protected final EngineInitializationContext engineInitializationContext; - public LibraryEngine(IRepository repository, EvaluationSettings evaluationSettings) { + public LibraryEngine( + IRepository repository, + EvaluationSettings evaluationSettings, + EngineInitializationContext engineInitializationContext) { this.repository = requireNonNull(repository, "repository can not be null"); this.settings = requireNonNull(evaluationSettings, "evaluationSettings can not be null"); fhirContext = repository.fhirContext(); + this.engineInitializationContext = engineInitializationContext; } public IRepository getRepository() { @@ -158,7 +164,11 @@ public IBaseParameters evaluateExpression( var requestSettings = new EvaluationSettings(settings); requestSettings.getLibrarySourceProviders().add(new StringLibrarySourceProvider(Lists.newArrayList(cql))); - var engine = Engines.forRepository(repository, requestSettings, bundle); + + var modifiedEngineInitializationContext = + engineInitializationContext.withRepository(repository).withEvaluationSettings(requestSettings); + + var engine = Engines.forContext(modifiedEngineInitializationContext, bundle); var evaluationParameters = cqlFhirParametersConverter.toCqlParameters(parameters); if (contextParameter != null) { @@ -343,7 +353,10 @@ public EvaluationResultsForMultiLib getEvaluationResult( // engine context built externally of LibraryEngine? var engineToUse = Objects.requireNonNullElseGet( - engine, () -> Engines.forRepository(repository, settings, additionalData)); + engine, + () -> Engines.forContext( + engineInitializationContext.withRepository(repository).withEvaluationSettings(settings), + additionalData)); var evaluationParameters = cqlFhirParametersConverterToUse.toCqlParameters(parameters); if (rawParameters != null && !rawParameters.isEmpty()) { diff --git a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/VersionedIdentifiers.java b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/VersionedIdentifiers.java index a85a1fa7da..e05968043b 100644 --- a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/VersionedIdentifiers.java +++ b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/VersionedIdentifiers.java @@ -1,8 +1,10 @@ package org.opencds.cqf.fhir.cql; +import java.util.regex.Pattern; import org.hl7.elm.r1.VersionedIdentifier; public class VersionedIdentifiers { + private static final Pattern LIBRARY_SPLIT_PATTERN = Pattern.compile("Library/"); private VersionedIdentifiers() { // empty @@ -14,13 +16,13 @@ public static VersionedIdentifier forUrl(String url) { "Invalid resource type for determining library version identifier: Library"); } - String[] urlSplit = url.split("Library/"); - if (urlSplit.length > 2) { + final String[] urlSplitByLibrary = LIBRARY_SPLIT_PATTERN.split(url); + if (urlSplitByLibrary.length > 2) { throw new IllegalArgumentException( "Invalid url, Library.url SHALL be /Library/"); } - String cqlName = urlSplit.length == 1 ? urlSplit[0] : urlSplit[1]; + final String cqlName = urlSplitByLibrary.length == 1 ? urlSplitByLibrary[0] : urlSplitByLibrary[1]; VersionedIdentifier versionedIdentifier = new VersionedIdentifier(); if (cqlName.contains("|")) { String[] nameVersion = cqlName.split("\\|"); diff --git a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/EnginesNpmLibraryHandler.java b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/EnginesNpmLibraryHandler.java new file mode 100644 index 0000000000..3906fe76ea --- /dev/null +++ b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/EnginesNpmLibraryHandler.java @@ -0,0 +1,28 @@ +package org.opencds.cqf.fhir.cql.npm; + +import org.cqframework.cql.cql2elm.LibraryManager; +import org.cqframework.cql.cql2elm.ModelManager; +import org.opencds.cqf.fhir.cql.Engines; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; + +/** + * Convenience class to extend {@link Engines} to handle NPM package specific logic. + */ +public class EnginesNpmLibraryHandler { + + private EnginesNpmLibraryHandler() { + // private constructor + } + + public static void registerNpmPackageLoader( + LibraryManager libraryManager, ModelManager modelManager, NpmPackageLoader npmPackageLoader) { + + npmPackageLoader.initNamespaceMappings(libraryManager); + + var loader = libraryManager.getLibrarySourceLoader(); + + loader.registerProvider(new NpmLibraryProvider(npmPackageLoader)); + + modelManager.getModelInfoLoader().registerModelInfoProvider(new NpmModelInfoProvider(npmPackageLoader)); + } +} diff --git a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/NpmLibraryProvider.java b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/NpmLibraryProvider.java new file mode 100644 index 0000000000..1474054fe7 --- /dev/null +++ b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/NpmLibraryProvider.java @@ -0,0 +1,64 @@ +package org.opencds.cqf.fhir.cql.npm; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Optional; +import java.util.function.Function; +import org.cqframework.cql.cql2elm.LibrarySourceProvider; +import org.hl7.elm.r1.VersionedIdentifier; +import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; +import org.opencds.cqf.fhir.utility.adapter.IAttachmentAdapter; +import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link LibrarySourceProvider} to provide a CQL Library Stream from an NPM package. + */ +public class NpmLibraryProvider implements LibrarySourceProvider { + + private static final Logger logger = LoggerFactory.getLogger(NpmLibraryProvider.class); + + private static final String TEXT_CQL = "text/cql"; + + private final NpmPackageLoader npmPackageLoader; + + public NpmLibraryProvider(NpmPackageLoader npmPackageLoader) { + this.npmPackageLoader = npmPackageLoader; + } + + @Override + @Nullable + public InputStream getLibrarySource(VersionedIdentifier versionedIdentifier) { + + var libraryInputStream = npmPackageLoader + .findMatchingLibrary(versionedIdentifier) + .map(this::findCqlAttachment) + .flatMap(Function.identity()) + .map(IAttachmentAdapter::getData) + .map(ByteArrayInputStream::new) + .orElse(null); + + if (libraryInputStream == null && NpmPackageLoader.DEFAULT != npmPackageLoader) { + logger.warn( + "ATTENTION! Non-NOOP NPM loader: Could not find CQL Library for identifier: {}", + versionedIdentifier); + } + + return libraryInputStream; + } + + @Nonnull + private Optional findCqlAttachment(ILibraryAdapter library) { + final IAdapterFactory adapterFactory = IAdapterFactory.forFhirVersion( + library.fhirContext().getVersion().getVersion()); + + return library.getContent().stream() + .map(adapterFactory::createAttachment) + .filter(attachment -> TEXT_CQL.equals(attachment.getContentType())) + .findFirst(); + } +} diff --git a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/NpmModelInfoProvider.java b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/NpmModelInfoProvider.java new file mode 100644 index 0000000000..29cd3fd99c --- /dev/null +++ b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/npm/NpmModelInfoProvider.java @@ -0,0 +1,54 @@ +package org.opencds.cqf.fhir.cql.npm; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.xml.bind.JAXB; +import java.io.ByteArrayInputStream; +import java.util.Optional; +import java.util.function.Function; +import org.hl7.cql.model.ModelIdentifier; +import org.hl7.cql.model.ModelInfoProvider; +import org.hl7.elm_modelinfo.r1.ModelInfo; +import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; +import org.opencds.cqf.fhir.utility.adapter.IAttachmentAdapter; +import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; + +/** + * {@link ModelInfoProvider} to provide a ELM ModelInfo from an NPM package. + */ +public class NpmModelInfoProvider implements ModelInfoProvider { + + private static final String APPLICATION_XML = "application/xml"; + + private final NpmPackageLoader npmPackageLoader; + + public NpmModelInfoProvider(NpmPackageLoader npmPackageLoader) { + this.npmPackageLoader = npmPackageLoader; + } + + @Override + @Nullable + public ModelInfo load(ModelIdentifier modelIdentifier) { + + return npmPackageLoader + .findMatchingLibrary(modelIdentifier) + .map(this::findElmXmlAttachment) + .flatMap(Function.identity()) + .map(IAttachmentAdapter::getData) + .map(ByteArrayInputStream::new) + .map(inputStream -> JAXB.unmarshal(inputStream, ModelInfo.class)) + .orElse(null); + } + + @Nonnull + private Optional findElmXmlAttachment(ILibraryAdapter library) { + final IAdapterFactory adapterFactory = IAdapterFactory.forFhirVersion( + library.fhirContext().getVersion().getVersion()); + + return library.getContent().stream() + .map(adapterFactory::createAttachment) + .filter(attachment -> APPLICATION_XML.equals(attachment.getContentType())) + .findFirst(); + } +} diff --git a/cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/EnginesTest.java b/cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/EnginesTest.java index f48af72171..6663b7fa09 100644 --- a/cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/EnginesTest.java +++ b/cqf-fhir-cql/src/test/java/org/opencds/cqf/fhir/cql/EnginesTest.java @@ -33,10 +33,12 @@ import org.junit.jupiter.api.Test; import org.opencds.cqf.cql.engine.data.SystemDataProvider; import org.opencds.cqf.cql.engine.execution.CqlEngine; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.engine.retrieve.FederatedDataProvider; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings; import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings; import org.opencds.cqf.fhir.utility.Constants; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,16 +50,20 @@ class EnginesTest { private static final Logger log = LoggerFactory.getLogger(EnginesTest.class); private InMemoryFhirRepository repository; + private EngineInitializationContext engineInitializationContext; @BeforeEach void beforeEach() { repository = new InMemoryFhirRepository(FhirContext.forR4Cached()); repository.update(new Patient().setId("pat1")); + + engineInitializationContext = + new EngineInitializationContext(repository, NpmPackageLoader.DEFAULT, EvaluationSettings.getDefault()); } @Test void defaultSettings() { - var engine = Engines.forRepository(repository); + var engine = Engines.forContext(engineInitializationContext); assertDataProviders(engine); } @@ -302,13 +308,12 @@ void additionalDataEmpty() { @Test void additionalDataEntry() { - var settings = EvaluationSettings.getDefault(); var bundleBuilder = new BundleBuilder(FhirContext.forR4Cached()); bundleBuilder.addTransactionCreateEntry(new Encounter().setId("en1")); var additionalData = bundleBuilder.getBundle(); - var engine = Engines.forRepository(repository, settings, additionalData); + var engine = Engines.forContext(engineInitializationContext, additionalData); assertNotNull(engine.getState()); @@ -389,11 +394,11 @@ private static List convert(Class clazz, Iterable + default Interval[@2020-01-01T00:00:00.0-06:00, @2021-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists (CrossPackageTarget."Encounter Finished") + """; + + private static final String EXPECTED_CQL_CROSS_TARGET = + """ + library opencds.crosspackagetarget.CrossPackageTarget version '0.3' + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + context Patient + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + """; + + @Test + void crossPackageLoadLibrary() throws IOException { + + var loader = setup(CROSS_PACKAGE_SOURCE_TGZ, CROSS_PACKAGE_TARGET_TGZ); + + var libraryProvider = new NpmLibraryProvider(loader); + + var versionedIdentifierSource = + new VersionedIdentifier().withSystem(CROSS_PACKAGE_SOURCE_URL).withId(CROSS_PACKAGE_SOURCE_ID); + + var librarySourceInputStream = libraryProvider.getLibrarySource(versionedIdentifierSource); + assertNotNull(librarySourceInputStream); + + var actualSourceCql = getStringFromInputStream(librarySourceInputStream); + + assertEquals(EXPECTED_CQL_CROSS_SOURCE, actualSourceCql); + + var versionedIdentifierTarget = + new VersionedIdentifier().withSystem(CROSS_PACKAGE_TARGET_URL).withId(CROSS_PACKAGE_TARGET_ID); + + var libraryTargetInputStream = libraryProvider.getLibrarySource(versionedIdentifierTarget); + + assertNotNull(libraryTargetInputStream); + + var actualTargetCql = getStringFromInputStream(libraryTargetInputStream); + + assertEquals(EXPECTED_CQL_CROSS_TARGET, actualTargetCql); + } + + @Nonnull + private NpmPackageLoaderInMemory setup(Path... tgzPaths) { + return NpmPackageLoaderInMemory.fromNpmPackageClasspath(getClass(), tgzPaths); + } + + @Nonnull + private String getStringFromInputStream(InputStream librarySourceInputStream) throws IOException { + return new String(librarySourceInputStream.readAllBytes(), StandardCharsets.UTF_8); + } +} diff --git a/cqf-fhir-cql/src/test/resources/org/opencds/cqf/fhir/cql/npm/crosspackagesource.tgz b/cqf-fhir-cql/src/test/resources/org/opencds/cqf/fhir/cql/npm/crosspackagesource.tgz new file mode 100644 index 0000000000..79954af46a Binary files /dev/null and b/cqf-fhir-cql/src/test/resources/org/opencds/cqf/fhir/cql/npm/crosspackagesource.tgz differ diff --git a/cqf-fhir-cql/src/test/resources/org/opencds/cqf/fhir/cql/npm/crosspackagetarget.tgz b/cqf-fhir-cql/src/test/resources/org/opencds/cqf/fhir/cql/npm/crosspackagetarget.tgz new file mode 100644 index 0000000000..080274ad5f Binary files /dev/null and b/cqf-fhir-cql/src/test/resources/org/opencds/cqf/fhir/cql/npm/crosspackagetarget.tgz differ diff --git a/cqf-fhir-cr-cli/src/main/java/org/opencds/cqf/fhir/cr/cli/command/CqlCommand.java b/cqf-fhir-cr-cli/src/main/java/org/opencds/cqf/fhir/cr/cli/command/CqlCommand.java index fd99cc3c2a..31ddf356b4 100644 --- a/cqf-fhir-cr-cli/src/main/java/org/opencds/cqf/fhir/cr/cli/command/CqlCommand.java +++ b/cqf-fhir-cr-cli/src/main/java/org/opencds/cqf/fhir/cr/cli/command/CqlCommand.java @@ -17,7 +17,9 @@ import org.hl7.elm.r1.VersionedIdentifier; import org.opencds.cqf.cql.engine.execution.EvaluationResult; import org.opencds.cqf.fhir.cql.Engines; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cr.cli.argument.CqlCommandArgument; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; @@ -80,7 +82,8 @@ public static Stream evaluate(CqlCommandArgument arguments) { Set expressions = arguments.content.expression != null ? Set.of(arguments.content.expression) : null; return arguments.parameters.context.stream().map(c -> { - var engine = Engines.forRepository(repository, evaluationSettings); + var engine = Engines.forContext( + new EngineInitializationContext(repository, NpmPackageLoader.DEFAULT, evaluationSettings)); if (arguments.content.cqlPath != null) { var provider = new DefaultLibrarySourceProvider(Path.of(arguments.content.cqlPath)); engine.getEnvironment() diff --git a/cqf-fhir-cr-cli/src/main/java/org/opencds/cqf/fhir/cr/cli/command/MeasureCommand.java b/cqf-fhir-cr-cli/src/main/java/org/opencds/cqf/fhir/cr/cli/command/MeasureCommand.java index 86a60e7db7..f57c336613 100644 --- a/cqf-fhir-cr-cli/src/main/java/org/opencds/cqf/fhir/cr/cli/command/MeasureCommand.java +++ b/cqf-fhir-cr-cli/src/main/java/org/opencds/cqf/fhir/cr/cli/command/MeasureCommand.java @@ -22,12 +22,15 @@ import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import org.opencds.cqf.cql.engine.execution.EvaluationResult; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cr.cli.argument.MeasureCommandArgument; import org.opencds.cqf.fhir.cr.cli.command.CqlCommand.SubjectAndResult; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureProcessor; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import picocli.CommandLine.ArgGroup; import picocli.CommandLine.Command; @@ -88,9 +91,11 @@ public static Stream evaluate(MeasureCommandArgument args) { var fhirContext = FhirContext.forCached(FhirVersionEnum.valueOf(cqlArgs.fhir.fhirVersion)); var parser = fhirContext.newJsonParser(); var resource = getMeasure(parser, args.measurePath, args.measureName); + var npmPackageLoader = NpmPackageLoader.DEFAULT; var processor = getR4MeasureProcessor( Utilities.createEvaluationSettings(cqlArgs.content.cqlPath, cqlArgs.hedisCompatibilityMode), - Utilities.createRepository(fhirContext, cqlArgs.fhir.terminologyUrl, cqlArgs.fhir.dataUrl)); + Utilities.createRepository(fhirContext, cqlArgs.fhir.terminologyUrl, cqlArgs.fhir.dataUrl), + npmPackageLoader); var start = args.periodStart != null ? LocalDate.parse(args.periodStart, DateTimeFormatter.ISO_LOCAL_DATE) @@ -131,13 +136,23 @@ private static Measure getMeasure(IParser parser, String measurePath, String mea @Nonnull private static R4MeasureProcessor getR4MeasureProcessor( - EvaluationSettings evaluationSettings, IRepository repository) { + EvaluationSettings evaluationSettings, IRepository repository, NpmPackageLoader npmPackageLoader) { MeasureEvaluationOptions evaluationOptions = new MeasureEvaluationOptions(); evaluationOptions.setApplyScoringSetMembership(false); evaluationOptions.setEvaluationSettings(evaluationSettings); - return new R4MeasureProcessor(repository, evaluationOptions, new MeasureProcessorUtils()); + return new R4MeasureProcessor( + repository, + new EngineInitializationContext(repository, NpmPackageLoader.DEFAULT, evaluationSettings), + evaluationOptions, + new MeasureProcessorUtils(), + getR4RepositoryOrNpmResourceProvider(repository, npmPackageLoader, evaluationSettings)); + } + + private static R4RepositoryOrNpmResourceProvider getR4RepositoryOrNpmResourceProvider( + IRepository repository, NpmPackageLoader npmPackageLoader, EvaluationSettings evaluationSettings) { + return new R4RepositoryOrNpmResourceProvider(repository, npmPackageLoader, evaluationSettings); } private void writeJsonToFile(String json, String patientId, Path path) { diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrProcessorConfig.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrProcessorConfig.java index 27071c6b5d..2f2c8a9e6f 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrProcessorConfig.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/CrProcessorConfig.java @@ -1,6 +1,8 @@ package org.opencds.cqf.fhir.cr.hapi.config; import ca.uhn.fhir.rest.api.server.IRepositoryFactory; +import java.util.Optional; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cr.activitydefinition.ActivityDefinitionProcessor; import org.opencds.cqf.fhir.cr.graphdefintion.GraphDefinitionProcessor; @@ -19,6 +21,8 @@ import org.opencds.cqf.fhir.cr.questionnaireresponse.QuestionnaireResponseProcessor; import org.opencds.cqf.fhir.cr.valueset.ValueSetProcessor; import org.opencds.cqf.fhir.utility.client.TerminologyServerClientSettings; +import org.opencds.cqf.fhir.utility.npm.NpmConfigDependencySubstitutor; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -27,38 +31,99 @@ public class CrProcessorConfig { @Bean IActivityDefinitionProcessorFactory activityDefinitionProcessorFactory( - IRepositoryFactory repositoryFactory, EvaluationSettings evaluationSettings) { - return rd -> new ActivityDefinitionProcessor(repositoryFactory.create(rd), evaluationSettings); + IRepositoryFactory repositoryFactory, + Optional optNpmPackageLoader, + EvaluationSettings evaluationSettings) { + + return requestDetails -> { + var repository = repositoryFactory.create(requestDetails); + + return new ActivityDefinitionProcessor( + repository, + evaluationSettings, + new EngineInitializationContext( + repository, + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optNpmPackageLoader), + evaluationSettings)); + }; } @Bean IPlanDefinitionProcessorFactory planDefinitionProcessorFactory( IRepositoryFactory repositoryFactory, + Optional optNpmPackageLoader, EvaluationSettings evaluationSettings, TerminologyServerClientSettings terminologyServerClientSettings) { - return rd -> new PlanDefinitionProcessor( - repositoryFactory.create(rd), evaluationSettings, terminologyServerClientSettings); + + return requestDetails -> { + var repository = repositoryFactory.create(requestDetails); + + return new PlanDefinitionProcessor( + repository, + evaluationSettings, + new EngineInitializationContext( + repository, + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optNpmPackageLoader), + evaluationSettings), + terminologyServerClientSettings); + }; } @Bean IQuestionnaireProcessorFactory questionnaireProcessorFactory( - IRepositoryFactory repositoryFactory, EvaluationSettings evaluationSettings) { - return rd -> new QuestionnaireProcessor(repositoryFactory.create(rd), evaluationSettings); + IRepositoryFactory repositoryFactory, + Optional optNpmPackageLoader, + EvaluationSettings evaluationSettings) { + + return requestDetails -> { + var repository = repositoryFactory.create(requestDetails); + + return new QuestionnaireProcessor( + repository, + evaluationSettings, + new EngineInitializationContext( + repository, + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optNpmPackageLoader), + evaluationSettings)); + }; } @Bean IQuestionnaireResponseProcessorFactory questionnaireResponseProcessorFactory( - IRepositoryFactory repositoryFactory, EvaluationSettings evaluationSettings) { - return rd -> new QuestionnaireResponseProcessor(repositoryFactory.create(rd), evaluationSettings); + IRepositoryFactory repositoryFactory, + Optional optNpmPackageLoader, + EvaluationSettings evaluationSettings) { + + return requestDetails -> { + var repository = repositoryFactory.create(requestDetails); + return new QuestionnaireResponseProcessor( + repository, + evaluationSettings, + new EngineInitializationContext( + repository, + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optNpmPackageLoader), + evaluationSettings)); + }; } @Bean ILibraryProcessorFactory libraryProcessorFactory( IRepositoryFactory repositoryFactory, + Optional optNpmPackageLoader, EvaluationSettings evaluationSettings, TerminologyServerClientSettings terminologyServerClientSettings) { - return rd -> - new LibraryProcessor(repositoryFactory.create(rd), evaluationSettings, terminologyServerClientSettings); + + return requestDetails -> { + var repository = repositoryFactory.create(requestDetails); + return new LibraryProcessor( + repository, + evaluationSettings, + new EngineInitializationContext( + repository, + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optNpmPackageLoader), + evaluationSettings), + terminologyServerClientSettings); + }; } @Bean @@ -81,8 +146,19 @@ IGraphDefinitionProcessorFactory graphDefinitionProcessorFactory( @Bean IGraphDefinitionApplyRequestBuilderFactory graphDefinitionApplyRequestBuilderFactory( - IRepositoryFactory repositoryFactory, EvaluationSettings evaluationSettings) { + IRepositoryFactory repositoryFactory, + Optional optNpmPackageLoader, + EvaluationSettings evaluationSettings) { - return rd -> new ApplyRequestBuilder(repositoryFactory.create(rd), evaluationSettings); + return requestDetails -> { + var repository = repositoryFactory.create(requestDetails); + return new ApplyRequestBuilder( + repository, + evaluationSettings, + new EngineInitializationContext( + repository, + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optNpmPackageLoader), + evaluationSettings)); + }; } } diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/dstu3/CrDstu3Config.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/dstu3/CrDstu3Config.java index 6582e851ce..937d788e5a 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/dstu3/CrDstu3Config.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/dstu3/CrDstu3Config.java @@ -6,6 +6,8 @@ import ca.uhn.fhir.rest.server.RestfulServer; import java.util.Arrays; import java.util.Map; +import java.util.Optional; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cr.hapi.config.CrBaseConfig; import org.opencds.cqf.fhir.cr.hapi.config.ProviderLoader; import org.opencds.cqf.fhir.cr.hapi.config.ProviderSelector; @@ -14,6 +16,8 @@ import org.opencds.cqf.fhir.cr.hapi.dstu3.measure.MeasureOperationsProvider; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.dstu3.Dstu3MeasureService; +import org.opencds.cqf.fhir.utility.npm.NpmConfigDependencySubstitutor; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -25,8 +29,19 @@ public class CrDstu3Config { @Bean IMeasureServiceFactory dstu3MeasureServiceFactory( - IRepositoryFactory repositoryFactory, MeasureEvaluationOptions pvaluationOptions) { - return rd -> new Dstu3MeasureService(repositoryFactory.create(rd), pvaluationOptions); + IRepositoryFactory repositoryFactory, + Optional optNpmPackageLoader, + MeasureEvaluationOptions evaluationOptions) { + return requestDetails -> { + var repository = repositoryFactory.create(requestDetails); + return new Dstu3MeasureService( + repository, + new EngineInitializationContext( + repository, + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optNpmPackageLoader), + evaluationOptions.getEvaluationSettings()), + evaluationOptions); + }; } @Bean diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/ApplyOperationConfig.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/ApplyOperationConfig.java index 3dc9ab180e..b7d36bd501 100644 --- a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/ApplyOperationConfig.java +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/config/r4/ApplyOperationConfig.java @@ -9,6 +9,7 @@ import org.opencds.cqf.fhir.cr.hapi.common.IGraphDefinitionProcessorFactory; import org.opencds.cqf.fhir.cr.hapi.common.IPlanDefinitionProcessorFactory; import org.opencds.cqf.fhir.cr.hapi.common.StringTimePeriodHandler; +import org.opencds.cqf.fhir.cr.hapi.config.CrBaseConfig; import org.opencds.cqf.fhir.cr.hapi.config.CrProcessorConfig; import org.opencds.cqf.fhir.cr.hapi.config.ProviderLoader; import org.opencds.cqf.fhir.cr.hapi.config.ProviderSelector; @@ -21,7 +22,7 @@ import org.springframework.context.annotation.Import; @Configuration -@Import(CrProcessorConfig.class) +@Import({CrProcessorConfig.class, CrBaseConfig.class}) public class ApplyOperationConfig { @Bean ActivityDefinitionApplyProvider r4ActivityDefinitionApplyProvider( 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 605e2c2cc0..d7fcc64a7f 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 @@ -6,6 +6,8 @@ import ca.uhn.fhir.rest.server.RestfulServer; import java.util.Arrays; import java.util.Map; +import java.util.Optional; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cr.cpg.r4.R4CqlExecutionService; import org.opencds.cqf.fhir.cr.crmi.R4ApproveService; @@ -27,6 +29,8 @@ import org.opencds.cqf.fhir.cr.hapi.r4.R4MeasureEvaluatorMultipleFactory; import org.opencds.cqf.fhir.cr.hapi.r4.R4MeasureEvaluatorSingleFactory; import org.opencds.cqf.fhir.cr.hapi.r4.R4MeasureServiceUtilsFactory; +import org.opencds.cqf.fhir.cr.hapi.r4.R4MultiMeasureServiceFactory; +import org.opencds.cqf.fhir.cr.hapi.r4.R4RepositoryOrNpmResourceProviderFactory; import org.opencds.cqf.fhir.cr.hapi.r4.cpg.CqlExecutionOperationProvider; import org.opencds.cqf.fhir.cr.hapi.r4.crmi.ApproveProvider; import org.opencds.cqf.fhir.cr.hapi.r4.crmi.DraftProvider; @@ -45,7 +49,10 @@ import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureService; import org.opencds.cqf.fhir.cr.measure.r4.R4MultiMeasureService; import org.opencds.cqf.fhir.cr.measure.r4.R4SubmitDataService; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; +import org.opencds.cqf.fhir.utility.npm.NpmConfigDependencySubstitutor; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -58,29 +65,67 @@ public class CrR4Config { @Bean R4MeasureEvaluatorSingleFactory r4MeasureServiceFactory( IRepositoryFactory repositoryFactory, + Optional optNpmPackageLoader, MeasureEvaluationOptions evaluationOptions, - MeasurePeriodValidator measurePeriodValidator) { - return rd -> new R4MeasureService(repositoryFactory.create(rd), evaluationOptions, measurePeriodValidator); + MeasurePeriodValidator measurePeriodValidator, + R4RepositoryOrNpmResourceProviderFactory r4RepositoryOrNpmResourceProviderFactory) { + return requestDetails -> { + var repository = repositoryFactory.create(requestDetails); + return new R4MeasureService( + repository, + new EngineInitializationContext( + repository, + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optNpmPackageLoader), + evaluationOptions.getEvaluationSettings()), + evaluationOptions, + measurePeriodValidator, + r4RepositoryOrNpmResourceProviderFactory.create(requestDetails)); + }; } @Bean R4MeasureEvaluatorMultipleFactory r4MeasureEvaluatorMultipleFactory( IRepositoryFactory repositoryFactory, + Optional optNpmPackageLoader, MeasureEvaluationOptions evaluationOptions, - MeasurePeriodValidator measurePeriodValidator) { - return rd -> new R4MultiMeasureService( - repositoryFactory.create(rd), evaluationOptions, rd.getFhirServerBase(), measurePeriodValidator); + MeasurePeriodValidator measurePeriodValidator, + R4RepositoryOrNpmResourceProviderFactory r4RepositoryOrNpmResourceProviderFactory) { + return requestDetails -> { + var repository = repositoryFactory.create(requestDetails); + return new R4MultiMeasureService( + repository, + new EngineInitializationContext( + repository, + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optNpmPackageLoader), + evaluationOptions.getEvaluationSettings()), + evaluationOptions, + requestDetails.getFhirServerBase(), + measurePeriodValidator, + r4RepositoryOrNpmResourceProviderFactory.create(requestDetails)); + }; } @Bean ISubmitDataProcessorFactory r4SubmitDataProcessorFactory(IRepositoryFactory repositoryFactory) { - return rd -> new R4SubmitDataService(repositoryFactory.create(rd)); + return requestDetails -> new R4SubmitDataService(repositoryFactory.create(requestDetails)); } @Bean ICqlExecutionServiceFactory r4CqlExecutionServiceFactory( - IRepositoryFactory repositoryFactory, EvaluationSettings evaluationSettings) { - return rd -> new R4CqlExecutionService(repositoryFactory.create(rd), evaluationSettings); + IRepositoryFactory repositoryFactory, + Optional optNpmPackageLoader, + EvaluationSettings evaluationSettings) { + + return requestDetails -> { + var repository = repositoryFactory.create(requestDetails); + return new R4CqlExecutionService( + repository, + evaluationSettings, + new EngineInitializationContext( + repository, + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optNpmPackageLoader), + evaluationSettings)); + }; } @Bean @@ -103,9 +148,20 @@ R4MeasureServiceUtilsFactory r4MeasureServiceUtilsFactory(IRepositoryFactory rep @Bean ICollectDataServiceFactory collectDataServiceFactory( IRepositoryFactory repositoryFactory, + Optional optNpmPackageLoader, MeasureEvaluationOptions measureEvaluationOptions, - R4MeasureServiceUtilsFactory r4MeasureServiceUtilsFactory) { - return rd -> new R4CollectDataService(repositoryFactory.create(rd), measureEvaluationOptions); + R4RepositoryOrNpmResourceProviderFactory r4RepositoryOrNpmResourceProviderFactory) { + return requestDetails -> { + var repository = repositoryFactory.create(requestDetails); + return new R4CollectDataService( + repository, + new EngineInitializationContext( + repository, + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optNpmPackageLoader), + measureEvaluationOptions.getEvaluationSettings()), + measureEvaluationOptions, + r4RepositoryOrNpmResourceProviderFactory.create(requestDetails)); + }; } @Bean @@ -123,15 +179,24 @@ IDataRequirementsServiceFactory dataRequirementsServiceFactory( @Bean ICareGapsServiceFactory careGapsServiceFactory( IRepositoryFactory repositoryFactory, + Optional optNpmPackageLoader, CareGapsProperties careGapsProperties, + R4MeasureServiceUtilsFactory r4MeasureServiceUtilsFactory, MeasureEvaluationOptions measureEvaluationOptions, - MeasurePeriodValidator measurePeriodValidator) { - return rd -> new R4CareGapsService( - careGapsProperties, - repositoryFactory.create(rd), - measureEvaluationOptions, - rd.getFhirServerBase(), - measurePeriodValidator); + MeasurePeriodValidator measurePeriodValidator, + R4MultiMeasureServiceFactory r4MultiMeasureServiceFactory, + R4RepositoryOrNpmResourceProviderFactory r4repositoryOrNpmResourceProviderFactory) { + return requestDetails -> { + var repository = repositoryFactory.create(requestDetails); + return new R4CareGapsService( + careGapsProperties, + repository, + r4MeasureServiceUtilsFactory.create(requestDetails), + measureEvaluationOptions, + requestDetails.getFhirServerBase(), + r4MultiMeasureServiceFactory.create(requestDetails), + r4repositoryOrNpmResourceProviderFactory.create(requestDetails)); + }; } @Bean @@ -205,4 +270,37 @@ public ProviderLoader r4PdLoader( return new ProviderLoader(restfulServer, applicationContext, selector); } + + @Bean + public R4RepositoryOrNpmResourceProviderFactory r4FhirOrNpmResourceProviderFactory( + IRepositoryFactory repositoryFactory, + Optional optPackageLoader, + EvaluationSettings evaluationSettings) { + return requestDetails -> new R4RepositoryOrNpmResourceProvider( + repositoryFactory.create(requestDetails), + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optPackageLoader), + evaluationSettings); + } + + @Bean + R4MultiMeasureServiceFactory r4MultiMeasureServiceFactory( + IRepositoryFactory repositoryFactory, + Optional optNpmPackageLoader, + MeasureEvaluationOptions evaluationOptions, + MeasurePeriodValidator measurePeriodValidator, + R4RepositoryOrNpmResourceProviderFactory r4RepositoryOrNpmResourceProviderFactory) { + return requestDetails -> { + var repository = repositoryFactory.create(requestDetails); + return new R4MultiMeasureService( + repository, + new EngineInitializationContext( + repository, + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optNpmPackageLoader), + evaluationOptions.getEvaluationSettings()), + evaluationOptions, + requestDetails.getFhirServerBase(), + measurePeriodValidator, + r4RepositoryOrNpmResourceProviderFactory.create(requestDetails)); + }; + } } diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/R4MultiMeasureServiceFactory.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/R4MultiMeasureServiceFactory.java new file mode 100644 index 0000000000..5dbc16515a --- /dev/null +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/R4MultiMeasureServiceFactory.java @@ -0,0 +1,18 @@ +/*- + * #%L + * Smile - Clinical Intelligence + * %% + * Copyright (C) 2024 - 2025 Smile Digital Health, Inc. + * %% + * All rights reserved. + * #L% + */ +package org.opencds.cqf.fhir.cr.hapi.r4; + +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.opencds.cqf.fhir.cr.measure.r4.R4MultiMeasureService; + +@FunctionalInterface +public interface R4MultiMeasureServiceFactory { + R4MultiMeasureService create(RequestDetails requestDetails); +} diff --git a/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/R4RepositoryOrNpmResourceProviderFactory.java b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/R4RepositoryOrNpmResourceProviderFactory.java new file mode 100644 index 0000000000..8f67c73b47 --- /dev/null +++ b/cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/R4RepositoryOrNpmResourceProviderFactory.java @@ -0,0 +1,12 @@ +package org.opencds.cqf.fhir.cr.hapi.r4; + +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; + +/** + * Factory to create an {@link R4RepositoryOrNpmResourceProvider} from a {@link RequestDetails} + */ +@FunctionalInterface +public interface R4RepositoryOrNpmResourceProviderFactory { + R4RepositoryOrNpmResourceProvider create(RequestDetails requestDetails); +} diff --git a/cqf-fhir-cr-spring/src/main/java/org/opencds/cqf/fhir/cr/spring/measure/MeasureConfiguration.java b/cqf-fhir-cr-spring/src/main/java/org/opencds/cqf/fhir/cr/spring/measure/MeasureConfiguration.java index 7e093c1377..c1f856b430 100644 --- a/cqf-fhir-cr-spring/src/main/java/org/opencds/cqf/fhir/cr/spring/measure/MeasureConfiguration.java +++ b/cqf-fhir-cr-spring/src/main/java/org/opencds/cqf/fhir/cr/spring/measure/MeasureConfiguration.java @@ -1,11 +1,16 @@ package org.opencds.cqf.fhir.cr.spring.measure; import ca.uhn.fhir.repository.IRepository; +import java.util.Optional; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; import org.opencds.cqf.fhir.cr.measure.common.SubjectProvider; import org.opencds.cqf.fhir.cr.measure.dstu3.Dstu3MeasureProcessor; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureProcessor; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; +import org.opencds.cqf.fhir.utility.npm.NpmConfigDependencySubstitutor; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -17,16 +22,34 @@ public class MeasureConfiguration { @Bean Dstu3MeasureProcessor dstu3MeasureProcessor( IRepository repository, + Optional optNpmPackageLoader, MeasureEvaluationOptions measureEvaluationOptions, SubjectProvider subjectProvider) { - return new Dstu3MeasureProcessor(repository, measureEvaluationOptions, subjectProvider); + return new Dstu3MeasureProcessor( + repository, + new EngineInitializationContext( + repository, + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optNpmPackageLoader), + measureEvaluationOptions.getEvaluationSettings()), + measureEvaluationOptions, + subjectProvider); } @Bean R4MeasureProcessor r4MeasureProcessor( IRepository repository, + Optional optNpmPackageLoader, MeasureEvaluationOptions measureEvaluationOptions, - MeasureProcessorUtils measureProcessorUtils) { - return new R4MeasureProcessor(repository, measureEvaluationOptions, measureProcessorUtils); + MeasureProcessorUtils measureProcessorUtils, + R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider) { + return new R4MeasureProcessor( + repository, + new EngineInitializationContext( + repository, + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(optNpmPackageLoader), + measureEvaluationOptions.getEvaluationSettings()), + measureEvaluationOptions, + measureProcessorUtils, + r4RepositoryOrNpmResourceProvider); } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/activitydefinition/ActivityDefinitionProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/activitydefinition/ActivityDefinitionProcessor.java index 5759094bdf..ef2641fae2 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/activitydefinition/ActivityDefinitionProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/activitydefinition/ActivityDefinitionProcessor.java @@ -15,6 +15,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.opencds.cqf.cql.engine.model.ModelResolver; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.ExtensionResolver; import org.opencds.cqf.fhir.cql.LibraryEngine; @@ -36,23 +37,30 @@ public class ActivityDefinitionProcessor implements IActivityDefinitionProcessor protected IApplyProcessor applyProcessor; protected IRequestResolverFactory requestResolverFactory; protected IRepository repository; + private EngineInitializationContext engineInitializationContext; protected ExtensionResolver extensionResolver; - public ActivityDefinitionProcessor(IRepository repository) { - this(repository, EvaluationSettings.getDefault()); + public ActivityDefinitionProcessor( + IRepository repository, EngineInitializationContext engineInitializationContext) { + this(repository, EvaluationSettings.getDefault(), engineInitializationContext); } - public ActivityDefinitionProcessor(IRepository repository, EvaluationSettings evaluationSettings) { - this(repository, evaluationSettings, null, null); + public ActivityDefinitionProcessor( + IRepository repository, + EvaluationSettings evaluationSettings, + EngineInitializationContext engineInitializationContext) { + this(repository, evaluationSettings, engineInitializationContext, null, null); } public ActivityDefinitionProcessor( IRepository repository, EvaluationSettings evaluationSettings, + EngineInitializationContext engineInitializationContext, IApplyProcessor applyProcessor, IRequestResolverFactory requestResolverFactory) { this.repository = requireNonNull(repository, "repository can not be null"); this.evaluationSettings = requireNonNull(evaluationSettings, "evaluationSettings can not be null"); + this.engineInitializationContext = engineInitializationContext; this.resourceResolver = new ResourceResolver("ActivityDefinition", this.repository); fhirVersion = repository.fhirContext().getVersion().getVersion(); modelResolver = FhirModelResolverCache.resolverForVersion(fhirVersion); @@ -159,7 +167,7 @@ public , R extends IBaseResource> IBaseResource settingContext, parameters, data, - new LibraryEngine(repository, evaluationSettings)); + new LibraryEngine(repository, evaluationSettings, engineInitializationContext)); } public , R extends IBaseResource> IBaseResource apply( diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/cpg/r4/R4CqlExecutionService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/cpg/r4/R4CqlExecutionService.java index 54182e803f..4f31bc803d 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/cpg/r4/R4CqlExecutionService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/cpg/r4/R4CqlExecutionService.java @@ -13,6 +13,7 @@ import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Parameters; import org.opencds.cqf.fhir.cql.Engines; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.cpg.CqlExecutionProcessor; @@ -22,10 +23,15 @@ public class R4CqlExecutionService { protected IRepository repository; protected EvaluationSettings evaluationSettings; + protected EngineInitializationContext engineInitializationContext; - public R4CqlExecutionService(IRepository repository, EvaluationSettings evaluationSettings) { + public R4CqlExecutionService( + IRepository repository, + EvaluationSettings evaluationSettings, + EngineInitializationContext engineInitializationContext) { this.repository = repository; this.evaluationSettings = evaluationSettings; + this.engineInitializationContext = engineInitializationContext; } // should use adapters to make this version agnostic @@ -62,7 +68,7 @@ public Parameters evaluate( repository = Repositories.proxy( repository, useServerData.booleanValue(), dataEndpoint, contentEndpoint, terminologyEndpoint); } - var libraryEngine = new LibraryEngine(repository, this.evaluationSettings); + var libraryEngine = new LibraryEngine(repository, this.evaluationSettings, engineInitializationContext); var libraries = baseCqlExecutionProcessor.resolveIncludedLibraries(library); @@ -79,7 +85,7 @@ public Parameters evaluate( null); } - var engine = Engines.forRepository(repository, evaluationSettings, null); + var engine = Engines.forContext(engineInitializationContext.withRepository(repository), null); var libraryManager = engine.getEnvironment().getLibraryManager(); var libraryIdentifier = baseCqlExecutionProcessor.resolveLibraryIdentifier(content, null, libraryManager); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/cpg/r4/R4LibraryEvaluationService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/cpg/r4/R4LibraryEvaluationService.java index e7ea6e9b06..c0c391b915 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/cpg/r4/R4LibraryEvaluationService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/cpg/r4/R4LibraryEvaluationService.java @@ -14,6 +14,7 @@ import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Parameters; import org.opencds.cqf.fhir.cql.Engines; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.cpg.CqlExecutionProcessor; @@ -21,12 +22,17 @@ public class R4LibraryEvaluationService { - protected IRepository repository; - protected EvaluationSettings evaluationSettings; + protected final IRepository repository; + protected final EvaluationSettings evaluationSettings; + protected final EngineInitializationContext engineInitializationContext; - public R4LibraryEvaluationService(IRepository repository, EvaluationSettings evaluationSettings) { + public R4LibraryEvaluationService( + IRepository repository, + EvaluationSettings evaluationSettings, + EngineInitializationContext engineInitializationContext) { this.repository = repository; this.evaluationSettings = evaluationSettings; + this.engineInitializationContext = engineInitializationContext; } public Parameters evaluate( @@ -47,12 +53,19 @@ public Parameters evaluate( baseCqlExecutionProcessor.createIssue("warning", "prefetchData is not yet supported", repository))); } + final IRepository repositoryToUse; + final EngineInitializationContext engineInitializationContextToUse; if (contentEndpoint != null) { - repository = Repositories.proxy(repository, true, dataEndpoint, contentEndpoint, terminologyEndpoint); + repositoryToUse = Repositories.proxy(repository, true, dataEndpoint, contentEndpoint, terminologyEndpoint); + engineInitializationContextToUse = engineInitializationContext.withRepository(repositoryToUse); + } else { + repositoryToUse = repository; + engineInitializationContextToUse = engineInitializationContext; } - var libraryEngine = new LibraryEngine(repository, this.evaluationSettings); + var libraryEngine = + new LibraryEngine(repositoryToUse, this.evaluationSettings, engineInitializationContextToUse); var library = repository.read(Library.class, id); - var engine = Engines.forRepository(repository, evaluationSettings, data); + var engine = Engines.forContext(engineInitializationContextToUse, data); var libraryManager = engine.getEnvironment().getLibraryManager(); var libraryIdentifier = baseCqlExecutionProcessor.resolveLibraryIdentifier(null, library, libraryManager); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/graphdefintion/apply/ApplyRequestBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/graphdefintion/apply/ApplyRequestBuilder.java index 02bdab3f87..e8fd036add 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/graphdefintion/apply/ApplyRequestBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/graphdefintion/apply/ApplyRequestBuilder.java @@ -18,6 +18,7 @@ import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; import org.opencds.cqf.cql.engine.model.ModelResolver; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.common.ResourceResolver; @@ -32,6 +33,7 @@ public class ApplyRequestBuilder { private IRepository repository; private final EvaluationSettings evaluationSettings; + private final EngineInitializationContext engineInitializationContext; private final FhirVersionEnum fhirVersion; private GraphDefinition graphDefinition; private String subject; @@ -55,10 +57,14 @@ public class ApplyRequestBuilder { private ZonedDateTime periodEndString; private IPrimitiveType canonicalType; - public ApplyRequestBuilder(IRepository repository, EvaluationSettings evaluationSettings) { + public ApplyRequestBuilder( + IRepository repository, + EvaluationSettings evaluationSettings, + EngineInitializationContext engineInitializationContext) { this.repository = repository; this.fhirVersion = repository.fhirContext().getVersion().getVersion(); this.evaluationSettings = evaluationSettings; + this.engineInitializationContext = engineInitializationContext; } public ApplyRequestBuilder withGraphDefinitionId(IdType id) { @@ -206,7 +212,7 @@ public ApplyRequest buildApplyRequest() { IBaseResource resolvedGraphDefinition = new ResourceResolver("GraphDefinition", this.repository).resolve(eitherGraphDefinition); - LibraryEngine libraryEngine = new LibraryEngine(this.repository, this.evaluationSettings); + LibraryEngine libraryEngine = new LibraryEngine(repository, evaluationSettings, engineInitializationContext); ModelResolver modelResolver = FhirModelResolverCache.resolverForVersion(this.fhirVersion); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/library/LibraryProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/library/LibraryProcessor.java index 4304a2123a..3583a1ef9a 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/library/LibraryProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/library/LibraryProcessor.java @@ -16,6 +16,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.opencds.cqf.cql.engine.model.ModelResolver; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.common.DataRequirementsProcessor; @@ -33,6 +34,7 @@ import org.opencds.cqf.fhir.utility.model.FhirModelResolverCache; import org.opencds.cqf.fhir.utility.monad.Either3; +@SuppressWarnings("squid:S107") public class LibraryProcessor { protected final ModelResolver modelResolver; protected final FhirVersionEnum fhirVersion; @@ -43,21 +45,36 @@ public class LibraryProcessor { protected IRepository repository; protected EvaluationSettings evaluationSettings; protected TerminologyServerClientSettings terminologyServerClientSettings; + protected EngineInitializationContext engineInitializationContext; - public LibraryProcessor(IRepository repository) { - this(repository, EvaluationSettings.getDefault(), new TerminologyServerClientSettings()); + public LibraryProcessor(IRepository repository, EngineInitializationContext engineInitializationContext) { + this( + repository, + EvaluationSettings.getDefault(), + engineInitializationContext, + new TerminologyServerClientSettings()); } public LibraryProcessor( IRepository repository, EvaluationSettings evaluationSettings, + EngineInitializationContext engineInitializationContext, TerminologyServerClientSettings terminologyServerClientSettings) { - this(repository, evaluationSettings, terminologyServerClientSettings, null, null, null, null); + this( + repository, + evaluationSettings, + engineInitializationContext, + terminologyServerClientSettings, + null, + null, + null, + null); } public LibraryProcessor( IRepository repository, EvaluationSettings evaluationSettings, + EngineInitializationContext engineInitializationContext, TerminologyServerClientSettings terminologyServerClientSettings, IPackageProcessor packageProcessor, IReleaseProcessor releaseProcessor, @@ -65,6 +82,7 @@ public LibraryProcessor( IEvaluateProcessor evaluateProcessor) { this.repository = requireNonNull(repository, "repository can not be null"); this.evaluationSettings = requireNonNull(evaluationSettings, "evaluationSettings can not be null"); + this.engineInitializationContext = engineInitializationContext; this.terminologyServerClientSettings = requireNonNull(terminologyServerClientSettings, "terminologyServerClientSettings can not be null"); fhirVersion = this.repository.fhirContext().getVersion().getVersion(); @@ -192,7 +210,7 @@ public , R extends IBaseResource> IBaseParamete parameters, data, prefetchData, - new LibraryEngine(repository, this.evaluationSettings)); + new LibraryEngine(repository, evaluationSettings, engineInitializationContext)); } public , R extends IBaseResource> IBaseParameters evaluate( diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java index 3b569c79a5..75a65996b3 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureProcessor.java @@ -24,6 +24,7 @@ import org.opencds.cqf.cql.engine.fhir.model.Dstu3FhirModelResolver; import org.opencds.cqf.cql.engine.runtime.Interval; import org.opencds.cqf.fhir.cql.Engines; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; @@ -37,19 +38,25 @@ @SuppressWarnings("squid:S1135") public class Dstu3MeasureProcessor { private final IRepository repository; + private final EngineInitializationContext engineInitializationContext; private final MeasureEvaluationOptions measureEvaluationOptions; private final SubjectProvider subjectProvider; private final MeasureProcessorUtils measureProcessorUtils = new MeasureProcessorUtils(); - public Dstu3MeasureProcessor(IRepository repository, MeasureEvaluationOptions measureEvaluationOptions) { - this(repository, measureEvaluationOptions, new Dstu3RepositorySubjectProvider()); + public Dstu3MeasureProcessor( + IRepository repository, + EngineInitializationContext engineInitializationContext, + MeasureEvaluationOptions measureEvaluationOptions) { + this(repository, engineInitializationContext, measureEvaluationOptions, new Dstu3RepositorySubjectProvider()); } public Dstu3MeasureProcessor( IRepository repository, + EngineInitializationContext engineInitializationContext, MeasureEvaluationOptions measureEvaluationOptions, SubjectProvider subjectProvider) { this.repository = Objects.requireNonNull(repository); + this.engineInitializationContext = engineInitializationContext; this.measureEvaluationOptions = measureEvaluationOptions != null ? measureEvaluationOptions : MeasureEvaluationOptions.defaultOptions(); this.subjectProvider = subjectProvider; @@ -94,8 +101,7 @@ protected MeasureReport evaluateMeasure( } var subjects = subjectProvider.getSubjects(actualRepo, subjectIds).toList(); var evalType = getMeasureEvalType(reportType, subjects); - var context = Engines.forRepository( - this.repository, this.measureEvaluationOptions.getEvaluationSettings(), additionalData); + var context = Engines.forContext(engineInitializationContext, additionalData); // Note that we must build the LibraryEngine BEFORE we call // measureProcessorUtils.setMeasurementPeriod(), otherwise, we get an NPE. @@ -184,7 +190,8 @@ protected LibraryEngine getLibraryEngine(Parameters parameters, VersionedIdentif } } - return new LibraryEngine(repository, this.measureEvaluationOptions.getEvaluationSettings()); + return new LibraryEngine( + repository, this.measureEvaluationOptions.getEvaluationSettings(), engineInitializationContext); } private VersionedIdentifier getLibraryVersionIdentifier(Measure measure) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureService.java index 285b84f71a..b2cb12d197 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/dstu3/Dstu3MeasureService.java @@ -25,14 +25,20 @@ import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.dstu3.model.SearchParameter; import org.hl7.fhir.dstu3.model.StringType; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; public class Dstu3MeasureService implements Dstu3MeasureEvaluatorSingle { private final IRepository repository; + private final EngineInitializationContext engineInitializationContext; private final MeasureEvaluationOptions measureEvaluationOptions; - public Dstu3MeasureService(IRepository repository, MeasureEvaluationOptions measureEvaluationOptions) { + public Dstu3MeasureService( + IRepository repository, + EngineInitializationContext engineInitializationContext, + MeasureEvaluationOptions measureEvaluationOptions) { this.repository = repository; + this.engineInitializationContext = engineInitializationContext; this.measureEvaluationOptions = measureEvaluationOptions; } @@ -109,7 +115,8 @@ public MeasureReport evaluateMeasure( ensureSupplementalDataElementSearchParameter(); - var dstu3MeasureProcessor = new Dstu3MeasureProcessor(repository, measureEvaluationOptions); + var dstu3MeasureProcessor = + new Dstu3MeasureProcessor(repository, engineInitializationContext, measureEvaluationOptions); MeasureReport report = dstu3MeasureProcessor.evaluateMeasure( id, periodStart, periodEnd, reportType, Collections.singletonList(subject), additionalData, parameters); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsBundleBuilder.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsBundleBuilder.java index edb2ac2abd..0af47a095a 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsBundleBuilder.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsBundleBuilder.java @@ -44,11 +44,9 @@ import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.StringType; import org.opencds.cqf.fhir.cr.measure.CareGapsProperties; -import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; -import org.opencds.cqf.fhir.cr.measure.common.MeasurePeriodValidator; import org.opencds.cqf.fhir.cr.measure.enumeration.CareGapsStatusCode; -import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.Resources; import org.opencds.cqf.fhir.utility.builder.BundleBuilder; @@ -61,7 +59,7 @@ /** * Care Gaps Bundle Builder houses the logic for constructing a Care-Gaps Document Bundle for a Patient per Measures requested */ -@SuppressWarnings("UnstableApiUsage") +@SuppressWarnings({"UnstableApiUsage", "squid:S125"}) public class R4CareGapsBundleBuilder { private static final Map CARE_GAPS_CODES = ImmutableMap.of( "http://loinc.org/96315-7", @@ -74,24 +72,22 @@ public class R4CareGapsBundleBuilder { private static final FhirContext fhirContext = FhirContext.forCached(FhirVersionEnum.R4); private final CareGapsProperties careGapsProperties; private final String serverBase; - private final R4MeasureServiceUtils r4MeasureServiceUtils; private final R4MultiMeasureService r4MultiMeasureService; + private final R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider; public R4CareGapsBundleBuilder( CareGapsProperties careGapsProperties, IRepository repository, - MeasureEvaluationOptions measureEvaluationOptions, String serverBase, Map configuredResources, - MeasurePeriodValidator measurePeriodValidator) { + R4MultiMeasureService r4MultiMeasureService, + R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider) { this.repository = repository; this.careGapsProperties = careGapsProperties; this.serverBase = serverBase; this.configuredResources = configuredResources; - - r4MeasureServiceUtils = new R4MeasureServiceUtils(repository); - r4MultiMeasureService = - new R4MultiMeasureService(repository, measureEvaluationOptions, serverBase, measurePeriodValidator); + this.r4RepositoryOrNpmResourceProvider = r4RepositoryOrNpmResourceProvider; + this.r4MultiMeasureService = r4MultiMeasureService; } public List makePatientBundles( @@ -160,7 +156,7 @@ public Bundle makePatientBundle( MeasureReport mr = (MeasureReport) entry.getResource(); addProfile(mr); addResourceId(mr); - Measure measure = r4MeasureServiceUtils.resolveByUrl(mr.getMeasure()); + Measure measure = r4RepositoryOrNpmResourceProvider.resolveByUrl(mr.getMeasure()); // Applicable Reports per Gap-Status var gapStatus = gapEvaluator.getGroupGapStatus(measure, mr); var filteredGapStatus = filteredGapStatus(gapStatus, statuses); @@ -319,30 +315,36 @@ private void populateSDEResources(MeasureReport measureReport, Map resourceType = fhirContext - .getResourceDefinition(sdeId.getResourceType()) - .newInstance() - .getClass(); - IBaseResource resource = repository.read(resourceType, sdeId); - if (resource instanceof Resource resourceBase) { - resources.put(Ids.simple(sdeId), resourceBase); - } - } - } + handleSdeExtensions(resources, extension); } } } } + private void handleSdeExtensions(Map resources, Extension extension) { + Reference sdeRef = extension.hasValue() && extension.getValue() instanceof Reference extensionReference + ? extensionReference + : null; + if (sdeRef != null && sdeRef.hasReference() && !sdeRef.getReference().startsWith("#")) { + handleSdeReferences(resources, sdeRef); + } + } + + private void handleSdeReferences(Map resources, Reference sdeRef) { + // sde reference comes in format [ResourceType]/{id} + IdType sdeId = new IdType(sdeRef.getReference()); + if (!resources.containsKey(Ids.simple(sdeId))) { + Class resourceType = fhirContext + .getResourceDefinition(sdeId.getResourceType()) + .newInstance() + .getClass(); + IBaseResource resource = repository.read(resourceType, sdeId); + if (resource instanceof Resource resourceBase) { + resources.put(Ids.simple(sdeId), resourceBase); + } + } + } + private Bundle makeNewBundle() { return new BundleBuilder<>(Bundle.class) .withProfile(CARE_GAPS_BUNDLE_PROFILE) diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsProcessor.java index d7443aa9a4..2b695d8a5a 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsProcessor.java @@ -25,10 +25,10 @@ import org.opencds.cqf.fhir.cr.measure.CareGapsProperties; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.GroupDef; -import org.opencds.cqf.fhir.cr.measure.common.MeasurePeriodValidator; import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.constant.CareGapsConstants; import org.opencds.cqf.fhir.cr.measure.enumeration.CareGapsStatusCode; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; import org.opencds.cqf.fhir.utility.monad.Either3; import org.slf4j.Logger; @@ -46,24 +46,28 @@ public class R4CareGapsProcessor implements R4CareGapsProcessorInterface { private final R4MeasureServiceUtils r4MeasureServiceUtils; private final R4CareGapsBundleBuilder r4CareGapsBundleBuilder; private final R4RepositorySubjectProvider subjectProvider; + private final R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider; public R4CareGapsProcessor( CareGapsProperties careGapsProperties, IRepository repository, + R4MeasureServiceUtils r4MeasureServiceUtils, MeasureEvaluationOptions measureEvaluationOptions, String serverBase, - MeasurePeriodValidator measurePeriodValidator) { + R4MultiMeasureService r4MultiMeasureService, + R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider) { this.repository = repository; this.careGapsProperties = careGapsProperties; + this.r4MeasureServiceUtils = r4MeasureServiceUtils; + this.r4RepositoryOrNpmResourceProvider = r4RepositoryOrNpmResourceProvider; - r4MeasureServiceUtils = new R4MeasureServiceUtils(repository); r4CareGapsBundleBuilder = new R4CareGapsBundleBuilder( careGapsProperties, repository, - measureEvaluationOptions, serverBase, configuredResources, - measurePeriodValidator); + r4MultiMeasureService, + this.r4RepositoryOrNpmResourceProvider); subjectProvider = new R4RepositorySubjectProvider(measureEvaluationOptions.getSubjectProviderOptions()); } @@ -124,13 +128,10 @@ public R4CareGapsParameters setCareGapParameters( } @Override - public List resolveMeasure(List> measure) { - return measure.stream() - .map(x -> x.fold( - id -> repository.read(Measure.class, id), - r4MeasureServiceUtils::resolveByIdentifier, - canonical -> r4MeasureServiceUtils.resolveByUrl(canonical.asStringValue()))) - .collect(Collectors.toList()); + public List resolveMeasure(List> measureEithers) { + return this.r4RepositoryOrNpmResourceProvider + .foldMeasureEithers(measureEithers) + .getMeasures(); } @Override diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsService.java index 9ee34ef490..f7fa737c36 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CareGapsService.java @@ -9,13 +9,13 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.stream.Collectors; import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Parameters; import org.opencds.cqf.fhir.cr.measure.CareGapsProperties; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; -import org.opencds.cqf.fhir.cr.measure.common.MeasurePeriodValidator; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; +import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; import org.opencds.cqf.fhir.utility.monad.Either3; import org.opencds.cqf.fhir.utility.monad.Eithers; @@ -28,12 +28,20 @@ public class R4CareGapsService implements R4CareGapsServiceInterface { public R4CareGapsService( CareGapsProperties careGapsProperties, IRepository repository, + R4MeasureServiceUtils r4MeasureServiceUtils, MeasureEvaluationOptions measureEvaluationOptions, String serverBase, - MeasurePeriodValidator measurePeriodEvalutator) { + R4MultiMeasureService r4MultiMeasureService, + R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider) { r4CareGapsProcessor = new R4CareGapsProcessor( - careGapsProperties, repository, measureEvaluationOptions, serverBase, measurePeriodEvalutator); + careGapsProperties, + repository, + r4MeasureServiceUtils, + measureEvaluationOptions, + serverBase, + r4MultiMeasureService, + r4RepositoryOrNpmResourceProvider); } /** @@ -93,7 +101,7 @@ public List> liftMeasureParameters( if (eitherList.isEmpty()) { final List measureIdsAsStrings = Optional.ofNullable(measureId) .map(nonNullMeasureId -> - nonNullMeasureId.stream().map(IdType::getIdPart).collect(Collectors.toList())) + nonNullMeasureId.stream().map(IdType::getIdPart).toList()) .orElse(Collections.emptyList()); throw new InvalidRequestException( diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CollectDataService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CollectDataService.java index 46b3a0aa75..d20f516824 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CollectDataService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4CollectDataService.java @@ -17,25 +17,34 @@ import org.hl7.fhir.r4.model.Resource; import org.opencds.cqf.cql.engine.execution.CqlEngine; import org.opencds.cqf.fhir.cql.Engines; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.CompositeEvaluationResultsPerMeasure; import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; -import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.monad.Eithers; @SuppressWarnings("squid:S107") public class R4CollectDataService { private final IRepository repository; + private final EngineInitializationContext engineInitializationContext; private final MeasureEvaluationOptions measureEvaluationOptions; private final R4RepositorySubjectProvider subjectProvider; private final MeasureProcessorUtils measureProcessorUtils = new MeasureProcessorUtils(); + private final R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider; - public R4CollectDataService(IRepository repository, MeasureEvaluationOptions measureEvaluationOptions) { + public R4CollectDataService( + IRepository repository, + EngineInitializationContext engineInitializationContext, + MeasureEvaluationOptions measureEvaluationOptions, + R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider) { this.repository = repository; + this.engineInitializationContext = engineInitializationContext; this.measureEvaluationOptions = measureEvaluationOptions; this.subjectProvider = new R4RepositorySubjectProvider(measureEvaluationOptions.getSubjectProviderOptions()); + this.r4RepositoryOrNpmResourceProvider = r4RepositoryOrNpmResourceProvider; } /** @@ -66,15 +75,18 @@ public Parameters collectData( String practitioner) { Parameters parameters = new Parameters(); - var processor = - new R4MeasureProcessor(this.repository, this.measureEvaluationOptions, this.measureProcessorUtils); + var processor = new R4MeasureProcessor( + this.repository, + this.engineInitializationContext, + this.measureEvaluationOptions, + this.measureProcessorUtils, + this.r4RepositoryOrNpmResourceProvider); List subjectList = getSubjects(subject, practitioner, subjectProvider); - var context = - Engines.forRepository(this.repository, this.measureEvaluationOptions.getEvaluationSettings(), null); + var context = Engines.forContext(engineInitializationContext); - var foldedMeasure = R4MeasureServiceUtils.foldMeasure(Eithers.forMiddle3(measureId), repository); + var foldedMeasure = r4RepositoryOrNpmResourceProvider.foldMeasure(Eithers.forMiddle3(measureId)); if (!subjectList.isEmpty()) { for (String patient : subjectList) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java index 3056735564..667e860600 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessor.java @@ -27,11 +27,11 @@ import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.Resource; import org.opencds.cqf.cql.engine.execution.CqlEngine; import org.opencds.cqf.cql.engine.execution.EvaluationResult; import org.opencds.cqf.cql.engine.fhir.model.R4FhirModelResolver; import org.opencds.cqf.cql.engine.runtime.Interval; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cql.VersionedIdentifiers; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; @@ -44,25 +44,33 @@ import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType; import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.common.MultiLibraryIdMeasureEngineDetails; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; import org.opencds.cqf.fhir.cr.measure.r4.utils.R4DateHelper; -import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; import org.opencds.cqf.fhir.utility.monad.Either3; +import org.opencds.cqf.fhir.utility.npm.MeasureOrNpmResourceHolder; +import org.opencds.cqf.fhir.utility.npm.MeasureOrNpmResourceHolderList; import org.opencds.cqf.fhir.utility.search.Searches; public class R4MeasureProcessor { private final IRepository repository; + private final EngineInitializationContext engineInitializationContext; private final MeasureEvaluationOptions measureEvaluationOptions; private final MeasureProcessorUtils measureProcessorUtils; + private final R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider; public R4MeasureProcessor( IRepository repository, + EngineInitializationContext engineInitializationContext, MeasureEvaluationOptions measureEvaluationOptions, - MeasureProcessorUtils measureProcessorUtils) { + MeasureProcessorUtils measureProcessorUtils, + R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider) { this.repository = Objects.requireNonNull(repository); + this.engineInitializationContext = engineInitializationContext; this.measureEvaluationOptions = measureEvaluationOptions != null ? measureEvaluationOptions : MeasureEvaluationOptions.defaultOptions(); this.measureProcessorUtils = measureProcessorUtils; + this.r4RepositoryOrNpmResourceProvider = r4RepositoryOrNpmResourceProvider; } // Expose this so CQL measure evaluation can use the same Repository as the one passed to the @@ -80,8 +88,11 @@ public MeasureReport evaluateMeasure( MeasureEvalType evalType, CqlEngine context, CompositeEvaluationResultsPerMeasure compositeEvaluationResultsPerMeasure) { + + var measureOrNpmResourceHolder = r4RepositoryOrNpmResourceProvider.foldMeasure(measure); + return this.evaluateMeasure( - R4MeasureServiceUtils.foldMeasure(measure, this.repository), + measureOrNpmResourceHolder, periodStart, periodEnd, reportType, @@ -142,7 +153,7 @@ public MeasureReport evaluateMeasureResults( /** * Evaluation method that generates CQL results, Processes results, builds Measure Report - * @param measure Measure resource + * @param measureOrNpmResourceHolder Measure resource or NPM resource holder * @param periodStart start date of Measurement Period * @param periodEnd end date of Measurement Period * @param reportType type of report that defines MeasureReport Type @@ -151,7 +162,7 @@ public MeasureReport evaluateMeasureResults( * @return Measure Report resource */ public MeasureReport evaluateMeasure( - Measure measure, + MeasureOrNpmResourceHolder measureOrNpmResourceHolder, @Nullable ZonedDateTime periodStart, @Nullable ZonedDateTime periodEnd, String reportType, @@ -162,6 +173,8 @@ public MeasureReport evaluateMeasure( MeasureEvalType evaluationType = measureProcessorUtils.getEvalType(evalType, reportType, subjectIds); + var measure = measureOrNpmResourceHolder.getMeasure(); + // setup MeasureDef var measureDef = new R4MeasureDefBuilder().build(measure); @@ -179,7 +192,7 @@ public MeasureReport evaluateMeasure( new R4PopulationBasisValidator()); var measurementPeriod = postLibraryEvaluationPeriodProcessingAndContinuousVariableObservation( - measure, measureDef, periodStart, periodEnd, context); + measureOrNpmResourceHolder, measureDef, periodStart, periodEnd, context); // Build Measure Report with Results return new R4MeasureReportBuilder() @@ -201,14 +214,15 @@ public MeasureReport evaluateMeasure( * through good fortune before we didn't accidentally evaluate twice. */ private Interval postLibraryEvaluationPeriodProcessingAndContinuousVariableObservation( - Measure measure, + MeasureOrNpmResourceHolder measureOrNpmResourceHolder, MeasureDef measureDef, @Nullable ZonedDateTime periodStart, @Nullable ZonedDateTime periodEnd, CqlEngine context) { - var libraryVersionedIdentifiers = - getMultiLibraryIdMeasureEngineDetails(List.of(measure)).getLibraryIdentifiers(); + var libraryVersionedIdentifiers = getMultiLibraryIdMeasureEngineDetails( + MeasureOrNpmResourceHolderList.of(measureOrNpmResourceHolder)) + .getLibraryIdentifiers(); var compiledLibraries = getCompiledLibraries(libraryVersionedIdentifiers, context); @@ -224,7 +238,9 @@ private Interval postLibraryEvaluationPeriodProcessingAndContinuousVariableObser measureProcessorUtils.setMeasurementPeriod( measurementPeriodParams, context, - Optional.ofNullable(measure.getUrl()).map(List::of).orElse(List.of("Unknown Measure URL"))); + Optional.ofNullable(measureOrNpmResourceHolder.getMeasureUrl()) + .map(List::of) + .orElse(List.of("Unknown Measure URL"))); // DON'T pop the library off the stack yet, because we need it for continuousVariableObservation() @@ -241,94 +257,59 @@ private Interval postLibraryEvaluationPeriodProcessingAndContinuousVariableObser public CompositeEvaluationResultsPerMeasure evaluateMeasureWithCqlEngine( List subjects, - Either3 measureEither, - @Nullable ZonedDateTime periodStart, - @Nullable ZonedDateTime periodEnd, - Parameters parameters, - CqlEngine context) { - - return evaluateMultiMeasuresWithCqlEngine( - subjects, - List.of(R4MeasureServiceUtils.foldMeasure(measureEither, repository)), - periodStart, - periodEnd, - parameters, - context); - } - - public CompositeEvaluationResultsPerMeasure evaluateMeasureIdWithCqlEngine( - List subjects, - IIdType measureId, + MeasureOrNpmResourceHolder measureOrNpmResourceHolder, @Nullable ZonedDateTime periodStart, @Nullable ZonedDateTime periodEnd, Parameters parameters, CqlEngine context) { - return evaluateMultiMeasuresWithCqlEngine( + return evaluateMultiMeasuresPlusNpmHoldersWithCqlEngine( subjects, - List.of(R4MeasureServiceUtils.resolveById(measureId, repository)), + MeasureOrNpmResourceHolderList.of(measureOrNpmResourceHolder), periodStart, periodEnd, parameters, context); } - public CompositeEvaluationResultsPerMeasure evaluateMeasureWithCqlEngine( - List subjects, - Measure measure, - @Nullable ZonedDateTime periodStart, - @Nullable ZonedDateTime periodEnd, - Parameters parameters, - CqlEngine context) { - - return evaluateMultiMeasuresWithCqlEngine( - subjects, List.of(measure), periodStart, periodEnd, parameters, context); - } - - public CompositeEvaluationResultsPerMeasure evaluateMultiMeasureIdsWithCqlEngine( + public CompositeEvaluationResultsPerMeasure evaluateMultiMeasuresWithCqlEngine( List subjects, - List measureIds, + MeasureOrNpmResourceHolderList measureResourcesOrHolderList, @Nullable ZonedDateTime periodStart, @Nullable ZonedDateTime periodEnd, Parameters parameters, CqlEngine context) { - return evaluateMultiMeasuresWithCqlEngine( - subjects, - measureIds.stream() - .map(IIdType::toUnqualifiedVersionless) - .map(id -> R4MeasureServiceUtils.resolveById(id, repository)) - .toList(), - periodStart, - periodEnd, - parameters, - context); + return evaluateMultiMeasuresPlusNpmHoldersWithCqlEngine( + subjects, measureResourcesOrHolderList, periodStart, periodEnd, parameters, context); } - public CompositeEvaluationResultsPerMeasure evaluateMultiMeasuresWithCqlEngine( + CompositeEvaluationResultsPerMeasure evaluateMultiMeasuresPlusNpmHoldersWithCqlEngine( List subjects, - List measures, + MeasureOrNpmResourceHolderList measureOrNpmResourceHolderList, @Nullable ZonedDateTime periodStart, @Nullable ZonedDateTime periodEnd, Parameters parameters, CqlEngine context) { - measures.forEach(this::checkMeasureLibrary); + measureOrNpmResourceHolderList.checkMeasureLibraries(); var measurementPeriodParams = buildMeasurementPeriod(periodStart, periodEnd); var zonedMeasurementPeriod = MeasureProcessorUtils.getZonedTimeZoneForEval( measureProcessorUtils.getDefaultMeasurementPeriod(measurementPeriodParams, context)); + var measures = measureOrNpmResourceHolderList.getMeasures(); + // Do this to be backwards compatible with the previous single-library evaluation: // Trigger first-pass validation on measure scoring as well as other aspects of the Measures R4MeasureDefBuilder.triggerFirstPassValidation(measures); // Note that we must build the LibraryEngine BEFORE we call // measureProcessorUtils.setMeasurementPeriod(), otherwise, we get an NPE. - var multiLibraryIdMeasureEngineDetails = getMultiLibraryIdMeasureEngineDetails(measures); + var multiLibraryIdMeasureEngineDetails = getMultiLibraryIdMeasureEngineDetails(measureOrNpmResourceHolderList); preLibraryEvaluationPeriodProcessing( multiLibraryIdMeasureEngineDetails.getLibraryIdentifiers(), - measures, + measureOrNpmResourceHolderList, parameters, context, measurementPeriodParams); @@ -352,7 +333,7 @@ public CompositeEvaluationResultsPerMeasure evaluateMultiMeasuresWithCqlEngine( */ private void preLibraryEvaluationPeriodProcessing( List libraryVersionedIdentifiers, - List measures, + MeasureOrNpmResourceHolderList measureOrNpmResourceHolderList, Parameters parameters, CqlEngine context, Interval measurementPeriodParams) { @@ -373,7 +354,7 @@ private void preLibraryEvaluationPeriodProcessing( measureProcessorUtils.setMeasurementPeriod( measurementPeriodParams, context, - measures.stream() + measureOrNpmResourceHolderList.getMeasures().stream() .map(Measure::getUrl) .map(url -> Optional.ofNullable(url).orElse("Unknown Measure URL")) .toList()); @@ -383,15 +364,16 @@ private void preLibraryEvaluationPeriodProcessing( popAllLibrariesFromCqlEngine(context, libraries); } - private MultiLibraryIdMeasureEngineDetails getMultiLibraryIdMeasureEngineDetails(List measures) { + private MultiLibraryIdMeasureEngineDetails getMultiLibraryIdMeasureEngineDetails( + MeasureOrNpmResourceHolderList measureOrNpmResourceHolderList) { - var libraryIdentifiersToMeasureIds = measures.stream() + var libraryIdentifiersToMeasureIds = measureOrNpmResourceHolderList.getMeasuresOrNpmResourceHolders().stream() .collect(ImmutableListMultimap.toImmutableListMultimap( - this::getLibraryVersionIdentifier, // Key function - Resource::getIdElement // Value function - )); + r4RepositoryOrNpmResourceProvider::getLibraryVersionIdentifier, // Key function + MeasureOrNpmResourceHolder::getMeasureIdElement)); - var libraryEngine = new LibraryEngine(repository, this.measureEvaluationOptions.getEvaluationSettings()); + var libraryEngine = new LibraryEngine( + repository, this.measureEvaluationOptions.getEvaluationSettings(), this.engineInitializationContext); var builder = MultiLibraryIdMeasureEngineDetails.builder(libraryEngine); @@ -478,7 +460,8 @@ protected LibraryEngine getLibraryEngine(Parameters parameters, VersionedIdentif setArgParameters(parameters, context, lib); - return new LibraryEngine(repository, this.measureEvaluationOptions.getEvaluationSettings()); + return new LibraryEngine( + repository, this.measureEvaluationOptions.getEvaluationSettings(), engineInitializationContext); } private List getCompiledLibraries(List ids, CqlEngine context) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureService.java index 06be1e14e8..9f91336327 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureService.java @@ -14,31 +14,41 @@ import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; +import org.opencds.cqf.cql.engine.execution.CqlEngine; import org.opencds.cqf.fhir.cql.Engines; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.MeasurePeriodValidator; import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; import org.opencds.cqf.fhir.utility.monad.Either3; +import org.opencds.cqf.fhir.utility.npm.MeasureOrNpmResourceHolder; import org.opencds.cqf.fhir.utility.repository.FederatedRepository; import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; import org.opencds.cqf.fhir.utility.repository.Repositories; public class R4MeasureService implements R4MeasureEvaluatorSingle { private final IRepository repository; + private final EngineInitializationContext engineInitializationContext; private final MeasureEvaluationOptions measureEvaluationOptions; private final MeasurePeriodValidator measurePeriodValidator; private final R4RepositorySubjectProvider subjectProvider; private final MeasureProcessorUtils measureProcessorUtils = new MeasureProcessorUtils(); + private final R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider; public R4MeasureService( IRepository repository, + EngineInitializationContext engineInitializationContext, MeasureEvaluationOptions measureEvaluationOptions, - MeasurePeriodValidator measurePeriodValidator) { + MeasurePeriodValidator measurePeriodValidator, + R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider) { this.repository = repository; + this.engineInitializationContext = engineInitializationContext; this.measureEvaluationOptions = measureEvaluationOptions; this.measurePeriodValidator = measurePeriodValidator; this.subjectProvider = new R4RepositorySubjectProvider(measureEvaluationOptions.getSubjectProviderOptions()); + this.r4RepositoryOrNpmResourceProvider = r4RepositoryOrNpmResourceProvider; } @Override @@ -62,7 +72,11 @@ public MeasureReport evaluate( var proxyRepoForMeasureProcessor = Repositories.proxy(repository, true, dataEndpoint, contentEndpoint, terminologyEndpoint); var processor = new R4MeasureProcessor( - proxyRepoForMeasureProcessor, this.measureEvaluationOptions, measureProcessorUtils); + proxyRepoForMeasureProcessor, + this.engineInitializationContext, + this.measureEvaluationOptions, + this.measureProcessorUtils, + this.r4RepositoryOrNpmResourceProvider); R4MeasureServiceUtils r4MeasureServiceUtils = new R4MeasureServiceUtils(repository); r4MeasureServiceUtils.ensureSupplementalDataElementSearchParameter(); @@ -81,16 +95,25 @@ public MeasureReport evaluate( var subjects = getSubjects(subjectId, proxyRepoForMeasureProcessor, additionalData); + var measurePlusNpmResourceHolder = + r4RepositoryOrNpmResourceProvider.foldMeasure(measure, proxyRepoForMeasureProcessor); + // Replicate the old logic of using the repository used to initialize the measure processor // as the repository for the CQL engine context. - var context = Engines.forRepository( - proxyRepoForMeasureProcessor, this.measureEvaluationOptions.getEvaluationSettings(), additionalData); + var context = buildCqlEngineContext(additionalData, proxyRepoForMeasureProcessor, measurePlusNpmResourceHolder); - var evaluationResults = - processor.evaluateMeasureWithCqlEngine(subjects, measure, periodStart, periodEnd, parameters, context); + var evaluationResults = processor.evaluateMeasureWithCqlEngine( + subjects, measurePlusNpmResourceHolder, periodStart, periodEnd, parameters, context); measureReport = processor.evaluateMeasure( - measure, periodStart, periodEnd, reportType, subjects, evalType, context, evaluationResults); + measurePlusNpmResourceHolder, + periodStart, + periodEnd, + reportType, + subjects, + evalType, + context, + evaluationResults); // add ProductLine after report is generated measureReport = r4MeasureServiceUtils.addProductLineExtension(measureReport, productLine); @@ -99,6 +122,17 @@ public MeasureReport evaluate( return r4MeasureServiceUtils.addSubjectReference(measureReport, practitioner, subjectId); } + @Nonnull + private CqlEngine buildCqlEngineContext( + Bundle additionalData, + IRepository proxyRepoForMeasureProcessor, + MeasureOrNpmResourceHolder measurePlusNpmResourceHolder) { + return Engines.forContext( + engineInitializationContext.withRepositoryAndCachedPackageLoader( + proxyRepoForMeasureProcessor, measurePlusNpmResourceHolder), + additionalData); + } + @Nonnull private List getSubjects( String subjectId, IRepository proxyRepoForMeasureProcessor, Bundle additionalData) { diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MultiMeasureService.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MultiMeasureService.java index 99c9992124..f25475e9f2 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MultiMeasureService.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/R4MultiMeasureService.java @@ -4,6 +4,7 @@ import ca.uhn.fhir.repository.IRepository; import com.google.common.base.Strings; +import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.time.ZonedDateTime; import java.util.Collections; @@ -14,20 +15,23 @@ import org.hl7.fhir.r4.model.Bundle.BundleType; import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Resource; import org.opencds.cqf.cql.engine.execution.CqlEngine; import org.opencds.cqf.fhir.cql.Engines; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.CompositeEvaluationResultsPerMeasure; import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; import org.opencds.cqf.fhir.cr.measure.common.MeasurePeriodValidator; import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.builder.BundleBuilder; +import org.opencds.cqf.fhir.utility.npm.MeasureOrNpmResourceHolder; +import org.opencds.cqf.fhir.utility.npm.MeasureOrNpmResourceHolderList; import org.opencds.cqf.fhir.utility.repository.Repositories; /** @@ -40,6 +44,7 @@ public class R4MultiMeasureService implements R4MeasureEvaluatorMultiple { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(R4MultiMeasureService.class); private final IRepository repository; + private final EngineInitializationContext engineInitializationContext; private final MeasureEvaluationOptions measureEvaluationOptions; private final MeasurePeriodValidator measurePeriodValidator; private final MeasureProcessorUtils measureProcessorUtils = new MeasureProcessorUtils(); @@ -47,19 +52,28 @@ public class R4MultiMeasureService implements R4MeasureEvaluatorMultiple { private final R4RepositorySubjectProvider subjectProvider; private final R4MeasureProcessor r4MeasureProcessorStandardRepository; private final R4MeasureServiceUtils r4MeasureServiceUtilsStandardRepository; + private final R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider; public R4MultiMeasureService( IRepository repository, + EngineInitializationContext engineInitializationContext, MeasureEvaluationOptions measureEvaluationOptions, String serverBase, - MeasurePeriodValidator measurePeriodValidator) { + MeasurePeriodValidator measurePeriodValidator, + R4RepositoryOrNpmResourceProvider r4RepositoryOrNpmResourceProvider) { this.repository = repository; + this.engineInitializationContext = engineInitializationContext; this.measureEvaluationOptions = measureEvaluationOptions; this.measurePeriodValidator = measurePeriodValidator; this.serverBase = serverBase; this.subjectProvider = new R4RepositorySubjectProvider(measureEvaluationOptions.getSubjectProviderOptions()); - this.r4MeasureProcessorStandardRepository = - new R4MeasureProcessor(repository, this.measureEvaluationOptions, this.measureProcessorUtils); + this.r4RepositoryOrNpmResourceProvider = r4RepositoryOrNpmResourceProvider; + this.r4MeasureProcessorStandardRepository = new R4MeasureProcessor( + repository, + engineInitializationContext, + this.measureEvaluationOptions, + this.measureProcessorUtils, + this.r4RepositoryOrNpmResourceProvider); this.r4MeasureServiceUtilsStandardRepository = new R4MeasureServiceUtils(repository); } @@ -89,8 +103,12 @@ public Parameters evaluate( var repositoryToUse = Repositories.proxy(repository, true, dataEndpoint, contentEndpoint, terminologyEndpoint); - r4ProcessorToUse = - new R4MeasureProcessor(repositoryToUse, this.measureEvaluationOptions, this.measureProcessorUtils); + r4ProcessorToUse = new R4MeasureProcessor( + repositoryToUse, + this.engineInitializationContext, + this.measureEvaluationOptions, + this.measureProcessorUtils, + this.r4RepositoryOrNpmResourceProvider); r4MeasureServiceUtilsToUse = new R4MeasureServiceUtils(repositoryToUse); } else { @@ -99,8 +117,11 @@ public Parameters evaluate( } r4MeasureServiceUtilsToUse.ensureSupplementalDataElementSearchParameter(); - List measures = r4MeasureServiceUtilsToUse.getMeasures(measureId, measureIdentifier, measureUrl); - log.info("multi-evaluate-measure, measures to evaluate: {}", measures.size()); + + var measurePlusNpmResourceHolderList = + r4RepositoryOrNpmResourceProvider.getMeasureOrNpmDetails(measureId, measureIdentifier, measureUrl); + + log.info("multi-evaluate-measure, measures to evaluate: {}", measurePlusNpmResourceHolderList.size()); var evalType = r4MeasureServiceUtilsToUse.getMeasureEvalType(reportType, subject); @@ -110,14 +131,15 @@ public Parameters evaluate( // create parameters var result = new Parameters(); - var context = Engines.forRepository( - r4ProcessorToUse.getRepository(), - this.measureEvaluationOptions.getEvaluationSettings(), + // Replicate the old logic of using the repository used to initialize the measure processor + // as the repository for the CQL engine context. + var context = Engines.forContext( + buildEvaluationContext(r4ProcessorToUse.getRepository(), measurePlusNpmResourceHolderList), additionalData); // This is basically a Map of measure -> subject -> EvaluationResult - var compositeEvaluationResultsPerMeasure = r4ProcessorToUse.evaluateMultiMeasuresWithCqlEngine( - subjects, measures, periodStart, periodEnd, parameters, context); + var compositeEvaluationResultsPerMeasure = r4ProcessorToUse.evaluateMultiMeasuresPlusNpmHoldersWithCqlEngine( + subjects, measurePlusNpmResourceHolderList, periodStart, periodEnd, parameters, context); // evaluate Measures if (evalType.equals(MeasureEvalType.POPULATION) || evalType.equals(MeasureEvalType.SUBJECTLIST)) { @@ -127,7 +149,7 @@ public Parameters evaluate( compositeEvaluationResultsPerMeasure, context, result, - measures, + measurePlusNpmResourceHolderList, periodStart, periodEnd, reportType, @@ -143,7 +165,7 @@ public Parameters evaluate( compositeEvaluationResultsPerMeasure, context, result, - measures, + measurePlusNpmResourceHolderList, periodStart, periodEnd, reportType, @@ -162,7 +184,7 @@ protected void populationMeasureReport( CompositeEvaluationResultsPerMeasure compositeEvaluationResultsPerMeasure, CqlEngine context, Parameters result, - List measures, + MeasureOrNpmResourceHolderList measureOrNpmResourceHolderList, @Nullable ZonedDateTime periodStart, @Nullable ZonedDateTime periodEnd, String reportType, @@ -172,12 +194,13 @@ protected void populationMeasureReport( String productLine, String reporter) { - var totalMeasures = measures.size(); - for (Measure measure : measures) { + var totalMeasures = measureOrNpmResourceHolderList.size(); + for (MeasureOrNpmResourceHolder measureOrNpmResourceHolder : + measureOrNpmResourceHolderList.measuresOrNpmResourceHolders()) { MeasureReport measureReport; // evaluate each measure measureReport = r4Processor.evaluateMeasure( - measure, + measureOrNpmResourceHolder, periodStart, periodEnd, reportType, @@ -226,7 +249,7 @@ protected void subjectMeasureReport( CompositeEvaluationResultsPerMeasure compositeEvaluationResultsPerMeasure, CqlEngine context, Parameters result, - List measures, + MeasureOrNpmResourceHolderList measureOrNpmResourceHolderList, @Nullable ZonedDateTime periodStart, @Nullable ZonedDateTime periodEnd, String reportType, @@ -236,18 +259,19 @@ protected void subjectMeasureReport( String reporter) { // create individual reports for each subject, and each measure - var totalReports = subjects.size() * measures.size(); - var totalMeasures = measures.size(); + var totalReports = subjects.size() * measureOrNpmResourceHolderList.size(); + var totalMeasures = measureOrNpmResourceHolderList.size(); log.debug( "Evaluating individual MeasureReports for {} patients, and {} measures", subjects.size(), - measures.size()); - for (Measure measure : measures) { + measureOrNpmResourceHolderList.size()); + for (MeasureOrNpmResourceHolder measureOrNpmResourceHolder : + measureOrNpmResourceHolderList.getMeasuresOrNpmResourceHolders()) { for (String subject : subjects) { MeasureReport measureReport; // evaluate each measure measureReport = r4Processor.evaluateMeasure( - measure, + measureOrNpmResourceHolder, periodStart, periodEnd, reportType, @@ -282,15 +306,25 @@ protected void subjectMeasureReport( log.debug("MeasureReports remaining to evaluate {}", totalReports--); } } - if (measure.hasUrl()) { + if (measureOrNpmResourceHolder.hasMeasureUrl()) { log.info( "Completed evaluation for Measure: {}, Measures remaining to evaluate: {}", - measure.getUrl(), + measureOrNpmResourceHolder.getMeasureUrl(), totalMeasures--); } } } + @Nonnull + private EngineInitializationContext buildEvaluationContext( + IRepository proxyRepoForMeasureProcessor, MeasureOrNpmResourceHolderList measurePlusNpmResourceHolderList) { + + return engineInitializationContext + .withRepository(proxyRepoForMeasureProcessor) + .withNpmPackageLoader( + r4RepositoryOrNpmResourceProvider.npmPackageLoaderWithCache(measurePlusNpmResourceHolderList)); + } + protected List getSubjects(R4RepositorySubjectProvider subjectProvider, String subjectId) { return subjectProvider.getSubjects(repository, subjectId).toList(); } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/npm/R4RepositoryOrNpmResourceProvider.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/npm/R4RepositoryOrNpmResourceProvider.java new file mode 100644 index 0000000000..54c23d19ab --- /dev/null +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/npm/R4RepositoryOrNpmResourceProvider.java @@ -0,0 +1,458 @@ +package org.opencds.cqf.fhir.cr.measure.r4.npm; + +import ca.uhn.fhir.model.api.IQueryParameterType; +import ca.uhn.fhir.repository.IRepository; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.param.UriParam; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import jakarta.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import org.apache.commons.collections4.CollectionUtils; +import org.hl7.elm.r1.VersionedIdentifier; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CanonicalType; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Library; +import org.hl7.fhir.r4.model.Measure; +import org.hl7.fhir.r4.model.ResourceType; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; +import org.opencds.cqf.fhir.cql.EvaluationSettings; +import org.opencds.cqf.fhir.cql.VersionedIdentifiers; +import org.opencds.cqf.fhir.utility.monad.Either3; +import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.MeasureOrNpmResourceHolder; +import org.opencds.cqf.fhir.utility.npm.MeasureOrNpmResourceHolderList; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoaderWithCache; +import org.opencds.cqf.fhir.utility.npm.NpmResourceHolder; +import org.opencds.cqf.fhir.utility.search.Searches; + +/** + * Combined readonly operations on Repository and NPM resources for R4 Measures and Libraries, and possibly + * other resources such as PlanDefinition, ValueSet, etc in the future. + */ +public class R4RepositoryOrNpmResourceProvider { + + public static final String QUERIES_BY_MEASURE_IDS_ARE_NOT_SUPPORTED_BY_NPM_RESOURCES = + "Queries by measure ID: %s are not supported by NPM resources"; + + public static final String QUERIES_BY_MEASURE_IDENTIFIERS_ARE_NOT_SUPPORTED_BY_NPM_RESOURCES = + "Queries by measure identifiers: %s are not supported by NPM resources"; + + private final IRepository repository; + private final NpmPackageLoader npmPackageLoader; + private final EvaluationSettings evaluationSettings; + + public R4RepositoryOrNpmResourceProvider( + IRepository repository, NpmPackageLoader npmPackageLoader, EvaluationSettings evaluationSettings) { + this.repository = repository; + this.npmPackageLoader = npmPackageLoader; + this.evaluationSettings = evaluationSettings; + } + + public NpmPackageLoaderWithCache npmPackageLoaderWithCache(MeasureOrNpmResourceHolder measureOrNpmResourceHolder) { + return NpmPackageLoaderWithCache.of(measureOrNpmResourceHolder.npmResourceHolder(), npmPackageLoader); + } + + public NpmPackageLoaderWithCache npmPackageLoaderWithCache( + MeasureOrNpmResourceHolderList measureOrNpmResourceHolderList) { + return NpmPackageLoaderWithCache.of(measureOrNpmResourceHolderList.npmResourceHolders(), npmPackageLoader); + } + + public EngineInitializationContext getEngineInitializationContext() { + return new EngineInitializationContext(repository, npmPackageLoader, evaluationSettings); + } + + public EngineInitializationContext getEngineInitializationContextWithNpmCached( + MeasureOrNpmResourceHolderList measureOrNpmResourceHolderList) { + return new EngineInitializationContext( + repository, npmPackageLoaderWithCache(measureOrNpmResourceHolderList), evaluationSettings); + } + + public EngineInitializationContext getEngineInitializationContextWithNpmCached( + MeasureOrNpmResourceHolder measureOrNpmResourceHolder) { + return new EngineInitializationContext( + repository, npmPackageLoaderWithCache(measureOrNpmResourceHolder), evaluationSettings); + } + + public IRepository getRepository() { + return repository; + } + + public NpmPackageLoader getNpmPackageLoader() { + return npmPackageLoader; + } + + public EvaluationSettings getEvaluationSettings() { + return evaluationSettings; + } + + public List> getMeasureEithers( + List measureIds, List measureUrls) { + + if (CollectionUtils.isNotEmpty(measureIds) && CollectionUtils.isNotEmpty(measureUrls)) { + throw new InvalidRequestException("measure IDs and URLs cannot both be provided."); + } + + if (measureIds != null && !measureIds.isEmpty()) { + return measureIds.stream() + .map(measureId -> Eithers.forMiddle3(new IdType(measureId))) + .toList(); + } else if (measureUrls != null && !measureUrls.isEmpty()) { + return measureUrls.stream() + .map(measureUrl -> Eithers.forLeft3(new CanonicalType(measureUrl))) + .toList(); + } + + // Maybe should be an IllegalArgumentException instead, but preserve backwards compatibility + return List.of(); + } + + /** + * method to extract Library version defined on the Measure in question + *

+ * @param measureOrNpmResourceHolder FHIR or NPM Measure that has desired Library + * @return version identifier of Library + */ + public VersionedIdentifier getLibraryVersionIdentifier(MeasureOrNpmResourceHolder measureOrNpmResourceHolder) { + var url = measureOrNpmResourceHolder + .getMainLibraryUrl() + .orElseThrow(() -> new InvalidRequestException("Measure %s does not have a primary library specified" + .formatted(measureOrNpmResourceHolder.getMeasureUrl()))); + + // Check to see if this Library exists in an NPM Package. If not, search the Repository + if (!measureOrNpmResourceHolder.hasNpmLibrary()) { + Bundle b = this.repository.search(Bundle.class, Library.class, Searches.byCanonical(url), null); + if (b.getEntry().isEmpty()) { + var errorMsg = "Unable to find Library with url: %s".formatted(url); + throw new ResourceNotFoundException(errorMsg); + } + } + return VersionedIdentifiers.forUrl(url); + } + + private static Measure foldMeasureFromRepository( + Either3 measureEither, IRepository repository) { + + return measureEither.fold( + measureUrl -> resolveByUrlFromRepository(measureUrl, repository), + measureIdType -> resolveMeasureById(measureIdType, repository), + Function.identity()); + } + + public MeasureOrNpmResourceHolderList foldMeasures(List> measureEithers) { + if (measureEithers == null || measureEithers.isEmpty()) { + throw new InvalidRequestException("measure IDs or URLs parameter cannot be null or empty."); + } + + return MeasureOrNpmResourceHolderList.of( + measureEithers.stream().map(this::foldMeasure).toList()); + } + + public MeasureOrNpmResourceHolder foldMeasure(Either3 measureEither) { + if (evaluationSettings.isUseNpmForQualifyingResources()) { + return foldMeasureEitherForNpm(measureEither); + } + + return foldMeasureForRepository(measureEither); + } + + public MeasureOrNpmResourceHolder foldWithCustomIdTypeHandler( + Either3 measureEither, Function foldMiddle) { + + if (evaluationSettings.isUseNpmForQualifyingResources()) { + return foldMeasureEitherForNpm(measureEither); + } + + return measureEither.fold( + ((Function) this::resolveByUrl) + .andThen(MeasureOrNpmResourceHolder::measureOnly), + foldMiddle.andThen(MeasureOrNpmResourceHolder::measureOnly), + MeasureOrNpmResourceHolder::measureOnly); + } + + @Nonnull + private MeasureOrNpmResourceHolder foldMeasureEitherForRepository( + Either3 measureEither) { + + var folded = measureEither.fold( + measureIdType -> resolveMeasureById(measureIdType, repository), + this::resolveByIdentifier, + measureUrl -> resolveByUrlFromRepository(measureUrl, repository)); + + return MeasureOrNpmResourceHolder.measureOnly(folded); + } + + @Nonnull + private MeasureOrNpmResourceHolder foldMeasureForRepository(Either3 measureEither) { + + var folded = measureEither.fold( + this::resolveByUrlFromRepository, + measureIdType -> resolveMeasureById(measureIdType, repository), + Function.identity()); + + return MeasureOrNpmResourceHolder.measureOnly(folded); + } + + public static Measure resolveMeasureById(IIdType id, IRepository repository) { + if (id.getValueAsString().startsWith("Measure/")) { + // If the id is a Measure resource, we can use the read method directly + return repository.read(Measure.class, id); + } + // If not, add it to ensure it plays nicely with the InMemoryFhirRepository + return repository.read(Measure.class, new IdType(ResourceType.Measure.name(), id.getIdPart())); + } + + private Measure resolveByUrlFromRepository(CanonicalType measureUrl) { + return resolveByUrlFromRepository(measureUrl, repository); + } + + // If the caller chooses to provide their own IRepository (ex: federated) + public MeasureOrNpmResourceHolder foldMeasure( + Either3 measureEither, IRepository repository) { + + if (evaluationSettings.isUseNpmForQualifyingResources()) { + return foldMeasureEitherForNpm(measureEither); + } + + return MeasureOrNpmResourceHolder.measureOnly(foldMeasureFromRepository(measureEither, repository)); + } + + public MeasureOrNpmResourceHolderList foldMeasureEithers( + List> measureEithers) { + if (measureEithers == null || measureEithers.isEmpty()) { + throw new InvalidRequestException("measure IDs or URLs parameter cannot be null or empty."); + } + + return MeasureOrNpmResourceHolderList.of( + measureEithers.stream().map(this::foldMeasureEither).toList()); + } + + private MeasureOrNpmResourceHolder foldMeasureEither(Either3 measureEither) { + if (evaluationSettings.isUseNpmForQualifyingResources()) { + return foldMeasureForNpm(measureEither); + } + + return foldMeasureEitherForRepository(measureEither); + } + + public MeasureOrNpmResourceHolder foldMeasureEitherForNpm(Either3 measureEither) { + + return measureEither.fold( + measureUrl -> { + var npmResourceHolder = npmPackageLoader.loadNpmResources(measureUrl); + if (npmResourceHolder == null || npmResourceHolder == NpmResourceHolder.EMPTY) { + throw new InvalidRequestException( + "No NPM resources found for Measure URL: %s".formatted(measureUrl.getValue())); + } + return MeasureOrNpmResourceHolder.npmOnly(npmResourceHolder); + }, + measureId -> { + throw new InvalidRequestException( + QUERIES_BY_MEASURE_IDS_ARE_NOT_SUPPORTED_BY_NPM_RESOURCES.formatted(measureId)); + }, + MeasureOrNpmResourceHolder::measureOnly); + } + + private MeasureOrNpmResourceHolder foldMeasureForNpm(Either3 measureEither) { + + return measureEither.fold( + measureId -> { + throw new InvalidRequestException( + QUERIES_BY_MEASURE_IDS_ARE_NOT_SUPPORTED_BY_NPM_RESOURCES.formatted(measureId)); + }, + measureIdentifier -> { + throw new InvalidRequestException( + QUERIES_BY_MEASURE_IDENTIFIERS_ARE_NOT_SUPPORTED_BY_NPM_RESOURCES.formatted( + measureIdentifier)); + }, + measureUrl -> { + var npmResourceHolder = npmPackageLoader.loadNpmResources(measureUrl); + if (npmResourceHolder == null || npmResourceHolder == NpmResourceHolder.EMPTY) { + throw new InvalidRequestException( + "No NPM resources found for Measure URL: %s".formatted(measureUrl.getValue())); + } + return MeasureOrNpmResourceHolder.npmOnly(npmResourceHolder); + }); + } + + public MeasureOrNpmResourceHolderList getMeasureOrNpmDetails( + List measureIds, List measureIdentifiers, List measureCanonicals) { + + if ((measureIds == null || measureIds.isEmpty()) + && (measureCanonicals == null || measureCanonicals.isEmpty()) + && (measureIdentifiers == null || measureIdentifiers.isEmpty())) { + throw new InvalidRequestException("measure IDs, identifiers, or URLs parameter cannot be null or empty."); + } + + if (measureIds != null && !measureIds.isEmpty()) { + return withDistinctByKey(getMeasureOrNpmDetailsForMeasureIds(measureIds)); + } + + if (measureCanonicals != null && !measureCanonicals.isEmpty()) { + return withDistinctByKey(getMeasureOrNpmDetailsForMeasureCanonicals(measureCanonicals)); + } + + return withDistinctByKey(getMeasureOrNpmDetailsForMeasureIdents(measureIdentifiers)); + } + + private MeasureOrNpmResourceHolderList withDistinctByKey( + List measureOrNpmResourceHolders) { + return MeasureOrNpmResourceHolderList.of( + distinctByKey(measureOrNpmResourceHolders, MeasureOrNpmResourceHolder::getMeasureUrl)); + } + + private List getMeasureOrNpmDetailsForMeasureIds(List measureIds) { + if (evaluationSettings.isUseNpmForQualifyingResources()) { + throw new InvalidRequestException( + "Queries by measure IDs: %s are not supported by NPM resources".formatted(measureIds)); + } + + return measureIds.stream() + .map(this::resolveMeasureById) + .map(MeasureOrNpmResourceHolder::measureOnly) + .toList(); + } + + private List getMeasureOrNpmDetailsForMeasureCanonicals( + List measureCanonicals) { + if (evaluationSettings.isUseNpmForQualifyingResources()) { + return measureCanonicals.stream() + .map(this::resolveByUrlFromNpm) + .map(MeasureOrNpmResourceHolder::npmOnly) + .toList(); + } + + return measureCanonicals.stream() + .map(CanonicalType::new) + .map(this::resolveByUrl) + .map(MeasureOrNpmResourceHolder::measureOnly) + .toList(); + } + + private List getMeasureOrNpmDetailsForMeasureIdents(List measureIdentifiers) { + if (evaluationSettings.isUseNpmForQualifyingResources()) { + throw new InvalidRequestException( + QUERIES_BY_MEASURE_IDENTIFIERS_ARE_NOT_SUPPORTED_BY_NPM_RESOURCES.formatted(measureIdentifiers)); + } + + return measureIdentifiers.stream() + .map(measureIdentifier -> + MeasureOrNpmResourceHolder.measureOnly(resolveByIdentifier(measureIdentifier))) + .toList(); + } + + public NpmResourceHolder resolveByUrlFromNpm(String measureCanonical) { + return this.npmPackageLoader.loadNpmResources(new CanonicalType(measureCanonical)); + } + + public static List distinctByKey(List list, Function keyExtractor) { + Set seen = new HashSet<>(); + return list.stream() + .filter(Objects::nonNull) + .filter(element -> seen.add(keyExtractor.apply(element))) + .toList(); + } + + public Measure resolveMeasureById(IdType id) { + if (evaluationSettings.isUseNpmForQualifyingResources()) { + throw new InvalidRequestException(QUERIES_BY_MEASURE_IDS_ARE_NOT_SUPPORTED_BY_NPM_RESOURCES.formatted(id)); + } + + return this.repository.read(Measure.class, id); + } + + public Measure resolveByUrl(String measureUrl) { + return resolveByUrl(new CanonicalType(measureUrl)); + } + + public Measure resolveByUrl(CanonicalType measureUrl) { + if (evaluationSettings.isUseNpmForQualifyingResources()) { + final NpmResourceHolder npmResourceHolder = npmPackageLoader.loadNpmResources(measureUrl); + + var optMeasureAdapter = npmResourceHolder.getMeasure(); + + if (optMeasureAdapter.isEmpty()) { + throw new IllegalArgumentException("No measure found for URL: %s".formatted(measureUrl.getValue())); + } + + var measureAdapter = optMeasureAdapter.get(); + + if (!(measureAdapter.get() instanceof Measure measure)) { + throw new IllegalArgumentException("MeasureAdapter is not a Measure for URL: %s".formatted(measureUrl)); + } + + return measure; + } + + return resolveByUrlFromRepository(measureUrl); + } + + private static Measure resolveByUrlFromRepository(CanonicalType url, IRepository repository) { + Map> searchParameters = new HashMap<>(); + var urlAsString = url.getValueAsString(); + if (urlAsString.contains("|")) { + // uri & version + var splitId = urlAsString.split("\\|"); + var uri = splitId[0]; + var version = splitId[1]; + searchParameters.put("url", Collections.singletonList(new UriParam(uri))); + searchParameters.put("version", Collections.singletonList(new TokenParam(version))); + } else { + // uri only + searchParameters.put("url", Collections.singletonList(new UriParam(urlAsString))); + } + + Bundle result = repository.search(Bundle.class, Measure.class, searchParameters); + if (result == null || result.getEntry().isEmpty()) { + throw new ResourceNotFoundException("No Measure found for URL: %s".formatted(url)); + } + if (result.getEntryFirstRep().getResource() instanceof Measure measure) { + return measure; + } + throw new IllegalStateException("expected Measure resource but found: %s" + .formatted(result.getEntryFirstRep() + .getResource() + .getResourceType() + .name())); + } + + public Measure resolveByIdentifier(String identifier) { + List params = new ArrayList<>(); + Map> searchParams = new HashMap<>(); + Bundle bundle; + if (identifier.contains("|")) { + // system & value + var splitId = identifier.split("\\|"); + var system = splitId[0]; + var code = splitId[1]; + params.add(new TokenParam(system, code)); + } else { + // value only + params.add(new TokenParam(identifier)); + } + searchParams.put("identifier", params); + bundle = this.repository.search(Bundle.class, Measure.class, searchParams); + + if (bundle != null && !bundle.getEntry().isEmpty()) { + if (bundle.getEntry().size() > 1) { + var msg = "Measure Identifier: %s, found more than one matching measure resource".formatted(identifier); + throw new InvalidRequestException(msg); + } + return (Measure) bundle.getEntryFirstRep().getResource(); + } else { + var msg = "Measure Identifier: %s, found no matching measure resources".formatted(identifier); + throw new InvalidRequestException(msg); + } + } +} diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtils.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtils.java index a00edf8452..8ae019608a 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtils.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtils.java @@ -15,31 +15,16 @@ import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.US_COUNTRY_CODE; import static org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants.US_COUNTRY_DISPLAY; -import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.repository.IRepository; -import ca.uhn.fhir.rest.param.TokenParam; -import ca.uhn.fhir.rest.param.UriParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Objects; import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; @@ -57,10 +42,9 @@ import org.opencds.cqf.fhir.cr.measure.common.MeasureReportType; import org.opencds.cqf.fhir.cr.measure.common.MeasureScoring; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureEvalType; -import org.opencds.cqf.fhir.utility.Canonicals; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.monad.Either3; -import org.opencds.cqf.fhir.utility.search.Searches; +import org.opencds.cqf.fhir.utility.monad.Eithers; public class R4MeasureServiceUtils { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(R4MeasureServiceUtils.class); @@ -70,6 +54,21 @@ public R4MeasureServiceUtils(IRepository repository) { this.repository = repository; } + public static Either3 getMeasureEither( + @Nullable String measureUrl, @Nullable IdType measureId) { + if (measureUrl == null && measureId == null) { + throw new IllegalArgumentException("Must have one of measureUrl or measureId populated but both are null"); + } + + if (measureUrl != null && measureId != null) { + throw new IllegalArgumentException( + "Must have only one of measureUrl or measureId populated but both are non-null"); + } + + return Eithers.for3( + Optional.ofNullable(measureUrl).map(CanonicalType::new).orElse(null), measureId, null); + } + public MeasureReport addProductLineExtension(MeasureReport measureReport, String productLine) { if (productLine != null) { Extension ext = new Extension(); @@ -182,97 +181,11 @@ public Optional getReporter(String reporter) { return Optional.ofNullable(reference); } - public Measure resolveById(IdType id) { - return this.repository.read(Measure.class, id); - } - - public Measure resolveByUrl(String url) { - Map> searchParameters = new HashMap<>(); - if (url.contains("|")) { - // uri & version - var splitId = url.split("\\|"); - var uri = splitId[0]; - var version = splitId[1]; - searchParameters.put("url", Collections.singletonList(new UriParam(uri))); - searchParameters.put("version", Collections.singletonList(new TokenParam(version))); - } else { - // uri only - searchParameters.put("url", Collections.singletonList(new UriParam(url))); - } - - Bundle result = this.repository.search(Bundle.class, Measure.class, searchParameters); - return (Measure) result.getEntryFirstRep().getResource(); - } - - public Measure resolveByIdentifier(String identifier) { - List params = new ArrayList<>(); - Map> searchParams = new HashMap<>(); - Bundle bundle; - if (identifier.contains("|")) { - // system & value - var splitId = identifier.split("\\|"); - var system = splitId[0]; - var code = splitId[1]; - params.add(new TokenParam(system, code)); - } else { - // value only - params.add(new TokenParam(identifier)); - } - searchParams.put("identifier", params); - bundle = this.repository.search(Bundle.class, Measure.class, searchParams); - - if (bundle != null && !bundle.getEntry().isEmpty()) { - if (bundle.getEntry().size() > 1) { - var msg = "Measure Identifier: %s, found more than one matching measure resource".formatted(identifier); - throw new InvalidRequestException(msg); - } - return (Measure) bundle.getEntryFirstRep().getResource(); - } else { - var msg = "Measure Identifier: %s, found no matching measure resources".formatted(identifier); - throw new InvalidRequestException(msg); - } - } - - public List getMeasures( - List measureIds, List measureIdentifiers, List measureCanonicals) { - List measures = new ArrayList<>(); - if (measureIds != null && !measureIds.isEmpty()) { - for (IdType measureId : measureIds) { - Measure measureById = resolveById(measureId); - measures.add(measureById); - } - } - - if (measureCanonicals != null && !measureCanonicals.isEmpty()) { - for (String measureCanonical : measureCanonicals) { - Measure measureByUrl = resolveByUrl(measureCanonical); - measures.add(measureByUrl); - } - } - - if (measureIdentifiers != null && !measureIdentifiers.isEmpty()) { - for (String measureIdentifier : measureIdentifiers) { - Measure measureByIdentifier = resolveByIdentifier(measureIdentifier); - measures.add(measureByIdentifier); - } - } - - return distinctByKey(measures, Measure::getUrl); - } - - public static List distinctByKey(List list, Function keyExtractor) { - Set seen = new HashSet<>(); - return list.stream() - .filter(Objects::nonNull) - .filter(element -> seen.add(keyExtractor.apply(element))) - .toList(); - } - public List getMeasureGroupScoringTypes(Measure measure) { var groupScoringCodes = measure.getGroup().stream() .map(t -> (CodeableConcept) t.getExtensionByUrl(CQFM_SCORING_EXT_URL).getValue()) - .collect(Collectors.toList()); + .toList(); // extract measureScoring Type from components return groupScoringCodes.stream() .map(t -> MeasureScoring.fromCode(t.getCodingFirstRep().getCode())) @@ -294,10 +207,7 @@ public boolean hasMultipleGroupScoringTypes(Measure measure) { */ public boolean hasGroupScoringDef(Measure measure) { - return !measure.getGroup().stream() - .filter(t -> t.getExtensionByUrl(CQFM_SCORING_EXT_URL) != null) - .collect(Collectors.toList()) - .isEmpty(); + return measure.getGroup().stream().anyMatch(t -> t.getExtensionByUrl(CQFM_SCORING_EXT_URL) != null); } public List getMeasureScoringDef(Measure measure) { @@ -348,40 +258,4 @@ public MeasureEvalType convertToNonVersionedMeasureEvalTypeOrDefault(R4MeasureEv public boolean isSubjectListEffectivelyEmpty(List subjectIds) { return subjectIds == null || subjectIds.isEmpty() || subjectIds.get(0) == null; } - - public static List foldMeasures( - List> measures, IRepository repository) { - return measures.stream() - .map(measure -> foldMeasure(measure, repository)) - .toList(); - } - - public static Measure foldMeasure(Either3 measure, IRepository repository) { - return measure.fold( - measureCanonicalType -> resolveByUrl(measureCanonicalType, repository), - measureIdType -> resolveById(measureIdType, repository), - Function.identity()); - } - - private static Measure resolveByUrl(CanonicalType url, IRepository repository) { - var parts = Canonicals.getParts(url); - var result = repository.search( - Bundle.class, Measure.class, Searches.byNameAndVersion(parts.idPart(), parts.version())); - return (Measure) result.getEntryFirstRep().getResource(); - } - - public static List resolveByIds(List ids, IRepository repository) { - var idStringArray = ids.stream().map(IPrimitiveType::getValueAsString).toArray(String[]::new); - var searchParameters = Searches.byId(idStringArray); - - return repository.search(Bundle.class, Measure.class, searchParameters).getEntry().stream() - .map(BundleEntryComponent::getResource) - .filter(Measure.class::isInstance) - .map(Measure.class::cast) - .toList(); - } - - public static Measure resolveById(IIdType id, IRepository repository) { - return repository.read(Measure.class, id); - } } diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/PlanDefinitionProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/PlanDefinitionProcessor.java index 59b2ca43f5..afaed99a66 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/PlanDefinitionProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/PlanDefinitionProcessor.java @@ -8,6 +8,7 @@ import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.repository.IRepository; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseBackboneElement; @@ -18,6 +19,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.opencds.cqf.cql.engine.model.ModelResolver; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.activitydefinition.apply.IRequestResolverFactory; @@ -46,23 +48,41 @@ public class PlanDefinitionProcessor { protected org.opencds.cqf.fhir.cr.activitydefinition.apply.IApplyProcessor activityProcessor; protected IRequestResolverFactory requestResolverFactory; protected IRepository repository; - protected EvaluationSettings evaluationSettings; - protected TerminologyServerClientSettings terminologyServerClientSettings; + protected final EvaluationSettings evaluationSettings; + protected final EngineInitializationContext engineInitializationContext; - public PlanDefinitionProcessor(IRepository repository) { - this(repository, EvaluationSettings.getDefault(), new TerminologyServerClientSettings()); + @Nullable + protected final TerminologyServerClientSettings terminologyServerClientSettings; + + public PlanDefinitionProcessor(IRepository repository, EngineInitializationContext engineInitializationContext) { + this( + repository, + EvaluationSettings.getDefault(), + engineInitializationContext, + new TerminologyServerClientSettings()); } public PlanDefinitionProcessor( IRepository repository, EvaluationSettings evaluationSettings, + EngineInitializationContext engineInitializationContext, TerminologyServerClientSettings terminologyServerClientSettings) { - this(repository, evaluationSettings, terminologyServerClientSettings, null, null, null, null, null); + this( + repository, + evaluationSettings, + engineInitializationContext, + terminologyServerClientSettings, + null, + null, + null, + null, + null); } public PlanDefinitionProcessor( IRepository repository, EvaluationSettings evaluationSettings, + EngineInitializationContext engineInitializationContext, TerminologyServerClientSettings terminologyServerClientSettings, IApplyProcessor applyProcessor, IPackageProcessor packageProcessor, @@ -71,9 +91,12 @@ public PlanDefinitionProcessor( IRequestResolverFactory requestResolverFactory) { this.repository = requireNonNull(repository, "repository can not be null"); this.evaluationSettings = requireNonNull(evaluationSettings, "evaluationSettings can not be null"); + this.engineInitializationContext = engineInitializationContext; if (packageProcessor == null) { this.terminologyServerClientSettings = requireNonNull(terminologyServerClientSettings, "terminologyServerClientSettings can not be null"); + } else { + this.terminologyServerClientSettings = null; } fhirVersion = this.repository.fhirContext().getVersion().getVersion(); modelResolver = FhirModelResolverCache.resolverForVersion(fhirVersion); @@ -98,7 +121,7 @@ protected void initApplyProcessor() { } applyProcessor = applyProcessor != null ? applyProcessor - : new ApplyProcessor(repository, modelResolver, activityProcessor); + : new ApplyProcessor(repository, engineInitializationContext, modelResolver, activityProcessor); } protected , R extends IBaseResource> R resolvePlanDefinition( @@ -202,7 +225,7 @@ public , R extends IBaseResource> IBaseResource null, null, null, - new LibraryEngine(repository, evaluationSettings)); + new LibraryEngine(repository, evaluationSettings, engineInitializationContext)); } public , R extends IBaseResource> IBaseResource apply( @@ -276,7 +299,8 @@ public , R extends IBaseResource> IBaseResource parameters, data, prefetchData, - new LibraryEngine(repository, this.evaluationSettings)); + new LibraryEngine( + repository, this.evaluationSettings, engineInitializationContext.withRepository(repository))); } public , R extends IBaseResource> IBaseResource apply( @@ -404,7 +428,8 @@ public , R extends IBaseResource> IBaseParamete parameters, data, prefetchData, - new LibraryEngine(repository, this.evaluationSettings)); + new LibraryEngine( + repository, this.evaluationSettings, engineInitializationContext.withRepository(repository))); } public , R extends IBaseResource> IBaseParameters applyR5( diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ApplyProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ApplyProcessor.java index 052b111bad..3065cd83aa 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ApplyProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/plandefinition/apply/ApplyProcessor.java @@ -21,6 +21,7 @@ import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.opencds.cqf.cql.engine.model.ModelResolver; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cr.common.ExtensionProcessor; import org.opencds.cqf.fhir.cr.common.ICpgRequest; import org.opencds.cqf.fhir.cr.questionnaire.generate.GenerateProcessor; @@ -55,15 +56,16 @@ public class ApplyProcessor implements IApplyProcessor { public ApplyProcessor( IRepository repository, + EngineInitializationContext engineInitializationContext, ModelResolver modelResolver, org.opencds.cqf.fhir.cr.activitydefinition.apply.IApplyProcessor activityProcessor) { this.repository = repository; this.modelResolver = modelResolver; this.activityProcessor = activityProcessor; extensionProcessor = new ExtensionProcessor(); - generateProcessor = new GenerateProcessor(this.repository); + generateProcessor = new GenerateProcessor(this.repository, engineInitializationContext); populateProcessor = new PopulateProcessor(); - extractProcessor = new QuestionnaireResponseProcessor(this.repository); + extractProcessor = new QuestionnaireResponseProcessor(this.repository, engineInitializationContext); processRequest = new ResponseBuilder(populateProcessor); processGoal = new ProcessGoal(); processAction = new ProcessAction(this.repository, this, generateProcessor); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessor.java index 9e9cfdaeed..ff4fbb51de 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessor.java @@ -16,6 +16,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.opencds.cqf.cql.engine.model.ModelResolver; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.common.DataRequirementsProcessor; @@ -42,28 +43,34 @@ public class QuestionnaireProcessor { protected final EvaluationSettings evaluationSettings; protected final FhirVersionEnum fhirVersion; protected IRepository repository; + protected EngineInitializationContext engineInitializationContext; protected IGenerateProcessor generateProcessor; protected IPackageProcessor packageProcessor; protected IDataRequirementsProcessor dataRequirementsProcessor; protected IPopulateProcessor populateProcessor; - public QuestionnaireProcessor(IRepository repository) { - this(repository, EvaluationSettings.getDefault()); + public QuestionnaireProcessor(IRepository repository, EngineInitializationContext engineInitializationContext) { + this(repository, EvaluationSettings.getDefault(), engineInitializationContext); } - public QuestionnaireProcessor(IRepository repository, EvaluationSettings evaluationSettings) { - this(repository, evaluationSettings, null, null, null, null); + public QuestionnaireProcessor( + IRepository repository, + EvaluationSettings evaluationSettings, + EngineInitializationContext engineInitializationContext) { + this(repository, evaluationSettings, engineInitializationContext, null, null, null, null); } public QuestionnaireProcessor( IRepository repository, EvaluationSettings evaluationSettings, + EngineInitializationContext engineInitializationContext, IGenerateProcessor generateProcessor, IPackageProcessor packageProcessor, IDataRequirementsProcessor dataRequirementsProcessor, IPopulateProcessor populateProcessor) { this.repository = requireNonNull(repository, "repository can not be null"); this.evaluationSettings = requireNonNull(evaluationSettings, "evaluationSettings can not be null"); + this.engineInitializationContext = engineInitializationContext; this.questionnaireResolver = new ResourceResolver("Questionnaire", this.repository); this.structureDefResolver = new ResourceResolver("StructureDefinition", this.repository); fhirVersion = this.repository.fhirContext().getVersion().getVersion(); @@ -148,13 +155,17 @@ public , R extends IBaseResource> IBaseResource resolveStructureDefinition(profile), supportedOnly, requiredOnly, - libraryEngine != null ? libraryEngine : new LibraryEngine(repository, evaluationSettings), + libraryEngine != null + ? libraryEngine + : new LibraryEngine(repository, evaluationSettings, engineInitializationContext), modelResolver); return generateQuestionnaire(request, id); } public IBaseResource generateQuestionnaire(GenerateRequest request, String id) { - var processor = generateProcessor != null ? generateProcessor : new GenerateProcessor(this.repository); + var processor = generateProcessor != null + ? generateProcessor + : new GenerateProcessor(this.repository, engineInitializationContext); return request == null ? processor.generate(id) : processor.generate(request, id); } @@ -205,7 +216,9 @@ public PopulateRequest buildPopulateRequest( launchContext, parameters, data, - libraryEngine != null ? libraryEngine : new LibraryEngine(repository, evaluationSettings), + libraryEngine != null + ? libraryEngine + : new LibraryEngine(repository, evaluationSettings, engineInitializationContext), modelResolver); } @@ -252,7 +265,7 @@ public , R extends IBaseResource> IBaseResource launchContext, parameters, data, - new LibraryEngine(repository, this.evaluationSettings)); + new LibraryEngine(repository, this.evaluationSettings, engineInitializationContext)); } public , R extends IBaseResource> IBaseResource populate( diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/GenerateProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/GenerateProcessor.java index 6c11d54b32..fbcd9b5c4b 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/GenerateProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/GenerateProcessor.java @@ -15,6 +15,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.ICompositeType; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; import org.opencds.cqf.fhir.utility.adapter.IElementDefinitionAdapter; @@ -29,10 +30,10 @@ public class GenerateProcessor implements IGenerateProcessor { protected final FhirVersionEnum fhirVersion; protected final ItemGenerator itemGenerator; - public GenerateProcessor(IRepository repository) { + public GenerateProcessor(IRepository repository, EngineInitializationContext engineInitializationContext) { this.repository = repository; this.fhirVersion = repository.fhirContext().getVersion().getVersion(); - itemGenerator = new ItemGenerator(repository); + itemGenerator = new ItemGenerator(repository, engineInitializationContext); } @Override diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ItemGenerator.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ItemGenerator.java index d85cab08f3..a58ba00084 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ItemGenerator.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaire/generate/ItemGenerator.java @@ -21,6 +21,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.opencds.cqf.cql.engine.execution.CqlEngine; import org.opencds.cqf.fhir.cql.Engines; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cr.common.ExpressionProcessor; import org.opencds.cqf.fhir.cr.common.ExtensionProcessor; import org.opencds.cqf.fhir.cr.questionnaire.Helpers; @@ -48,9 +49,9 @@ public class ItemGenerator { protected final ElementHasCaseFeature elementHasCaseFeature; protected final ItemTypeIsChoice itemTypeIsChoice; - public ItemGenerator(IRepository repository) { + public ItemGenerator(IRepository repository, EngineInitializationContext engineInitializationContext) { this.repository = repository; - engine = Engines.forRepository(this.repository); + engine = Engines.forContext(engineInitializationContext); expressionProcessor = new ExpressionProcessor(); extensionProcessor = new ExtensionProcessor(); elementHasCaseFeature = new ElementHasCaseFeature(); diff --git a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/QuestionnaireResponseProcessor.java b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/QuestionnaireResponseProcessor.java index 7aeac5703e..3c3b37e8cb 100644 --- a/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/QuestionnaireResponseProcessor.java +++ b/cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/questionnaireresponse/QuestionnaireResponseProcessor.java @@ -14,6 +14,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.opencds.cqf.cql.engine.model.ModelResolver; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.common.ResourceResolver; @@ -35,20 +36,29 @@ public class QuestionnaireResponseProcessor { protected final EvaluationSettings evaluationSettings; protected final FhirVersionEnum fhirVersion; protected IRepository repository; + protected EngineInitializationContext engineInitializationContext; protected IExtractProcessor extractProcessor; - public QuestionnaireResponseProcessor(IRepository repository) { - this(repository, EvaluationSettings.getDefault()); + public QuestionnaireResponseProcessor( + IRepository repository, EngineInitializationContext engineInitializationContext) { + this(repository, EvaluationSettings.getDefault(), engineInitializationContext); } - public QuestionnaireResponseProcessor(IRepository repository, EvaluationSettings evaluationSettings) { - this(repository, evaluationSettings, null); + public QuestionnaireResponseProcessor( + IRepository repository, + EvaluationSettings evaluationSettings, + EngineInitializationContext engineInitializationContext) { + this(repository, evaluationSettings, engineInitializationContext, null); } public QuestionnaireResponseProcessor( - IRepository repository, EvaluationSettings evaluationSettings, IExtractProcessor extractProcessor) { + IRepository repository, + EvaluationSettings evaluationSettings, + EngineInitializationContext engineInitializationContext, + IExtractProcessor extractProcessor) { this.repository = requireNonNull(repository, "repository can not be null"); this.evaluationSettings = requireNonNull(evaluationSettings, "evaluationSettings can not be null"); + this.engineInitializationContext = engineInitializationContext; this.questionnaireResponseResolver = new ResourceResolver("QuestionnaireResponse", this.repository); this.questionnaireResolver = new ResourceResolver(QUESTIONNAIRE, this.repository); this.fhirVersion = this.repository.fhirContext().getVersion().getVersion(); @@ -137,7 +147,7 @@ public IBaseBundle extract( questionnaireId, parameters, data, - new LibraryEngine(repository, evaluationSettings)); + new LibraryEngine(repository, evaluationSettings, engineInitializationContext)); } public IBaseBundle extract( diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/ActivityDefinitionProcessorFactory.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/ActivityDefinitionProcessorFactory.java index e4c7dbb56c..1f5f1f553d 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/ActivityDefinitionProcessorFactory.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/ActivityDefinitionProcessorFactory.java @@ -1,14 +1,28 @@ package org.opencds.cqf.fhir.cr; import ca.uhn.fhir.repository.IRepository; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; +import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cr.activitydefinition.ActivityDefinitionProcessor; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.operations.IActivityDefinitionProcessor; import org.opencds.cqf.fhir.utility.repository.operations.IActivityDefinitionProcessorFactory; public class ActivityDefinitionProcessorFactory implements IActivityDefinitionProcessorFactory { + private final NpmPackageLoader npmPackageLoader; + private final EvaluationSettings evaluationSettings; + + public ActivityDefinitionProcessorFactory( + NpmPackageLoader npmPackageLoader, EvaluationSettings evaluationSettings) { + this.npmPackageLoader = npmPackageLoader; + this.evaluationSettings = evaluationSettings; + } + @Override public IActivityDefinitionProcessor create(IRepository repository) { - return new ActivityDefinitionProcessor(repository); + var engineInitializationContext = + new EngineInitializationContext(repository, npmPackageLoader, evaluationSettings); + return new ActivityDefinitionProcessor(repository, engineInitializationContext); } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/TestOperationProvider.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/TestOperationProvider.java index d87dd2a32d..f8b7c708bb 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/TestOperationProvider.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/TestOperationProvider.java @@ -1,10 +1,25 @@ package org.opencds.cqf.fhir.cr; import ca.uhn.fhir.context.FhirContext; +import javax.annotation.Nonnull; +import org.opencds.cqf.fhir.cql.EvaluationSettings; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.operations.RepositoryOperationProvider; public class TestOperationProvider { - public static RepositoryOperationProvider newProvider(FhirContext fhirContext) { - return new RepositoryOperationProvider(fhirContext, new ActivityDefinitionProcessorFactory(), null, null, null); + public static RepositoryOperationProvider newProvider( + FhirContext fhirContext, NpmPackageLoader npmPackageLoader, EvaluationSettings evaluationSettings) { + return new RepositoryOperationProvider( + fhirContext, + newActivityDefinitionProcessorFactory(npmPackageLoader, evaluationSettings), + null, + null, + null); + } + + @Nonnull + private static ActivityDefinitionProcessorFactory newActivityDefinitionProcessorFactory( + NpmPackageLoader npmPackageLoader, EvaluationSettings evaluationSettings) { + return new ActivityDefinitionProcessorFactory(npmPackageLoader, evaluationSettings); } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/activitydefinition/ActivityDefinitionProcessorTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/activitydefinition/ActivityDefinitionProcessorTests.java index a577d22d1e..f41bb66c24 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/activitydefinition/ActivityDefinitionProcessorTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/activitydefinition/ActivityDefinitionProcessorTests.java @@ -32,11 +32,14 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; +import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.activitydefinition.apply.IRequestResolverFactory; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.monad.Either3; import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; @TestInstance(Lifecycle.PER_CLASS) @@ -58,7 +61,10 @@ private IRepository createRepository(FhirContext fhirContext, String version) { } private ActivityDefinitionProcessor createProcessor(IRepository repository) { - return new ActivityDefinitionProcessor(repository); + var engineInitializationContext = + new EngineInitializationContext(repository, NpmPackageLoader.DEFAULT, EvaluationSettings.getDefault()); + + return new ActivityDefinitionProcessor(repository, engineInitializationContext); } @BeforeAll diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/activitydefinition/RequestResourceResolver.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/activitydefinition/RequestResourceResolver.java index 4370bd1af7..690ce42041 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/activitydefinition/RequestResourceResolver.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/activitydefinition/RequestResourceResolver.java @@ -7,6 +7,7 @@ import java.nio.file.Path; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.activitydefinition.apply.ApplyRequest; @@ -14,6 +15,7 @@ import org.opencds.cqf.fhir.cr.activitydefinition.apply.IRequestResolverFactory; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.model.FhirModelResolverCache; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; public class RequestResourceResolver { @@ -22,6 +24,7 @@ public class RequestResourceResolver { public static class Given { private IRequestResolverFactory resolverFactory; private IRepository repository; + private EngineInitializationContext engineInitializationContext; private String activityDefinitionId; public Given repository(IRepository repository) { @@ -36,6 +39,8 @@ public Given repositoryFor(FhirContext fhirContext, String repositoryPath) { fhirContext, Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); this.resolverFactory = IRequestResolverFactory.getDefault(fhirContext.getVersion().getVersion()); + this.engineInitializationContext = new EngineInitializationContext( + this.repository, NpmPackageLoader.DEFAULT, EvaluationSettings.getDefault()); return this; } @@ -55,12 +60,14 @@ public When when() { .getImplementingClass(); var activityDefinition = repository.read(activityDefinitionClass, Ids.newId(activityDefinitionClass, activityDefinitionId)); - return new When(repository, activityDefinition, buildResolver(activityDefinition)); + return new When( + repository, engineInitializationContext, activityDefinition, buildResolver(activityDefinition)); } } public static class When { private final IRepository repository; + private final EngineInitializationContext engineInitializationContext; private final IBaseResource activityDefinition; private final BaseRequestResourceResolver resolver; private IIdType subjectId; @@ -68,8 +75,13 @@ public static class When { private IIdType practitionerId; private IIdType organizationId; - When(IRepository repository, IBaseResource activityDefinition, BaseRequestResourceResolver resolver) { + When( + IRepository repository, + EngineInitializationContext engineInitializationContext, + IBaseResource activityDefinition, + BaseRequestResourceResolver resolver) { this.repository = repository; + this.engineInitializationContext = engineInitializationContext; this.activityDefinition = activityDefinition; this.resolver = resolver; } @@ -108,7 +120,7 @@ public IBaseResource resolve() { null, null, null, - new LibraryEngine(repository, EvaluationSettings.getDefault()), + new LibraryEngine(repository, EvaluationSettings.getDefault(), engineInitializationContext), FhirModelResolverCache.resolverForVersion( repository.fhirContext().getVersion().getVersion()))); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/cpg/r4/Library.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/cpg/r4/Library.java index 2347062ceb..48d15ec66c 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/cpg/r4/Library.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/cpg/r4/Library.java @@ -6,15 +6,18 @@ import ca.uhn.fhir.repository.IRepository; import java.nio.file.Path; import java.util.List; +import java.util.Optional; import java.util.function.Supplier; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Endpoint; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Parameters; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; public class Library { @@ -64,6 +67,7 @@ public static Library.Given given() { public static class Given { private IRepository repository; + private EngineInitializationContext engineInitializationContext; private EvaluationSettings evaluationSettings; public Given() { @@ -87,6 +91,10 @@ public Library.Given repositoryFor(String repositoryPath) { this.repository = new IgRepository( FhirContext.forR4Cached(), Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); + this.engineInitializationContext = new EngineInitializationContext( + this.repository, + NpmPackageLoader.DEFAULT, + Optional.ofNullable(this.evaluationSettings).orElse(EvaluationSettings.getDefault())); return this; } @@ -96,11 +104,11 @@ public Library.Given evaluationSettings(EvaluationSettings evaluationSettings) { } private R4CqlExecutionService buildCqlService() { - return new R4CqlExecutionService(repository, evaluationSettings); + return new R4CqlExecutionService(repository, evaluationSettings, engineInitializationContext); } private R4LibraryEvaluationService buildLibraryEvaluationService() { - return new R4LibraryEvaluationService(repository, evaluationSettings); + return new R4LibraryEvaluationService(repository, evaluationSettings, engineInitializationContext); } public Library.When when() { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/graphdefinition/TestGraphDefinition.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/graphdefinition/TestGraphDefinition.java index f4f4dfaa37..38582bb631 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/graphdefinition/TestGraphDefinition.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/graphdefinition/TestGraphDefinition.java @@ -10,16 +10,19 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.repository.IRepository; +import jakarta.annotation.Nonnull; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.util.Optional; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.IdType; import org.opencds.cqf.cql.engine.model.ModelResolver; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; @@ -28,6 +31,7 @@ import org.opencds.cqf.fhir.cr.graphdefintion.GraphDefinitionProcessor; import org.opencds.cqf.fhir.cr.graphdefintion.apply.ApplyRequest; import org.opencds.cqf.fhir.cr.graphdefintion.apply.ApplyRequestBuilder; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; @@ -60,10 +64,14 @@ public static Given given() { public static class Given { private IRepository repository; private EvaluationSettings evaluationSettings; + private NpmPackageLoader npmPackageLoader; + private EngineInitializationContext engineInitializationContext; public Given repository(IRepository repository) { this.repository = repository; this.evaluationSettings = EvaluationSettings.getDefault(); + this.npmPackageLoader = NpmPackageLoader.DEFAULT; + this.engineInitializationContext = buildEngineInitializationContext(); return this; } @@ -75,12 +83,15 @@ public Given evaluationSettings(EvaluationSettings evaluationSettings) { public Given repositoryFor(FhirContext fhirContext, String repositoryPath) { this.repository = new IgRepository( fhirContext, Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); + this.npmPackageLoader = NpmPackageLoader.DEFAULT; + this.engineInitializationContext = buildEngineInitializationContext(); return this; } public GraphDefinitionProcessor buildProcessor(IRepository repository) { if (repository instanceof IgRepository igRepository) { - igRepository.setOperationProvider(TestOperationProvider.newProvider(repository.fhirContext())); + igRepository.setOperationProvider(TestOperationProvider.newProvider( + repository.fhirContext(), npmPackageLoader, evaluationSettings)); } if (evaluationSettings == null) { evaluationSettings = EvaluationSettings.getDefault(); @@ -97,8 +108,16 @@ public GraphDefinitionProcessor buildProcessor(IRepository repository) { return new GraphDefinitionProcessor(repository); } + @Nonnull + private EngineInitializationContext buildEngineInitializationContext() { + return new EngineInitializationContext( + this.repository, + npmPackageLoader, + Optional.ofNullable(this.evaluationSettings).orElse(EvaluationSettings.getDefault())); + } + public When when() { - return new When(repository, buildProcessor(repository), evaluationSettings); + return new When(repository, engineInitializationContext, buildProcessor(repository), evaluationSettings); } } @@ -110,11 +129,13 @@ public static class When { public When( IRepository repository, + EngineInitializationContext engineInitializationContext, GraphDefinitionProcessor graphDefinitionProcessor, EvaluationSettings evaluationSettings) { this.repository = repository; this.processor = graphDefinitionProcessor; - this.applyRequestBuilder = new ApplyRequestBuilder(this.repository, evaluationSettings); + this.applyRequestBuilder = + new ApplyRequestBuilder(this.repository, evaluationSettings, engineInitializationContext); this.jsonParser = repository.fhirContext().newJsonParser(); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/graphdefinition/apply/ApplyRequestBuilderTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/graphdefinition/apply/ApplyRequestBuilderTest.java index 16cc62b484..e2d569a31b 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/graphdefinition/apply/ApplyRequestBuilderTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/graphdefinition/apply/ApplyRequestBuilderTest.java @@ -9,14 +9,17 @@ import ca.uhn.fhir.repository.IRepository; import org.hl7.fhir.r4.model.GraphDefinition; import org.hl7.fhir.r4.model.IdType; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.graphdefintion.apply.ApplyRequest; import org.opencds.cqf.fhir.cr.graphdefintion.apply.ApplyRequestBuilder; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; @ExtendWith(MockitoExtension.class) @@ -27,13 +30,23 @@ class ApplyRequestBuilderTest { private EvaluationSettings evaluationSettings = EvaluationSettings.getDefault(); + private EngineInitializationContext engineInitializationContext; + private FhirContext fhirContext = FhirContext.forR4Cached(); + @BeforeEach + void beforeEach() { + engineInitializationContext = + new EngineInitializationContext(repository, NpmPackageLoader.DEFAULT, evaluationSettings); + } + @Test void build_withoutSubject_throwsIllegalArgumentException() { when(repository.fhirContext()).thenReturn(fhirContext); - ApplyRequestBuilder builder = new ApplyRequestBuilder(repository, evaluationSettings); + ApplyRequestBuilder builder = new ApplyRequestBuilder( + repository, evaluationSettings, engineInitializationContext) + .withPractitioner("Practitioner/123"); IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, builder::buildApplyRequest); assertEquals("Missing required parameter: 'subject'", ex.getMessage()); @@ -43,8 +56,9 @@ void build_withoutSubject_throwsIllegalArgumentException() { void build_withoutPractitioner_throwsIllegalArgumentException() { when(repository.fhirContext()).thenReturn(fhirContext); - ApplyRequestBuilder builder = - new ApplyRequestBuilder(repository, evaluationSettings).withSubject("Patient/123"); + ApplyRequestBuilder builder = new ApplyRequestBuilder( + repository, evaluationSettings, engineInitializationContext) + .withSubject("Patient/123"); IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, builder::buildApplyRequest); assertEquals("Missing required parameter: 'practitioner'", ex.getMessage()); @@ -55,7 +69,8 @@ void build_withGraphDefinitionAndSubject_returnsApplyRequest() { IRepository localRepository = new InMemoryFhirRepository(fhirContext); IdType id = (IdType) localRepository.create(new GraphDefinition()).getId(); - ApplyRequestBuilder builder = new ApplyRequestBuilder(localRepository, evaluationSettings) + ApplyRequestBuilder builder = new ApplyRequestBuilder( + localRepository, evaluationSettings, engineInitializationContext) .withGraphDefinitionId(id) .withSubject("Patient/123") .withPractitioner("Practitioner/456") diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/library/LibraryProcessorTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/library/LibraryProcessorTests.java index 2ba3b0f98e..ba1606693b 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/library/LibraryProcessorTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/library/LibraryProcessorTests.java @@ -10,11 +10,13 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import java.nio.file.Path; +import javax.annotation.Nonnull; import org.hl7.fhir.r4.model.Library; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cr.common.DataRequirementsProcessor; @@ -25,6 +27,7 @@ import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.client.TerminologyServerClientSettings; import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; @ExtendWith(MockitoExtension.class) @@ -41,7 +44,8 @@ class LibraryProcessorTests { void defaultSettings() { var repository = new IgRepository(fhirContextR4, Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/r4")); - var processor = new LibraryProcessor(repository); + var engineInitializationContext = getEngineInitializationContext(repository); + var processor = new LibraryProcessor(repository, engineInitializationContext); assertNotNull(processor.evaluationSettings()); } @@ -58,6 +62,7 @@ void testRequest() { void processor() { var repository = new IgRepository(fhirContextR5, Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/r5")); + var engineInitializationContext = getEngineInitializationContext(repository); var packageProcessor = new PackageProcessor(repository); var releaseProcessor = new ReleaseProcessor(repository); var dataRequirementsProcessor = new DataRequirementsProcessor(repository); @@ -65,6 +70,7 @@ void processor() { var processor = new LibraryProcessor( repository, EvaluationSettings.getDefault(), + engineInitializationContext, new TerminologyServerClientSettings(), packageProcessor, releaseProcessor, @@ -193,4 +199,9 @@ void testPrefetchData() { .thenEvaluate() .hasResults(6); } + + @Nonnull + private EngineInitializationContext getEngineInitializationContext(IgRepository repository) { + return new EngineInitializationContext(repository, NpmPackageLoader.DEFAULT, EvaluationSettings.getDefault()); + } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/library/TestLibrary.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/library/TestLibrary.java index e0f4d989d7..0505247a56 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/library/TestLibrary.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/library/TestLibrary.java @@ -21,6 +21,7 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.List; +import java.util.Optional; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBackboneElement; import org.hl7.fhir.instance.model.api.IBaseBundle; @@ -29,6 +30,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.opencds.cqf.cql.engine.model.ModelResolver; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; @@ -40,6 +42,7 @@ import org.opencds.cqf.fhir.utility.client.TerminologyServerClientSettings; import org.opencds.cqf.fhir.utility.model.FhirModelResolverCache; import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; @@ -70,6 +73,8 @@ public static Given given() { public static class Given { private IRepository repository; + private NpmPackageLoader npmPackageLoader; + private EngineInitializationContext engineInitializationContext; private EvaluationSettings evaluationSettings; public Given repository(IRepository repository) { @@ -80,6 +85,11 @@ public Given repository(IRepository repository) { public Given repositoryFor(FhirContext fhirContext, String repositoryPath) { this.repository = new IgRepository( fhirContext, Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); + this.npmPackageLoader = NpmPackageLoader.DEFAULT; + this.engineInitializationContext = new EngineInitializationContext( + this.repository, + npmPackageLoader, + Optional.ofNullable(this.evaluationSettings).orElse(EvaluationSettings.getDefault())); return this; } @@ -90,7 +100,8 @@ public Given evaluationSettings(EvaluationSettings evaluationSettings) { public LibraryProcessor buildProcessor(IRepository repository) { if (repository instanceof IgRepository igRepository) { - igRepository.setOperationProvider(TestOperationProvider.newProvider(repository.fhirContext())); + igRepository.setOperationProvider(TestOperationProvider.newProvider( + repository.fhirContext(), npmPackageLoader, evaluationSettings)); } if (evaluationSettings == null) { evaluationSettings = EvaluationSettings.getDefault(); @@ -103,7 +114,11 @@ public LibraryProcessor buildProcessor(IRepository repository) { .getTerminologySettings() .setValuesetExpansionMode(VALUESET_EXPANSION_MODE.PERFORM_NAIVE_EXPANSION); } - return new LibraryProcessor(repository, evaluationSettings, new TerminologyServerClientSettings()); + return new LibraryProcessor( + repository, + evaluationSettings, + engineInitializationContext.withRepository(repository), + new TerminologyServerClientSettings()); } public When when() { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/dstu3/Measure.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/dstu3/Measure.java index 8449d7339e..526d6efe91 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/dstu3/Measure.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/dstu3/Measure.java @@ -8,6 +8,7 @@ import ca.uhn.fhir.repository.IRepository; import java.nio.file.Path; import java.util.Collections; +import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Collectors; import org.hl7.fhir.dstu3.model.Bundle; @@ -19,12 +20,15 @@ import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.dstu3.model.Reference; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; +import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants; import org.opencds.cqf.fhir.cr.measure.dstu3.Measure.SelectedGroup.SelectedReference; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; public class Measure { @@ -54,6 +58,7 @@ public static Given given() { public static class Given { private IRepository repository; + private EngineInitializationContext engineInitializationContext; private MeasureEvaluationOptions evaluationOptions; public Given() { @@ -79,6 +84,12 @@ public Given repositoryFor(String repositoryPath) { this.repository = new IgRepository( FhirContext.forDstu3Cached(), Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); + this.engineInitializationContext = new EngineInitializationContext( + this.repository, + NpmPackageLoader.DEFAULT, + Optional.ofNullable(this.evaluationOptions) + .map(MeasureEvaluationOptions::getEvaluationSettings) + .orElse(EvaluationSettings.getDefault())); return this; } @@ -88,7 +99,8 @@ public Given evaluationOptions(MeasureEvaluationOptions evaluationOptions) { } private Dstu3MeasureProcessor buildProcessor() { - return new Dstu3MeasureProcessor(repository, evaluationOptions, new Dstu3RepositorySubjectProvider()); + return new Dstu3MeasureProcessor( + repository, engineInitializationContext, evaluationOptions, new Dstu3RepositorySubjectProvider()); } public When when() { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CareGaps.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CareGaps.java index 92c3e702f4..2eb5641018 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CareGaps.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CareGaps.java @@ -12,10 +12,12 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.repository.IRepository; +import jakarta.annotation.Nonnull; import java.nio.file.Path; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.function.Supplier; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; @@ -33,12 +35,17 @@ import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.ResourceType; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; +import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE; import org.opencds.cqf.fhir.cr.measure.CareGapsProperties; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.MeasurePeriodValidator; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; +import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; public class CareGaps { @@ -89,6 +96,8 @@ public static Given given() { public static class Given { private IRepository repository; + private NpmPackageLoader npmPackageLoader = NpmPackageLoader.DEFAULT; + private EngineInitializationContext engineInitializationContext; private MeasureEvaluationOptions evaluationOptions; private CareGapsProperties careGapsProperties; private final String serverBase; @@ -124,9 +133,32 @@ public CareGaps.Given repositoryFor(String repositoryPath) { this.repository = new IgRepository( FhirContext.forR4Cached(), Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); + // We're explicitly NOT using NPM here + this.npmPackageLoader = NpmPackageLoader.DEFAULT; + this.engineInitializationContext = new EngineInitializationContext( + this.repository, + NpmPackageLoader.DEFAULT, + Optional.ofNullable(this.evaluationOptions) + .map(MeasureEvaluationOptions::getEvaluationSettings) + .orElse(EvaluationSettings.getDefault())); return this; } + // Use this if you wish to do anything with NPM + public Given repositoryPlusNpmFor(String repositoryPath) { + var igRepository = new IgRepository( + FhirContext.forR4Cached(), + Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); + this.repository = igRepository; + this.npmPackageLoader = igRepository.getNpmPackageLoader(); + mutateEvaluationOptionsToEnableNpm(); + return this; + } + + private void mutateEvaluationOptionsToEnableNpm() { + this.evaluationOptions.getEvaluationSettings().setUseNpmForQualifyingResources(true); + } + public CareGaps.Given evaluationOptions(MeasureEvaluationOptions evaluationOptions) { this.evaluationOptions = evaluationOptions; return this; @@ -138,8 +170,27 @@ public CareGaps.Given careGapsProperties(CareGapsProperties careGapsProperties) } private R4CareGapsService buildCareGapsService() { + var r4RepositoryOrNpmResourceProvider = getR4RepositoryOrNpmResourceProvider(); return new R4CareGapsService( - careGapsProperties, repository, evaluationOptions, serverBase, measurePeriodEvaluator); + careGapsProperties, + repository, + new R4MeasureServiceUtils(repository), + evaluationOptions, + serverBase, + new R4MultiMeasureService( + repository, + engineInitializationContext, + evaluationOptions, + serverBase, + measurePeriodEvaluator, + r4RepositoryOrNpmResourceProvider), + r4RepositoryOrNpmResourceProvider); + } + + @Nonnull + private R4RepositoryOrNpmResourceProvider getR4RepositoryOrNpmResourceProvider() { + return new R4RepositoryOrNpmResourceProvider( + repository, npmPackageLoader, evaluationOptions.getEvaluationSettings()); } public When when() { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CollectData.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CollectData.java index 960c2ac810..3cabba9d26 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CollectData.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/CollectData.java @@ -6,21 +6,26 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.repository.IRepository; +import jakarta.annotation.Nonnull; import java.nio.file.Path; import java.time.LocalDate; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.Optional; import java.util.function.Supplier; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Resource; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; +import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; -import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; public class CollectData { @@ -65,8 +70,9 @@ public static Given given() { public static class Given { private IRepository repository; + private EngineInitializationContext engineInitializationContext; private final MeasureEvaluationOptions evaluationOptions; - private final R4MeasureServiceUtils measureServiceUtils; + private NpmPackageLoader npmPackageLoader; public Given() { this.evaluationOptions = MeasureEvaluationOptions.defaultOptions(); @@ -80,8 +86,6 @@ public Given() { .getEvaluationSettings() .getTerminologySettings() .setValuesetExpansionMode(VALUESET_EXPANSION_MODE.PERFORM_NAIVE_EXPANSION); - - this.measureServiceUtils = new R4MeasureServiceUtils(repository); } public Given repository(IRepository repository) { @@ -93,16 +97,29 @@ public Given repositoryFor(String repositoryPath) { this.repository = new IgRepository( FhirContext.forR4Cached(), Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); + this.engineInitializationContext = new EngineInitializationContext( + this.repository, + NpmPackageLoader.DEFAULT, + Optional.ofNullable(this.evaluationOptions) + .map(MeasureEvaluationOptions::getEvaluationSettings) + .orElse(EvaluationSettings.getDefault())); return this; } private R4CollectDataService buildR4CollectDataService() { - return new R4CollectDataService(repository, evaluationOptions); + return new R4CollectDataService( + repository, engineInitializationContext, evaluationOptions, getR4RepositoryOrNpmResourceProvider()); } public When when() { return new When(buildR4CollectDataService()); } + + @Nonnull + private R4RepositoryOrNpmResourceProvider getR4RepositoryOrNpmResourceProvider() { + return new R4RepositoryOrNpmResourceProvider( + repository, npmPackageLoader, evaluationOptions.getEvaluationSettings()); + } } public static class When { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java index c69517447d..431b2882c8 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/Measure.java @@ -2,9 +2,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.opencds.cqf.fhir.cr.measure.common.MeasureInfo.EXT_URL; import static org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants.CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL; import static org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants.EXT_CRITERIA_REFERENCE_URL; @@ -17,6 +19,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.repository.IRepository; +import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.nio.file.Path; import java.time.LocalDate; @@ -27,12 +30,14 @@ import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.IdType; @@ -53,14 +58,18 @@ import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.ResourceType; import org.hl7.fhir.r4.model.StringType; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; +import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.MeasurePeriodValidator; import org.opencds.cqf.fhir.cr.measure.r4.Measure.SelectedGroup.SelectedReference; -import org.opencds.cqf.fhir.cr.measure.r4.utils.R4MeasureServiceUtils; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; +import org.opencds.cqf.fhir.utility.monad.Either3; import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.r4.ContainedHelper; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; @@ -71,7 +80,7 @@ public class Measure { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); @FunctionalInterface - interface Validator { + public interface Validator { void validate(T value); } @@ -118,9 +127,10 @@ public static Given given() { public static class Given { private IRepository repository; + private EngineInitializationContext engineInitializationContext; private MeasureEvaluationOptions evaluationOptions; private final MeasurePeriodValidator measurePeriodValidator; - private final R4MeasureServiceUtils measureServiceUtils; + private NpmPackageLoader npmPackageLoader; public Given(@Nullable Boolean applyScoringSetMembership) { this.evaluationOptions = MeasureEvaluationOptions.defaultOptions(); @@ -144,35 +154,77 @@ public Given(@Nullable Boolean applyScoringSetMembership) { .setValuesetExpansionMode(VALUESET_EXPANSION_MODE.PERFORM_NAIVE_EXPANSION); this.measurePeriodValidator = new MeasurePeriodValidator(); - - this.measureServiceUtils = new R4MeasureServiceUtils(repository); } public Given repository(IRepository repository) { this.repository = repository; + // We're explicitly NOT using NPM here + this.npmPackageLoader = NpmPackageLoader.DEFAULT; + this.engineInitializationContext = getEngineInitializationContext(); return this; } + // Use this if you wish to have nothing to do with NPM public Given repositoryFor(String repositoryPath) { this.repository = new IgRepository( FhirContext.forR4Cached(), Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); + // We're explicitly NOT using NPM here + this.npmPackageLoader = NpmPackageLoader.DEFAULT; + this.engineInitializationContext = getEngineInitializationContext(); + + return this; + } + // Use this if you wish to do anything with NPM + public Given repositoryPlusNpmFor(String repositoryPath) { + var igRepository = new IgRepository( + FhirContext.forR4Cached(), + Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); + this.repository = igRepository; + this.npmPackageLoader = igRepository.getNpmPackageLoader(); + this.engineInitializationContext = getEngineInitializationContext(); + mutateEvaluationSettingsToEnableNpm(); return this; } + private void mutateEvaluationSettingsToEnableNpm() { + this.evaluationOptions.getEvaluationSettings().setUseNpmForQualifyingResources(true); + } + public Given evaluationOptions(MeasureEvaluationOptions evaluationOptions) { this.evaluationOptions = evaluationOptions; return this; } private R4MeasureService buildMeasureService() { - return new R4MeasureService(repository, evaluationOptions, measurePeriodValidator); + return new R4MeasureService( + repository, + engineInitializationContext, + evaluationOptions, + measurePeriodValidator, + getR4RepositoryOrNpmResourceProvider()); } public When when() { return new When(buildMeasureService()); } + + @Nonnull + private EngineInitializationContext getEngineInitializationContext() { + return new EngineInitializationContext( + this.repository, + npmPackageLoader, + Optional.ofNullable(evaluationOptions) + .map(MeasureEvaluationOptions::getEvaluationSettings) + .orElse(EvaluationSettings.getDefault())); + } + + @Nonnull + private R4RepositoryOrNpmResourceProvider getR4RepositoryOrNpmResourceProvider() { + return new R4RepositoryOrNpmResourceProvider( + repository, npmPackageLoader, evaluationOptions.getEvaluationSettings()); + } } public static class When { @@ -183,6 +235,7 @@ public static class When { } private String measureId; + private CanonicalType measureUrl; private ZonedDateTime periodStart; private ZonedDateTime periodEnd; private String subject; @@ -199,6 +252,11 @@ public When measureId(String measureId) { return this; } + public When measureUrl(String measureUrl) { + this.measureUrl = new CanonicalType(measureUrl); + return this; + } + public When periodEnd(String periodEnd) { this.periodEnd = LocalDate.parse(periodEnd, DateTimeFormatter.ISO_LOCAL_DATE).atStartOfDay(ZoneId.systemDefault()); @@ -253,7 +311,7 @@ public When productLine(String productLine) { public When evaluate() { this.operation = () -> service.evaluate( - Eithers.forMiddle3(new IdType("Measure", measureId)), + deriveMeasureEither(measureId, measureUrl), periodStart, periodEnd, reportType, @@ -277,6 +335,20 @@ public SelectedReport then() { return new SelectedReport(this.operation.get()); } + + @Nonnull + private Either3 deriveMeasureEither( + @Nullable String measureId, @Nullable CanonicalType measureUrl) { + if (measureId != null) { + return Eithers.forMiddle3(new IdType("Measure", measureId)); + } + + if (measureUrl != null) { + return Eithers.forLeft3(measureUrl); + } + + throw new IllegalStateException("Expected either a measure ID or a measure URL but there is neither"); + } } public static class SelectedReport extends Selected { @@ -315,10 +387,25 @@ public SelectedReference reference(Selector referenceS } public SelectedReference evaluatedResource(String name) { - return this.reference(x -> x.getEvaluatedResource().stream() - .filter(y -> y.getReference().equals(name)) - .findFirst() - .get()); + return this.reference(measureReport -> reportSelectorByName(report(), name)); + } + + private Reference reportSelectorByName(MeasureReport measureReport, String name) { + var optResourceReference = measureReport.getEvaluatedResource().stream() + .filter(evaluatedResource -> + evaluatedResource.getReference().equals(name)) + .findFirst(); + + if (optResourceReference.isEmpty()) { + fail("No evaluated resource with name: %s within: %s" + .formatted( + name, + measureReport.getEvaluatedResource().stream() + .map(Reference::getReference) + .toList())); + } + + return optResourceReference.get(); } public SelectedReport hasEvaluatedResourceCount(int count) { @@ -746,7 +833,7 @@ private static String formatDate(Date javaUtilDate) { } } - static class SelectedExtension extends Selected { + public static class SelectedExtension extends Selected { public SelectedExtension(Extension value, SelectedReport parent) { super(value, parent); @@ -768,7 +855,7 @@ public SelectedExtension extensionHasSDEId(String id) { } } - static class SelectedContained extends Selected { + public static class SelectedContained extends Selected { public SelectedContained(Resource value, SelectedReport parent) { super(value, parent); @@ -848,12 +935,12 @@ public SelectedGroup hasDateOfCompliance() { .get(0) .getValue() .isEmpty()); - assertTrue( + assertInstanceOf( + Period.class, this.value() - .getExtensionsByUrl(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL) - .get(0) - .getValue() - instanceof Period); + .getExtensionsByUrl(CQFM_CARE_GAP_DATE_OF_COMPLIANCE_EXT_URL) + .get(0) + .getValue()); return this; } @@ -898,7 +985,7 @@ public SelectedStratifier stratifier( return new SelectedStratifier(s, this); } - static class SelectedReference extends Selected { + public static class SelectedReference extends Selected { public SelectedReference(Reference value, SelectedReport parent) { super(value, parent); @@ -963,7 +1050,7 @@ public SelectedReference hasPopulations(String... population) { } } - static class SelectedPopulation + public static class SelectedPopulation extends Selected { public SelectedPopulation(MeasureReportGroupPopulationComponent value, SelectedGroup parent) { @@ -975,6 +1062,11 @@ public SelectedPopulation hasCount(int count) { return this; } + public SelectedPopulation hasCode(String code) { + assertEquals(code, value().getCode().getCodingFirstRep().getCode()); + return this; + } + public SelectedPopulation hasSubjectResults() { assertNotNull(value().getSubjectResults().getReference()); return this; @@ -988,7 +1080,7 @@ public SelectedPopulation passes( } } - static class SelectedStratifier + public static class SelectedStratifier extends Selected { public SelectedStratifier(MeasureReportGroupStratifierComponent value, SelectedGroup parent) { @@ -1043,7 +1135,7 @@ public SelectedStratum stratum( } } - static class SelectedStratum extends Selected { + public static class SelectedStratum extends Selected { public SelectedStratum(MeasureReport.StratifierGroupComponent value, SelectedStratifier parent) { super(value, parent); @@ -1080,7 +1172,7 @@ public SelectedStratumPopulation population( } } - static class SelectedStratumPopulation + public static class SelectedStratumPopulation extends Selected { public SelectedStratumPopulation( diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasure.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasure.java index 1e74ae23af..6b180fa347 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasure.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/MultiMeasure.java @@ -9,6 +9,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.repository.IRepository; +import jakarta.annotation.Nonnull; import java.nio.file.Path; import java.time.LocalDate; import java.time.ZoneId; @@ -17,6 +18,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Optional; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -39,16 +41,20 @@ import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.ResourceType; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; +import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.terminology.TerminologySettings.VALUESET_EXPANSION_MODE; import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; import org.opencds.cqf.fhir.cr.measure.common.MeasurePeriodValidator; import org.opencds.cqf.fhir.cr.measure.constant.MeasureConstants; +import org.opencds.cqf.fhir.cr.measure.r4.npm.R4RepositoryOrNpmResourceProvider; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; @SuppressWarnings("squid:S1135") -class MultiMeasure { +public class MultiMeasure { public static final String CLASS_PATH = "org/opencds/cqf/fhir/cr/measure/r4"; @FunctionalInterface @@ -95,9 +101,11 @@ public static MultiMeasure.Given given() { public static class Given { private IRepository repository; + private EngineInitializationContext engineInitializationContext; private MeasureEvaluationOptions evaluationOptions; private String serverBase; - private MeasurePeriodValidator measurePeriodValidator; + private final MeasurePeriodValidator measurePeriodValidator; + private NpmPackageLoader npmPackageLoader; public Given() { this.evaluationOptions = MeasureEvaluationOptions.defaultOptions(); @@ -119,16 +127,44 @@ public Given() { public MultiMeasure.Given repository(IRepository repository) { this.repository = repository; + mutateEvaluationOptionsToDisableNpm(); + this.evaluationOptions.getEvaluationSettings().setUseNpmForQualifyingResources(false); return this; } - public MultiMeasure.Given repositoryFor(String repositoryPath) { + // Use this if you wish to have nothing to do with NPM + public Given repositoryFor(String repositoryPath) { this.repository = new IgRepository( FhirContext.forR4Cached(), Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); + // We're explicitly NOT using NPM here + this.npmPackageLoader = NpmPackageLoader.DEFAULT; + this.evaluationOptions.getEvaluationSettings().setUseNpmForQualifyingResources(false); + mutateEvaluationOptionsToDisableNpm(); + this.engineInitializationContext = buildEngineInitializationContext(); return this; } + // Use this if you wish to do anything with NPM + public Given repositoryPlusNpmFor(String repositoryPath) { + var igRepository = new IgRepository( + FhirContext.forR4Cached(), + Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); + this.repository = igRepository; + this.npmPackageLoader = igRepository.getNpmPackageLoader(); + mutateEvaluationOptionsToEnableNpm(); + this.engineInitializationContext = buildEngineInitializationContext(); + return this; + } + + private void mutateEvaluationOptionsToDisableNpm() { + this.evaluationOptions.getEvaluationSettings().setUseNpmForQualifyingResources(false); + } + + private void mutateEvaluationOptionsToEnableNpm() { + this.evaluationOptions.getEvaluationSettings().setUseNpmForQualifyingResources(true); + } + public MultiMeasure.Given evaluationOptions(MeasureEvaluationOptions evaluationOptions) { this.evaluationOptions = evaluationOptions; return this; @@ -149,8 +185,59 @@ public IRepository getRepository() { return this.repository; } + public IgRepository getIgRepository() { + if (this.repository == null) { + throw new IllegalStateException( + "Repository has not been set. Use 'repository' or 'repositoryFor' to set it."); + } + + if (this.repository instanceof IgRepository igRepository) { + return igRepository; + } + + return null; + } + + @Nonnull + private EngineInitializationContext buildEngineInitializationContext() { + return new EngineInitializationContext( + this.repository, + npmPackageLoader, + Optional.ofNullable(this.evaluationOptions) + .map(MeasureEvaluationOptions::getEvaluationSettings) + .orElse(EvaluationSettings.getDefault())); + } + + @Nonnull + public NpmPackageLoader getNpmPackageLoader() { + if (this.npmPackageLoader == null) { + throw new IllegalStateException("NpmPackageLoader has not been set"); + } + return this.npmPackageLoader; + } + + public EngineInitializationContext getEngineInitializationContext() { + if (this.engineInitializationContext == null) { + throw new IllegalStateException( + "EngineInitializationContext has not been set. Use 'repository' or 'repositoryFor' to set it."); + } + return this.engineInitializationContext; + } + private R4MultiMeasureService buildMeasureService() { - return new R4MultiMeasureService(repository, evaluationOptions, serverBase, measurePeriodValidator); + return new R4MultiMeasureService( + repository, + engineInitializationContext, + evaluationOptions, + serverBase, + measurePeriodValidator, + getR4RepositoryOrNpmResourceProvider()); + } + + @Nonnull + private R4RepositoryOrNpmResourceProvider getR4RepositoryOrNpmResourceProvider() { + return new R4RepositoryOrNpmResourceProvider( + repository, npmPackageLoader, evaluationOptions.getEvaluationSettings()); } public MultiMeasure.When when() { @@ -596,6 +683,11 @@ public SelectedReference

hasPopulations(String... population) { return this; } + + public SelectedReference

hasEvaluatedResourceReferenceCount(int count) { + assertEquals(count, this.value().getExtension().size()); + return this; + } } public static class SelectedPopulation diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessorTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessorTest.java deleted file mode 100644 index 6fb6d298e4..0000000000 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/R4MeasureProcessorTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.opencds.cqf.fhir.cr.measure.r4; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import java.util.List; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.ResourceType; -import org.junit.jupiter.api.Test; -import org.opencds.cqf.fhir.cql.Engines; -import org.opencds.cqf.fhir.cr.measure.MeasureEvaluationOptions; -import org.opencds.cqf.fhir.cr.measure.common.MeasureDef; -import org.opencds.cqf.fhir.cr.measure.common.MeasureProcessorUtils; -import org.opencds.cqf.fhir.cr.measure.r4.MultiMeasure.Given; - -class R4MeasureProcessorTest { - private static final Given GIVEN_REPO = MultiMeasure.given().repositoryFor("MinimalMeasureEvaluation"); - private static final IdType MINIMAL_COHORT_BOOLEAN_BASIS_SINGLE_GROUP = - new IdType(ResourceType.Measure.name(), "MinimalCohortBooleanBasisSingleGroup"); - private static final String SUBJECT_ID = "Patient/female-1914"; - - // This test could probably be improved with better data and more assertions, but it's to - // confirm that a method exposed for downstream works with reasonable sanity. - @Test - void evaluateMultiMeasureIdsWithCqlEngine() { - var repository = GIVEN_REPO.getRepository(); - var r4MeasureProcessor = new R4MeasureProcessor( - repository, MeasureEvaluationOptions.defaultOptions(), new MeasureProcessorUtils()); - - var cqlEngine = Engines.forRepository(repository); - - var results = r4MeasureProcessor.evaluateMultiMeasureIdsWithCqlEngine( - List.of(SUBJECT_ID), - List.of(MINIMAL_COHORT_BOOLEAN_BASIS_SINGLE_GROUP), - null, - null, - new Parameters(), - cqlEngine); - - assertNotNull(results); - var measureDef = new MeasureDef("", "", "", List.of(), List.of()); - var evaluationResults = - results.processMeasureForSuccessOrFailure(MINIMAL_COHORT_BOOLEAN_BASIS_SINGLE_GROUP, measureDef); - - assertNotNull(evaluationResults); - - var evaluationResult = evaluationResults.get(SUBJECT_ID); - assertNotNull(evaluationResult); - - var expressionResults = evaluationResult.expressionResults; - assertNotNull(expressionResults); - - var expressionResult = expressionResults.get("Initial Population"); - assertNotNull(expressionResult); - - var evaluatedResources = expressionResult.evaluatedResources(); - assertEquals(1, evaluatedResources.size()); - } -} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/BaseMeasureWithNpmForR4Test.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/BaseMeasureWithNpmForR4Test.java new file mode 100644 index 0000000000..249de7381e --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/BaseMeasureWithNpmForR4Test.java @@ -0,0 +1,74 @@ +package org.opencds.cqf.fhir.cr.measure.r4.npm; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Month; +import java.time.ZoneOffset; +import java.util.Date; + +public abstract class BaseMeasureWithNpmForR4Test { + + static final LocalDateTime LOCAL_DATE_TIME_2020_01_01 = + LocalDate.of(2020, Month.JANUARY, 1).atStartOfDay(); + static final LocalDateTime LOCAL_DATE_TIME_2021_01_01_MINUS_ONE_SECOND = + LocalDate.of(2021, Month.JANUARY, 1).atStartOfDay().minusNanos(1); + + static final LocalDateTime LOCAL_DATE_TIME_2021_01_01 = + LocalDate.of(2021, Month.JANUARY, 1).atStartOfDay(); + static final LocalDateTime LOCAL_DATE_TIME_2022_01_01_MINUS_ONE_SECOND = + LocalDate.of(2022, Month.JANUARY, 1).atStartOfDay().minusNanos(1); + + static final LocalDateTime LOCAL_DATE_TIME_2022_01_01 = + LocalDate.of(2022, Month.JANUARY, 1).atStartOfDay(); + static final LocalDateTime LOCAL_DATE_TIME_2023_01_01_MINUS_ONE_SECOND = + LocalDate.of(2023, Month.JANUARY, 1).atStartOfDay().minusNanos(1); + + static final LocalDateTime LOCAL_DATE_TIME_2024_01_01 = + LocalDate.of(2024, Month.JANUARY, 1).atStartOfDay(); + static final LocalDateTime LOCAL_DATE_TIME_2025_01_01_MINUS_ONE_SECOND = + LocalDate.of(2025, Month.JANUARY, 1).atStartOfDay().minusNanos(1); + + static final String PIPE = "|"; + static final String VERSION_0_1 = "0.1"; + static final String VERSION_0_2 = "0.2"; + static final String VERSION_0_5 = "0.5"; + + static final String PATIENT_FEMALE_1944 = "Patient/female-1944"; + static final String PATIENT_MALE_1944 = "Patient/male-1944"; + + static final String ENCOUNTER_MALE_1988_FINISHED_ENCOUNTER_1 = "Encounter/male-1988-finished-encounter-1"; + static final String ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1 = "Encounter/female-1944-finished-encounter-1"; + + static final String SIMPLE_ALPHA = "SimpleAlpha"; + static final String SIMPLE_BRAVO = "SimpleBravo"; + + static final String MULTILIB_CROSSPACKAGE_SOURCE_1 = "MultiLibCrossPackageSource1"; + static final String MULTILIB_CROSSPACKAGE_SOURCE_2 = "MultiLibCrossPackageSource2"; + + static final String SIMPLE_URL = "http://example.com"; + static final String MULTILIB_CROSSPACKAGE_SOURCE_URL = "http://multilib.cross.package.source.npm.opencds.org"; + + static final String SLASH_MEASURE_SLASH = "/Measure/"; + + static final String MEASURE_URL_ALPHA = SIMPLE_URL + SLASH_MEASURE_SLASH + SIMPLE_ALPHA; + static final String MEASURE_URL_ALPHA_WITH_VERSION = MEASURE_URL_ALPHA + PIPE + VERSION_0_2; + static final String MEASURE_URL_BRAVO = SIMPLE_URL + SLASH_MEASURE_SLASH + SIMPLE_BRAVO; + static final String MEASURE_URL_BRAVO_WITH_VERSION = MEASURE_URL_BRAVO + PIPE + VERSION_0_5; + + static final String MEASURE_URL_CROSSPACKAGE_SOURCE_1 = + MULTILIB_CROSSPACKAGE_SOURCE_URL + SLASH_MEASURE_SLASH + MULTILIB_CROSSPACKAGE_SOURCE_1; + static final String MEASURE_URL_CROSSPACKAGE_SOURCE_1_WITH_VERSION = + MEASURE_URL_CROSSPACKAGE_SOURCE_1 + PIPE + VERSION_0_1; + static final String MEASURE_URL_CROSSPACKAGE_SOURCE_2 = + MULTILIB_CROSSPACKAGE_SOURCE_URL + SLASH_MEASURE_SLASH + MULTILIB_CROSSPACKAGE_SOURCE_2; + static final String MEASURE_URL_CROSSPACKAGE_SOURCE_2_WITH_VERSION = + MEASURE_URL_CROSSPACKAGE_SOURCE_2 + PIPE + VERSION_0_1; + + static final String INITIAL_POPULATION = "initial-population"; + static final String DENOMINATOR = "denominator"; + static final String NUMERATOR = "numerator"; + + static Date toJavaUtilDate(LocalDateTime localDateTime) { + return Date.from(localDateTime.atZone(ZoneOffset.UTC).toInstant()); + } +} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/BaseR4RepositoryOrNpmResourceProviderTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/BaseR4RepositoryOrNpmResourceProviderTest.java new file mode 100644 index 0000000000..e0be6550b0 --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/BaseR4RepositoryOrNpmResourceProviderTest.java @@ -0,0 +1,300 @@ +package org.opencds.cqf.fhir.cr.measure.r4.npm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Stream; +import org.hl7.fhir.r4.model.CanonicalType; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Measure; +import org.hl7.fhir.r4.model.ResourceType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.cql.EvaluationSettings; +import org.opencds.cqf.fhir.utility.monad.Either3; +import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.MeasureOrNpmResourceHolder; +import org.opencds.cqf.fhir.utility.npm.MeasureOrNpmResourceHolderList; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; +import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; + +abstract class BaseR4RepositoryOrNpmResourceProviderTest { + + private static final String MEASURE_URL_HARD_CODED_1 = "http://example.com/Measure/HardCoded1"; + private static final IdType MEASURE_ID_HARD_CODED_1 = new IdType(ResourceType.Measure.name(), "HardCoded1"); + private static final String MEASURE_URL_HARD_CODED_2 = "http://example.com/Measure/HardCoded2"; + private static final IdType MEASURE_ID_HARD_CODED_2 = new IdType(ResourceType.Measure.name(), "HardCoded2"); + private static final String MEASURE_URL_HARD_CODED_3 = "http://example.com/Measure/HardCoded3"; + private static final IdType MEASURE_ID_HARD_CODED_3 = new IdType(ResourceType.Measure.name(), "HardCoded3"); + + private final EvaluationSettings evaluationSettings = EvaluationSettings.getDefault(); + + abstract IgRepository getIgRepository(); + + R4RepositoryOrNpmResourceProvider testSubject; + + @BeforeEach + void beforeEach() { + final IgRepository igRepository = getIgRepository(); + final NpmPackageLoader npmPackageLoader = igRepository.getNpmPackageLoader(); + evaluationSettings.setUseNpmForQualifyingResources(igRepository.hasNpm()); + testSubject = new R4RepositoryOrNpmResourceProvider(igRepository, npmPackageLoader, evaluationSettings); + } + + @Test + void resolveByUrl() { + final CanonicalType url = getMeasureUrl1(); + final Measure measure = testSubject.resolveByUrl(url); + + assertNotNull(measure); + assertEquals(url.asStringValue(), measure.getUrl()); + } + + @Test + void foldSingleMeasureEitherUrl() { + var url = getMeasureUrl1(); + var measureOrNpmResourceHolder = testSubject.foldMeasure(getMeasureEitherForFoldMeasuresUrl()); + + assertNotNull(measureOrNpmResourceHolder); + assertRepositoryOrNpm(measureOrNpmResourceHolder); + var measure = measureOrNpmResourceHolder.getMeasure(); + assertNotNull(measure); + assertEquals(url.asStringValue(), measure.getUrl()); + } + + @Test + abstract void foldSingleMeasureEitherId(); + + @Test + void foldSingleMeasureEitherMeasureResource() { + var measureOrNpmResourceHolder = testSubject.foldMeasure(getMeasureEitherForFoldMeasuresResource()); + + assertNotNull(measureOrNpmResourceHolder); + assertFalse(measureOrNpmResourceHolder.isNpm()); + var measure = measureOrNpmResourceHolder.getMeasure(); + assertNotNull(measure); + assertEquals(MEASURE_ID_HARD_CODED_1, measure.getIdElement()); + assertEquals(MEASURE_URL_HARD_CODED_1, measure.getUrl()); + } + + @Test + void foldMeasuresUrls() { + var measureEithers = Stream.of(getMeasureUrl1(), getMeasureUrl2(), getMeasureUrl3()) + .map(Eithers::forLeft3) + .toList(); + + var holderList = testSubject.foldMeasures(measureEithers); + assertNotNull(holderList); + assertRepositoryOrNpm(holderList); + + var measures = holderList.getMeasures(); + assertEquals(3, measures.size()); + var expectedMeasures = List.of( + getMeasureHardCoded(getMeasureId1(), getMeasureUrl1()), + getMeasureHardCoded(getMeasureId2(), getMeasureUrl2()), + getMeasureHardCoded(getMeasureId3(), getMeasureUrl3())); + + assertMeasuresEquals(expectedMeasures, holderList.getMeasures()); + } + + @Test + abstract void foldMeasuresIds(); + + @Test + void foldMeasuresResources() { + + var measureEithers = Stream.of(getMeasureHardCoded1(), getMeasureHardCoded2(), getMeasureHardCoded3()) + .map(Eithers::forRight3) + .toList(); + + var holderList = testSubject.foldMeasures(measureEithers); + assertNotNull(holderList); + assertNonNpm(holderList); + + var measures = holderList.getMeasures(); + assertEquals(3, measures.size()); + assertMeasuresEquals( + List.of(getMeasureHardCoded1(), getMeasureHardCoded2(), getMeasureHardCoded3()), + holderList.getMeasures()); + } + + @Test + void foldWithCustomIdTypeHandlerMeasureUrl() { + var url = getMeasureUrl1(); + var measureOrNpmResourceHolder = + testSubject.foldWithCustomIdTypeHandler(getMeasureEitherForFoldMeasuresUrl(), getCustomIdTypeHandler()); + + assertNotNull(measureOrNpmResourceHolder); + assertRepositoryOrNpm(measureOrNpmResourceHolder); + var measure = measureOrNpmResourceHolder.getMeasure(); + assertNotNull(measure); + assertEquals(url.asStringValue(), measure.getUrl()); + } + + @Test + abstract void foldWithCustomIdTypeHandlerMeasureId(); + + @Test + void foldWithCustomIdTypeHandlerMeasureResource() { + var measureOrNpmResourceHolder = testSubject.foldWithCustomIdTypeHandler( + getMeasureEitherForFoldMeasuresResource(), getCustomIdTypeHandler()); + + assertNotNull(measureOrNpmResourceHolder); + assertFalse(measureOrNpmResourceHolder.isNpm()); + + var measure = measureOrNpmResourceHolder.getMeasure(); + assertNotNull(measure); + assertEquals(MEASURE_ID_HARD_CODED_1, measure.getIdElement()); + assertEquals(MEASURE_URL_HARD_CODED_1, measure.getUrl()); + } + + @Test + void getMeasureEithersInvalidBothPopulated() { + var measureIds = List.of("x"); + var measureUrls = List.of("y"); + + assertThrows(InvalidRequestException.class, () -> testSubject.getMeasureEithers(measureIds, measureUrls)); + } + + @Test + void getMeasureEithersInvalidBothNull() { + assertTrue(testSubject.getMeasureEithers(null, null).isEmpty()); + } + + @Test + void getMeasureEithersInvalidBothEmpty() { + assertTrue(testSubject.getMeasureEithers(null, null).isEmpty()); + } + + @Test + void getMeasureEithersIds() { + var actualMeasureEithers = testSubject.getMeasureEithers(List.of("x", "y", "z"), null); + var expectedMeasureIds = Stream.of("x", "y", "z") + .map(IdType::new) + .map(Eithers::forMiddle3) + .toList(); + + assertNotNull(actualMeasureEithers); + assertEquals(expectedMeasureIds, actualMeasureEithers); + } + + @Test + void getMeasureEithersUrls() { + var actualMeasureEithers = + testSubject.getMeasureEithers(List.of(), List.of("fakeUrl1", "fakeUrl2", "fakeUrl3")); + var expectedMeasureEithers = Stream.of("fakeUrl1", "fakeUrl2", "fakeUrl3") + .map(CanonicalType::new) + .map(Eithers::forLeft3) + .toList(); + + assertNotNull(actualMeasureEithers); + assertMeasureEithers(expectedMeasureEithers, actualMeasureEithers); + } + + private Either3 getMeasureEitherForFoldMeasuresUrl() { + return Eithers.forLeft3(getMeasureUrl1()); + } + + Either3 getMeasureEitherForFoldMeasuresId() { + return Eithers.forMiddle3(getMeasureId1()); + } + + private Either3 getMeasureEitherForFoldMeasuresResource() { + return Eithers.forRight3(getMeasureHardCoded1()); + } + + Function getCustomIdTypeHandler() { + return idType -> (Measure) new Measure().setId(idType); + } + + private Measure getMeasureHardCoded1() { + return getMeasureHardCoded(MEASURE_ID_HARD_CODED_1, MEASURE_URL_HARD_CODED_1); + } + + private Measure getMeasureHardCoded2() { + return getMeasureHardCoded(MEASURE_ID_HARD_CODED_2, MEASURE_URL_HARD_CODED_2); + } + + private Measure getMeasureHardCoded3() { + return getMeasureHardCoded(MEASURE_ID_HARD_CODED_3, MEASURE_URL_HARD_CODED_3); + } + + Measure getMeasureHardCoded(IdType id, CanonicalType url) { + return (Measure) new Measure().setUrl(url.asStringValue()).setId(id); + } + + private Measure getMeasureHardCoded(IdType id, String url) { + return (Measure) new Measure().setUrl(url).setId(id); + } + + private void assertMeasureEithers( + List> expectedMeasureEithers, + List> actualMeasureEithers) { + + assertEquals(expectedMeasureEithers.size(), actualMeasureEithers.size()); + + for (int index = 0; index < expectedMeasureEithers.size(); index++) { + var expectedEither = expectedMeasureEithers.get(0); + var actualEither = actualMeasureEithers.get(0); + + assertEquals(expectedEither.isLeft(), actualEither.isLeft()); + assertEquals(expectedEither.isMiddle(), actualEither.isMiddle()); + assertEquals(expectedEither.isRight(), actualEither.isRight()); + + if (expectedEither.isLeft()) { + assertEquals( + expectedEither.leftOrThrow().getValueAsString(), + actualEither.leftOrThrow().getValueAsString()); + } else { + assertEquals(expectedEither, actualEither); + } + } + } + + void assertMeasuresEquals(List expectedMeasures, List actualMeasures) { + + assertEquals(expectedMeasures.size(), actualMeasures.size()); + + for (int index = 0; index < expectedMeasures.size(); index++) { + var expectedMeasure = expectedMeasures.get(index); + var actualMeasure = actualMeasures.get(index); + + assertEquals(expectedMeasure.getId(), actualMeasure.getId()); + assertEquals(expectedMeasure.getUrl(), actualMeasure.getUrl()); + } + } + + abstract CanonicalType getMeasureUrl1(); + + abstract CanonicalType getMeasureUrl2(); + + abstract CanonicalType getMeasureUrl3(); + + abstract IdType getMeasureId1(); + + abstract IdType getMeasureId2(); + + abstract IdType getMeasureId3(); + + void assertRepositoryOrNpm(MeasureOrNpmResourceHolderList holderList) { + for (MeasureOrNpmResourceHolder measureOrNpmResourceHolder : holderList.getMeasuresOrNpmResourceHolders()) { + assertRepositoryOrNpm(measureOrNpmResourceHolder); + } + } + + // In the case of Eithers with Measure resources, we always expect non-NPM, for now + private void assertNonNpm(MeasureOrNpmResourceHolderList holderList) { + for (MeasureOrNpmResourceHolder holder : holderList.measuresOrNpmResourceHolders()) { + // If we're passing through resources, we always mark them as non-NPM for now + assertFalse(holder.isNpm()); + } + } + + abstract void assertRepositoryOrNpm(MeasureOrNpmResourceHolder holder); +} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/MultiMeasureWithNpmForR4Test.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/MultiMeasureWithNpmForR4Test.java new file mode 100644 index 0000000000..8e99ad07f7 --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/MultiMeasureWithNpmForR4Test.java @@ -0,0 +1,322 @@ +package org.opencds.cqf.fhir.cr.measure.r4.npm; + +import org.hl7.fhir.r4.model.MeasureReport.MeasureReportStatus; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; +import org.opencds.cqf.fhir.cr.measure.r4.MultiMeasure; +import org.opencds.cqf.fhir.cr.measure.r4.MultiMeasure.Given; + +@SuppressWarnings({"java:S2699"}) +class MultiMeasureWithNpmForR4Test extends BaseMeasureWithNpmForR4Test { + private static final Given NPM_REPO_MULTI_MEASURE = MultiMeasure.given().repositoryPlusNpmFor("BasicNpmPackages"); + + @Test + void evaluateSucceedsWithMinimalMeasureAndSingleSubject() { + + NPM_REPO_MULTI_MEASURE + .when() + .measureUrl(MEASURE_URL_ALPHA) + .measureUrl(MEASURE_URL_BRAVO) + .subject(PATIENT_FEMALE_1944) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .evaluate() + .then() + .hasMeasureReportCount(2) + .hasMeasureReportCountPerUrl(1, MEASURE_URL_ALPHA_WITH_VERSION) + .hasMeasureReportCountPerUrl(1, MEASURE_URL_BRAVO_WITH_VERSION) + .measureReport(MEASURE_URL_ALPHA_WITH_VERSION, PATIENT_FEMALE_1944) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasMeasureReportStatus(MeasureReportStatus.COMPLETE) + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(1) + .up() + .up() + .up() + .measureReport(MEASURE_URL_BRAVO_WITH_VERSION, PATIENT_FEMALE_1944) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasMeasureReportStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(1) + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(0); + + NPM_REPO_MULTI_MEASURE + .when() + .measureUrl(MEASURE_URL_ALPHA_WITH_VERSION) + .measureUrl(MEASURE_URL_BRAVO_WITH_VERSION) + .subject(PATIENT_FEMALE_1944) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .evaluate() + .then() + .hasMeasureReportCount(2) + .hasMeasureReportCountPerUrl(1, MEASURE_URL_ALPHA_WITH_VERSION) + .hasMeasureReportCountPerUrl(1, MEASURE_URL_BRAVO_WITH_VERSION) + .measureReport(MEASURE_URL_ALPHA_WITH_VERSION, PATIENT_FEMALE_1944) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasMeasureReportStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(1) + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(1) + .up() + .up() + .up() + .measureReport(MEASURE_URL_BRAVO_WITH_VERSION, PATIENT_FEMALE_1944) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasMeasureReportStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(1) + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + // No match for the second library, so zero + .hasCount(0); + } + + @Test + void evaluateSucceedsWithMinimalMeasureAndAllSubjects() { + + NPM_REPO_MULTI_MEASURE + .when() + .measureUrl(MEASURE_URL_ALPHA) + .measureUrl(MEASURE_URL_BRAVO) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .evaluate() + .then() + .hasMeasureReportCount(20) + .hasMeasureReportCountPerUrl(10, MEASURE_URL_ALPHA_WITH_VERSION) + .hasMeasureReportCountPerUrl(10, MEASURE_URL_BRAVO_WITH_VERSION) + .measureReport(MEASURE_URL_ALPHA_WITH_VERSION, PATIENT_FEMALE_1944) + .hasMeasureReportStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(11) + .evaluatedResource("Encounter/female-1914-planned-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-1931-finished-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-1988-2-finished-encounter-invalid-period") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-1988-planned-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-1988-finished-encounter-2") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-2021-finished-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/male-1931-planned-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/male-1944-finished-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_1988_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/male-2022-finished-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(8) + .up() + .up() + .up() + .measureReport(MEASURE_URL_BRAVO_WITH_VERSION, PATIENT_FEMALE_1944) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasMeasureReportStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(11) + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(3); + + NPM_REPO_MULTI_MEASURE + .when() + .measureUrl(MEASURE_URL_ALPHA_WITH_VERSION) + .measureUrl(MEASURE_URL_BRAVO_WITH_VERSION) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .evaluate() + .then() + .hasMeasureReportCount(20) + .hasMeasureReportCountPerUrl(10, MEASURE_URL_ALPHA_WITH_VERSION) + .hasMeasureReportCountPerUrl(10, MEASURE_URL_BRAVO_WITH_VERSION) + .measureReport(MEASURE_URL_ALPHA_WITH_VERSION, PATIENT_FEMALE_1944) + .hasMeasureReportStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(11) + .evaluatedResource("Encounter/female-1914-planned-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-1931-finished-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-1988-2-finished-encounter-invalid-period") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-1988-planned-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-1988-finished-encounter-2") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-2021-finished-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/male-1931-planned-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/male-1944-finished-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_1988_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/male-2022-finished-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(8) + .up() + .up() + .up() + .measureReport(MEASURE_URL_BRAVO_WITH_VERSION, PATIENT_FEMALE_1944) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasMeasureReportStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(11) + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(3); + } + + @Test + void evaluateSucceedsWithMultiLibCrossPackageSingleSubject() { + NPM_REPO_MULTI_MEASURE + .when() + .measureUrl(MEASURE_URL_CROSSPACKAGE_SOURCE_1) + .measureUrl(MEASURE_URL_CROSSPACKAGE_SOURCE_2) + .subject(PATIENT_FEMALE_1944) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .evaluate() + .then() + .hasMeasureReportCount(2) + .hasMeasureReportCountPerUrl(1, MEASURE_URL_CROSSPACKAGE_SOURCE_1_WITH_VERSION) + .hasMeasureReportCountPerUrl(1, MEASURE_URL_CROSSPACKAGE_SOURCE_2_WITH_VERSION) + .measureReport(MEASURE_URL_CROSSPACKAGE_SOURCE_1_WITH_VERSION) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasMeasureReportStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(1) + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(1) + .up() + .up() + .up() + .measureReport(MEASURE_URL_CROSSPACKAGE_SOURCE_2_WITH_VERSION) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasMeasureReportStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(1) + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + // No match for the second library, so zero + .hasCount(0); + } + + @Test + void evaluateSucceedsWithMultiLibCrossPackageAllSubjects() { + NPM_REPO_MULTI_MEASURE + .when() + .measureUrl(MEASURE_URL_CROSSPACKAGE_SOURCE_1) + .measureUrl(MEASURE_URL_CROSSPACKAGE_SOURCE_2) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .evaluate() + .then() + .hasMeasureReportCount(20) + .hasMeasureReportCountPerUrl(10, MEASURE_URL_CROSSPACKAGE_SOURCE_1_WITH_VERSION) + .hasMeasureReportCountPerUrl(10, MEASURE_URL_CROSSPACKAGE_SOURCE_2_WITH_VERSION) + .measureReport(MEASURE_URL_CROSSPACKAGE_SOURCE_1_WITH_VERSION, PATIENT_FEMALE_1944) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasMeasureReportStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(11) + .evaluatedResource("Encounter/female-1914-planned-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-1931-finished-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-1988-2-finished-encounter-invalid-period") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-1988-planned-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-1988-finished-encounter-2") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/female-2021-finished-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/male-1931-planned-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/male-1944-finished-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_1988_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource("Encounter/male-2022-finished-encounter-1") + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(8) + .up() + .up() + .up() + .measureReport(MEASURE_URL_CROSSPACKAGE_SOURCE_2_WITH_VERSION, PATIENT_FEMALE_1944) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasMeasureReportStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(11) + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(3); + } +} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/R4RepositoryOrNpmResourceProviderNonNpmTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/R4RepositoryOrNpmResourceProviderNonNpmTest.java new file mode 100644 index 0000000000..9424180415 --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/R4RepositoryOrNpmResourceProviderNonNpmTest.java @@ -0,0 +1,105 @@ +package org.opencds.cqf.fhir.cr.measure.r4.npm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.List; +import java.util.stream.Stream; +import org.hl7.fhir.r4.model.CanonicalType; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Measure; +import org.hl7.fhir.r4.model.ResourceType; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.cr.measure.r4.MultiMeasure; +import org.opencds.cqf.fhir.cr.measure.r4.MultiMeasure.Given; +import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.MeasureOrNpmResourceHolder; +import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; + +class R4RepositoryOrNpmResourceProviderNonNpmTest extends BaseR4RepositoryOrNpmResourceProviderTest { + + private static final Given GIVEN_REPO = MultiMeasure.given().repositoryFor("MinimalMeasureEvaluation"); + + @Override + IgRepository getIgRepository() { + return GIVEN_REPO.getIgRepository(); + } + + @Override + CanonicalType getMeasureUrl1() { + return new CanonicalType("http://example.com/Measure/MinimalProportionNoBasisSingleGroup"); + } + + @Override + CanonicalType getMeasureUrl2() { + return new CanonicalType("http://example.com/Measure/MinimalCohortBooleanBasisSingleGroup"); + } + + @Override + CanonicalType getMeasureUrl3() { + return new CanonicalType("http://example.com/Measure/MinimalProportionResourceBasisSingleGroup"); + } + + @Override + IdType getMeasureId1() { + return new IdType(ResourceType.Measure.name(), "MinimalProportionNoBasisSingleGroup"); + } + + @Override + IdType getMeasureId2() { + return new IdType(ResourceType.Measure.name(), "MinimalCohortBooleanBasisSingleGroup"); + } + + @Override + IdType getMeasureId3() { + return new IdType(ResourceType.Measure.name(), "MinimalProportionResourceBasisSingleGroup"); + } + + @Test + @Override + void foldSingleMeasureEitherId() { + var measureOrNpmResourceHolder = testSubject.foldMeasure(getMeasureEitherForFoldMeasuresId()); + + assertNotNull(measureOrNpmResourceHolder); + } + + @Test + @Override + void foldMeasuresIds() { + var measureEithers = Stream.of(getMeasureId1(), getMeasureId2(), getMeasureId3()) + .map(Eithers::forMiddle3) + .toList(); + + var holderList = testSubject.foldMeasures(measureEithers); + assertNotNull(holderList); + + var measures = holderList.getMeasures(); + assertEquals(3, measures.size()); + + var expectedMeasures = List.of( + getMeasureHardCoded(getMeasureId1(), getMeasureUrl1()), + getMeasureHardCoded(getMeasureId2(), getMeasureUrl2()), + getMeasureHardCoded(getMeasureId3(), getMeasureUrl3())); + + assertMeasuresEquals(expectedMeasures, holderList.getMeasures()); + } + + @Test + @Override + void foldWithCustomIdTypeHandlerMeasureId() { + var measureId = getMeasureId1(); + var measureOrNpmResourceHolder = + testSubject.foldWithCustomIdTypeHandler(getMeasureEitherForFoldMeasuresId(), getCustomIdTypeHandler()); + + assertNotNull(measureOrNpmResourceHolder); + var measure = measureOrNpmResourceHolder.getMeasure(); + assertNotNull(measure); + assertEquals(measureId.asStringValue(), measure.getIdElement().asStringValue()); + } + + @Override + void assertRepositoryOrNpm(MeasureOrNpmResourceHolder holder) { + assertFalse(holder.isNpm()); + } +} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/R4RepositoryOrNpmResourceProviderWithNpmTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/R4RepositoryOrNpmResourceProviderWithNpmTest.java new file mode 100644 index 0000000000..e0af01492a --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/R4RepositoryOrNpmResourceProviderWithNpmTest.java @@ -0,0 +1,89 @@ +package org.opencds.cqf.fhir.cr.measure.r4.npm; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import java.util.stream.Stream; +import org.hl7.fhir.r4.model.CanonicalType; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Measure; +import org.hl7.fhir.r4.model.ResourceType; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.cr.measure.r4.MultiMeasure; +import org.opencds.cqf.fhir.cr.measure.r4.MultiMeasure.Given; +import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.MeasureOrNpmResourceHolder; +import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; + +class R4RepositoryOrNpmResourceProviderWithNpmTest extends BaseR4RepositoryOrNpmResourceProviderTest { + + private static final Given GIVEN_REPO = MultiMeasure.given().repositoryPlusNpmFor("BasicNpmPackages"); + + @Override + IgRepository getIgRepository() { + return GIVEN_REPO.getIgRepository(); + } + + @Override + CanonicalType getMeasureUrl1() { + return new CanonicalType("http://example.com/Measure/SimpleAlpha"); + } + + @Override + CanonicalType getMeasureUrl2() { + return new CanonicalType("http://example.com/Measure/SimpleBravo"); + } + + @Override + CanonicalType getMeasureUrl3() { + return new CanonicalType("http://with-derived-library.npm.opencds.org/Measure/WithDerivedLibrary"); + } + + @Override + IdType getMeasureId1() { + return new IdType(ResourceType.Measure.name(), "SimpleAlpha"); + } + + @Override + IdType getMeasureId2() { + return new IdType(ResourceType.Measure.name(), "SimpleBravo"); + } + + @Override + IdType getMeasureId3() { + return new IdType(ResourceType.Measure.name(), "WithDerivedLibrary"); + } + + @Test + @Override + void foldMeasuresIds() { + var measureEithers = Stream.of(getMeasureId1(), getMeasureId2(), getMeasureId3()) + .map(Eithers::forMiddle3) + .toList(); + + assertThrows(InvalidRequestException.class, () -> testSubject.foldMeasures(measureEithers)); + } + + @Test + @Override + void foldSingleMeasureEitherId() { + var measureEither = getMeasureEitherForFoldMeasuresId(); + assertThrows(InvalidRequestException.class, () -> testSubject.foldMeasure(measureEither)); + } + + @Test + @Override + void foldWithCustomIdTypeHandlerMeasureId() { + var measureEither = getMeasureEitherForFoldMeasuresId(); + var customIdTypeHandler = getCustomIdTypeHandler(); + assertThrows( + InvalidRequestException.class, + () -> testSubject.foldWithCustomIdTypeHandler(measureEither, customIdTypeHandler)); + } + + @Override + void assertRepositoryOrNpm(MeasureOrNpmResourceHolder holder) { + assertTrue(holder.isNpm()); + } +} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/SingleMeasureWithNpmForR4Test.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/SingleMeasureWithNpmForR4Test.java new file mode 100644 index 0000000000..a18d7af154 --- /dev/null +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/npm/SingleMeasureWithNpmForR4Test.java @@ -0,0 +1,407 @@ +package org.opencds.cqf.fhir.cr.measure.r4.npm; + +import org.hl7.fhir.r4.model.MeasureReport.MeasureReportStatus; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.cr.measure.common.MeasureEvalType; +import org.opencds.cqf.fhir.cr.measure.r4.Measure; +import org.opencds.cqf.fhir.cr.measure.r4.Measure.Given; + +@SuppressWarnings({"java:S2699"}) +class SingleMeasureWithNpmForR4Test extends BaseMeasureWithNpmForR4Test { + + private static final String WITH_DERIVED_LIBRARY = "WithDerivedLibrary"; + private static final String WITH_TWO_LAYERS_DERIVED_LIBRARIES = "WithTwoLayersDerivedLibraries"; + + private static final String DERIVED_URL = "http://with-derived-library.npm.opencds.org"; + private static final String DERIVED_TWO_LAYERS_URL = "http://with-two-layers-derived-libraries.npm.opencds.org"; + + private static final String MEASURE_URL_WITH_DERIVED_LIBRARY = + DERIVED_URL + SLASH_MEASURE_SLASH + WITH_DERIVED_LIBRARY; + private static final String MEASURE_URL_WITH_DERIVED_LIBRARY_WITH_VERSION = + MEASURE_URL_WITH_DERIVED_LIBRARY + PIPE + VERSION_0_2; + private static final String MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES = + DERIVED_TWO_LAYERS_URL + SLASH_MEASURE_SLASH + WITH_TWO_LAYERS_DERIVED_LIBRARIES; + private static final String MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_WITH_VERSION = + MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES + PIPE + VERSION_0_1; + + private static final String CROSS_PACKAGE_SOURCE = "CrossPackageSource"; + private static final String CROSS_PACKAGE_TARGET = "CrossPackageTarget"; + + private static final String CROSS_PACKAGE_SOURCE_URL = "http://cross.package.source.npm.opencds.org"; + + private static final String MEASURE_URL_CROSS_PACKAGE_SOURCE = + CROSS_PACKAGE_SOURCE_URL + SLASH_MEASURE_SLASH + CROSS_PACKAGE_SOURCE; + private static final String MEASURE_URL_CROSS_PACKAGE_SOURCE_WITH_VERSION = + MEASURE_URL_CROSS_PACKAGE_SOURCE + PIPE + VERSION_0_2; + + private static final Given NPM_REPO_SINGLE_MEASURE = Measure.given().repositoryPlusNpmFor("BasicNpmPackages"); + + private static final String PATIENT_MALE_1988 = "Patient/male-1988"; + private static final String ENCOUNTER_FEMALE_1914_PLANNED_ENCOUNTER_1 = "Encounter/female-1914-planned-encounter-1"; + private static final String ENCOUNTER_FEMALE_1931_FINISHED_ENCOUNTER_1 = + "Encounter/female-1931-finished-encounter-1"; + private static final String ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1 = + "Encounter/female-1944-finished-encounter-1"; + private static final String ENCOUNTER_FEMALE_1988_2_FINISHED_ENCOUNTER_INVALID_PERIOD = + "Encounter/female-1988-2-finished-encounter-invalid-period"; + private static final String ENCOUNTER_FEMALE_1988_PLANNED_ENCOUNTER_1 = "Encounter/female-1988-planned-encounter-1"; + private static final String ENCOUNTER_FEMALE_1988_FINISHED_ENCOUNTER_2 = + "Encounter/female-1988-finished-encounter-2"; + private static final String ENCOUNTER_FEMALE_2021_FINISHED_ENCOUNTER_1 = + "Encounter/female-2021-finished-encounter-1"; + private static final String ENCOUNTER_MALE_1931_PLANNED_ENCOUNTER_1 = "Encounter/male-1931-planned-encounter-1"; + private static final String ENCOUNTER_MALE_1944_FINISHED_ENCOUNTER_1 = "Encounter/male-1944-finished-encounter-1"; + private static final String ENCOUNTER_MALE_2022_FINISHED_ENCOUNTER_1 = "Encounter/male-2022-finished-encounter-1"; + + @Test + void evaluateSucceedsWithMinimalMeasureAndSingleSubject() { + + NPM_REPO_SINGLE_MEASURE + .when() + .measureUrl(MEASURE_URL_ALPHA) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .subject(PATIENT_FEMALE_1944) + .evaluate() + .then() + .hasMeasureUrl(MEASURE_URL_ALPHA_WITH_VERSION) + .hasPeriodStart(toJavaUtilDate(LOCAL_DATE_TIME_2021_01_01)) + .hasPeriodEnd(toJavaUtilDate(LOCAL_DATE_TIME_2022_01_01_MINUS_ONE_SECOND)) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(1) + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + // We match the patient and the single finished encounter, which matches Alpha's where + .hasCount(1); + + NPM_REPO_SINGLE_MEASURE + .when() + .measureUrl(MEASURE_URL_ALPHA_WITH_VERSION) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .subject(PATIENT_MALE_1944) + .evaluate() + .then() + .hasMeasureUrl(MEASURE_URL_ALPHA_WITH_VERSION) + .hasPeriodStart(toJavaUtilDate(LOCAL_DATE_TIME_2021_01_01)) + .hasPeriodEnd(toJavaUtilDate(LOCAL_DATE_TIME_2022_01_01_MINUS_ONE_SECOND)) + .hasSubjectReference(PATIENT_MALE_1944) + .hasStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(1) + .evaluatedResource(ENCOUNTER_MALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + // We match the patient and the single finished encounter, which matches Alpha's where + .hasCount(1); + } + + @Test + void evaluateSucceedsWithBasicPatientAndSingleSubject() { + + NPM_REPO_SINGLE_MEASURE + .when() + .measureUrl(MEASURE_URL_BRAVO) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .subject(PATIENT_MALE_1944) + .evaluate() + .then() + .hasMeasureUrl(MEASURE_URL_BRAVO_WITH_VERSION) + .hasPeriodStart(toJavaUtilDate(LOCAL_DATE_TIME_2024_01_01)) + .hasPeriodEnd(toJavaUtilDate(LOCAL_DATE_TIME_2025_01_01_MINUS_ONE_SECOND)) + .hasSubjectReference(PATIENT_MALE_1944) + .hasStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(1) + .evaluatedResource(ENCOUNTER_MALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + // there are 0 planned encounters corresponding to Bravo's where and the patient + .hasCount(0); + } + + @Test + void evaluateSucceedsWithBasicPatientAllSubjects() { + + NPM_REPO_SINGLE_MEASURE + .when() + .measureUrl(MEASURE_URL_BRAVO) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .evaluate() + .then() + .hasMeasureUrl(MEASURE_URL_BRAVO_WITH_VERSION) + .hasPeriodStart(toJavaUtilDate(LOCAL_DATE_TIME_2024_01_01)) + .hasPeriodEnd(toJavaUtilDate(LOCAL_DATE_TIME_2025_01_01_MINUS_ONE_SECOND)) + .hasStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(11) + .evaluatedResource(ENCOUNTER_FEMALE_1914_PLANNED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1931_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1988_2_FINISHED_ENCOUNTER_INVALID_PERIOD) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1988_PLANNED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1988_FINISHED_ENCOUNTER_2) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_2021_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_1931_PLANNED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_1988_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_2022_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + .hasCode("initial-population") + .hasCount(3); // there are 3 planned encounters which corresponds to Bravo's where + } + + @Test + void evaluateWithDerivedLibraryOneLayerAndSingleSubject() { + + NPM_REPO_SINGLE_MEASURE + .when() + .measureUrl(MEASURE_URL_ALPHA) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .subject(PATIENT_FEMALE_1944) + .evaluate() + .then() + .hasMeasureUrl(MEASURE_URL_ALPHA_WITH_VERSION) + .hasPeriodStart(toJavaUtilDate(LOCAL_DATE_TIME_2021_01_01)) + .hasPeriodEnd(toJavaUtilDate(LOCAL_DATE_TIME_2022_01_01_MINUS_ONE_SECOND)) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(1) + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(1); + + NPM_REPO_SINGLE_MEASURE + .when() + .measureUrl(MEASURE_URL_WITH_DERIVED_LIBRARY) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .subject(PATIENT_FEMALE_1944) + .evaluate() + .then() + .hasMeasureUrl(MEASURE_URL_WITH_DERIVED_LIBRARY_WITH_VERSION) + .hasPeriodStart(toJavaUtilDate(LOCAL_DATE_TIME_2021_01_01)) + .hasPeriodEnd(toJavaUtilDate(LOCAL_DATE_TIME_2022_01_01_MINUS_ONE_SECOND)) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(1) + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(1); + } + + @Test + void evaluateWithDerivedLibraryOneLayerAndAllSubjects() { + NPM_REPO_SINGLE_MEASURE + .when() + .measureUrl(MEASURE_URL_ALPHA) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .evaluate() + .then() + .hasMeasureUrl(MEASURE_URL_ALPHA_WITH_VERSION) + .hasPeriodStart(toJavaUtilDate(LOCAL_DATE_TIME_2021_01_01)) + .hasPeriodEnd(toJavaUtilDate(LOCAL_DATE_TIME_2022_01_01_MINUS_ONE_SECOND)) + .hasStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(11) + .evaluatedResource(ENCOUNTER_FEMALE_1914_PLANNED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1931_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1988_2_FINISHED_ENCOUNTER_INVALID_PERIOD) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1988_PLANNED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1988_FINISHED_ENCOUNTER_2) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_2021_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_1931_PLANNED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_1988_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_2022_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(8); + + NPM_REPO_SINGLE_MEASURE + .when() + .measureUrl(MEASURE_URL_WITH_DERIVED_LIBRARY) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .evaluate() + .then() + .hasMeasureUrl(MEASURE_URL_WITH_DERIVED_LIBRARY_WITH_VERSION) + .hasPeriodStart(toJavaUtilDate(LOCAL_DATE_TIME_2021_01_01)) + .hasPeriodEnd(toJavaUtilDate(LOCAL_DATE_TIME_2022_01_01_MINUS_ONE_SECOND)) + .hasStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(11) + .evaluatedResource(ENCOUNTER_FEMALE_1914_PLANNED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1931_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1988_2_FINISHED_ENCOUNTER_INVALID_PERIOD) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1988_PLANNED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_1988_FINISHED_ENCOUNTER_2) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_FEMALE_2021_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_1931_PLANNED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_1944_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_1988_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .evaluatedResource(ENCOUNTER_MALE_2022_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1) + .up() + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(8); + } + + @Test + void evaluateWithSingleMeasureDerivedLibraryTwoLayersOneSubject() { + + NPM_REPO_SINGLE_MEASURE + .when() + .measureUrl(MEASURE_URL_BRAVO) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .subject(PATIENT_MALE_1988) + .evaluate() + .then() + .hasMeasureUrl(MEASURE_URL_BRAVO_WITH_VERSION) + .hasPeriodStart(toJavaUtilDate(LOCAL_DATE_TIME_2024_01_01)) + .hasPeriodEnd(toJavaUtilDate(LOCAL_DATE_TIME_2025_01_01_MINUS_ONE_SECOND)) + .hasSubjectReference(PATIENT_MALE_1988) + .hasStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(1) + .evaluatedResource(ENCOUNTER_MALE_1988_FINISHED_ENCOUNTER_1) + .hasEvaluatedResourceReferenceCount(1); + + NPM_REPO_SINGLE_MEASURE + .when() + .measureUrl(MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .subject(PATIENT_FEMALE_1944) + .evaluate() + .then() + .hasMeasureUrl(MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_WITH_VERSION) + .hasPeriodStart(toJavaUtilDate(LOCAL_DATE_TIME_2022_01_01)) + .hasPeriodEnd(toJavaUtilDate(LOCAL_DATE_TIME_2023_01_01_MINUS_ONE_SECOND)) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(1) + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(1) + .up() + .population(DENOMINATOR) + .hasCount(0) + .up() + .population(NUMERATOR) + .hasCount(0); + } + + @Test + void evaluateWithDerivedLibraryTwoLayersAllSubjects() { + NPM_REPO_SINGLE_MEASURE + .when() + .measureUrl(MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .evaluate() + .then() + .hasMeasureUrl(MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_WITH_VERSION) + .hasPeriodStart(toJavaUtilDate(LOCAL_DATE_TIME_2022_01_01)) + .hasPeriodEnd(toJavaUtilDate(LOCAL_DATE_TIME_2023_01_01_MINUS_ONE_SECOND)) + .hasStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(11) + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(8) + .up() + .population(DENOMINATOR) + .hasCount(1) + .up() + .population(NUMERATOR) + .hasCount(0); + } + + @Test + void evaluateWithDerivedLibraryCrossPackageSingleSubject() { + + NPM_REPO_SINGLE_MEASURE + .when() + .measureUrl(MEASURE_URL_CROSS_PACKAGE_SOURCE) + .reportType(MeasureEvalType.SUBJECT.toCode()) + .subject(PATIENT_FEMALE_1944) + .evaluate() + .then() + .hasMeasureUrl(MEASURE_URL_CROSS_PACKAGE_SOURCE_WITH_VERSION) + .hasPeriodStart(toJavaUtilDate(LOCAL_DATE_TIME_2020_01_01)) + .hasPeriodEnd(toJavaUtilDate(LOCAL_DATE_TIME_2021_01_01_MINUS_ONE_SECOND)) + .hasSubjectReference(PATIENT_FEMALE_1944) + .hasStatus(MeasureReportStatus.COMPLETE) + .hasEvaluatedResourceCount(1) + .firstGroup() + .population(INITIAL_POPULATION) + .hasCount(1); + } +} diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtilsTest.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtilsTest.java index d8c8716dde..2efaa14f3d 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtilsTest.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/utils/R4MeasureServiceUtilsTest.java @@ -14,11 +14,15 @@ import java.util.List; import java.util.Optional; import java.util.stream.Stream; +import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.MeasureReport; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -29,6 +33,7 @@ import org.opencds.cqf.fhir.cr.measure.constant.MeasureReportConstants; import org.opencds.cqf.fhir.cr.measure.r4.R4MeasureEvalType; import org.opencds.cqf.fhir.utility.monad.Either; +import org.opencds.cqf.fhir.utility.monad.Either3; import org.opencds.cqf.fhir.utility.monad.Eithers; @ExtendWith(MockitoExtension.class) @@ -224,6 +229,36 @@ void productLine(@Nullable String productLine, @Nullable Extension expectedExten equalTo(expectedExtension.getValue().primitiveValue())); } + @Test + void getMeasureEitherBothNull() { + assertThrows(IllegalArgumentException.class, () -> R4MeasureServiceUtils.getMeasureEither(null, null)); + } + + @Test + void getMeasureEitherBothNotNull() { + var id = new IdType("yyy"); + assertThrows(IllegalArgumentException.class, () -> R4MeasureServiceUtils.getMeasureEither("xxx", id)); + } + + @Test + void getMeasureEitherUrl() { + final String url = "http://example.com"; + + final Either3 measureEither = R4MeasureServiceUtils.getMeasureEither(url, null); + + assertEquals(url, measureEither.leftOrThrow().primitiveValue()); + } + + @Test + void getMeasureEitherId() { + final IdType idType = new IdType("Measure/123"); + + final Either3 measureEither = + R4MeasureServiceUtils.getMeasureEither(null, idType); + + assertEquals(Eithers.forMiddle3(idType), measureEither); + } + private static Extension buildExtensionForProductLine(String productLine) { return new Extension() .setUrl(MeasureReportConstants.MEASUREREPORT_PRODUCT_LINE_EXT_URL) diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/plandefinition/PlanDefinitionProcessorTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/plandefinition/PlanDefinitionProcessorTests.java index 5065c63959..fef966ec4b 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/plandefinition/PlanDefinitionProcessorTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/plandefinition/PlanDefinitionProcessorTests.java @@ -12,12 +12,14 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import java.nio.file.Path; +import javax.annotation.Nonnull; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cr.activitydefinition.apply.IRequestResolverFactory; import org.opencds.cqf.fhir.cr.common.DataRequirementsProcessor; @@ -28,6 +30,7 @@ import org.opencds.cqf.fhir.utility.client.TerminologyServerClientSettings; import org.opencds.cqf.fhir.utility.model.FhirModelResolverCache; import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; @SuppressWarnings("squid:S2699") @@ -40,7 +43,8 @@ class PlanDefinitionProcessorTests { void defaultSettings() { var repository = new IgRepository(fhirContextR4, Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/r4")); - var processor = new PlanDefinitionProcessor(repository); + var engineInitializationContext = getEngineInitializationContext(repository); + var processor = new PlanDefinitionProcessor(repository, engineInitializationContext); assertNotNull(processor.evaluationSettings()); } @@ -48,6 +52,7 @@ void defaultSettings() { void processor() { var repository = new IgRepository(fhirContextR5, Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/r5")); + var engineInitializationContext = getEngineInitializationContext(repository); var modelResolver = FhirModelResolverCache.resolverForVersion(FhirVersionEnum.R5); var activityProcessor = new org.opencds.cqf.fhir.cr.activitydefinition.apply.ApplyProcessor( repository, IRequestResolverFactory.getDefault(FhirVersionEnum.R5)); @@ -57,8 +62,9 @@ void processor() { var processor = new PlanDefinitionProcessor( repository, EvaluationSettings.getDefault(), + engineInitializationContext, new TerminologyServerClientSettings(), - new ApplyProcessor(repository, modelResolver, activityProcessor), + new ApplyProcessor(repository, engineInitializationContext, modelResolver, activityProcessor), packageProcessor, dataRequirementsProcessor, activityProcessor, @@ -566,4 +572,9 @@ void dataRequirementsR5() { .thenDataRequirements() .hasDataRequirements(30); } + + @Nonnull + private EngineInitializationContext getEngineInitializationContext(IgRepository repository) { + return new EngineInitializationContext(repository, NpmPackageLoader.DEFAULT, EvaluationSettings.getDefault()); + } } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/plandefinition/TestPlanDefinition.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/plandefinition/TestPlanDefinition.java index de8dc7caa5..96ad22d429 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/plandefinition/TestPlanDefinition.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/plandefinition/TestPlanDefinition.java @@ -37,6 +37,7 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.json.JSONException; import org.opencds.cqf.cql.engine.model.ModelResolver; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.TERMINOLOGY_FILTER_MODE; @@ -50,6 +51,7 @@ import org.opencds.cqf.fhir.utility.client.TerminologyServerClientSettings; import org.opencds.cqf.fhir.utility.model.FhirModelResolverCache; import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; import org.skyscreamer.jsonassert.JSONAssert; @@ -81,16 +83,19 @@ public static Given given() { public static class Given { private IRepository repository; + private NpmPackageLoader npmPackageLoader; private EvaluationSettings evaluationSettings; public Given repository(IRepository repository) { this.repository = repository; + this.npmPackageLoader = NpmPackageLoader.DEFAULT; return this; } public Given repositoryFor(FhirContext fhirContext, String repositoryPath) { this.repository = new IgRepository( fhirContext, Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); + this.npmPackageLoader = NpmPackageLoader.DEFAULT; return this; } @@ -101,7 +106,8 @@ public Given evaluationSettings(EvaluationSettings evaluationSettings) { public PlanDefinitionProcessor buildProcessor(IRepository repository) { if (repository instanceof IgRepository igRepository) { - igRepository.setOperationProvider(TestOperationProvider.newProvider(repository.fhirContext())); + igRepository.setOperationProvider(TestOperationProvider.newProvider( + repository.fhirContext(), npmPackageLoader, evaluationSettings)); } if (evaluationSettings == null) { evaluationSettings = EvaluationSettings.getDefault(); @@ -114,7 +120,10 @@ public PlanDefinitionProcessor buildProcessor(IRepository repository) { .getTerminologySettings() .setValuesetExpansionMode(VALUESET_EXPANSION_MODE.PERFORM_NAIVE_EXPANSION); } - return new PlanDefinitionProcessor(repository, evaluationSettings, new TerminologyServerClientSettings()); + var engineInitializationContext = + new EngineInitializationContext(repository, NpmPackageLoader.DEFAULT, evaluationSettings); + return new PlanDefinitionProcessor( + repository, evaluationSettings, engineInitializationContext, new TerminologyServerClientSettings()); } public When when() { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessorTests.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessorTests.java index b0a6996749..27b73583e9 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessorTests.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/QuestionnaireProcessorTests.java @@ -24,12 +24,15 @@ import org.hl7.fhir.r4.model.QuestionnaireResponse; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; +import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cr.common.PackageProcessor; import org.opencds.cqf.fhir.cr.questionnaire.generate.GenerateProcessor; import org.opencds.cqf.fhir.cr.questionnaire.populate.PopulateProcessor; import org.opencds.cqf.fhir.cr.questionnaireresponse.TestQuestionnaireResponse; import org.opencds.cqf.fhir.utility.BundleHelper; import org.opencds.cqf.fhir.utility.Ids; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; @SuppressWarnings("squid:S2699") @@ -40,11 +43,15 @@ class QuestionnaireProcessorTests { new IgRepository(fhirContextR4, Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/r4")); private final IRepository repositoryR5 = new IgRepository(fhirContextR5, Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/r5")); + private final EngineInitializationContext engineInitializationContextR4 = + new EngineInitializationContext(repositoryR4, NpmPackageLoader.DEFAULT, EvaluationSettings.getDefault()); + private final EngineInitializationContext engineInitializationContextR5 = + new EngineInitializationContext(repositoryR5, NpmPackageLoader.DEFAULT, EvaluationSettings.getDefault()); @Test void processors() { var bundle = given().repository(repositoryR4) - .generateProcessor(new GenerateProcessor(repositoryR4)) + .generateProcessor(new GenerateProcessor(repositoryR4, engineInitializationContextR4)) .packageProcessor(new PackageProcessor(repositoryR4)) .populateProcessor(new PopulateProcessor()) .when() diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestItemGenerator.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestItemGenerator.java index b3404ada44..2f8aa8d3c7 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestItemGenerator.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestItemGenerator.java @@ -26,9 +26,12 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.json.JSONException; import org.opencds.cqf.cql.engine.model.ModelResolver; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; +import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.utility.Constants; import org.opencds.cqf.fhir.utility.model.FhirModelResolverCache; import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; import org.skyscreamer.jsonassert.JSONAssert; @@ -66,7 +69,10 @@ public Given repositoryFor(FhirContext fhirContext, String repositoryPath) { } public static QuestionnaireProcessor buildProcessor(IRepository repository) { - return new QuestionnaireProcessor(repository); + var engineInitializationContext = new EngineInitializationContext( + repository, NpmPackageLoader.DEFAULT, EvaluationSettings.getDefault()); + + return new QuestionnaireProcessor(repository, engineInitializationContext); } public When when() { diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestQuestionnaire.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestQuestionnaire.java index 769ac951c9..fa2dc0d091 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestQuestionnaire.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaire/TestQuestionnaire.java @@ -16,6 +16,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBackboneElement; import org.hl7.fhir.instance.model.api.IBaseBundle; @@ -25,6 +26,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.json.JSONException; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cql.LibraryEngine; import org.opencds.cqf.fhir.cql.engine.retrieve.RetrieveSettings.SEARCH_FILTER_MODE; @@ -42,6 +44,7 @@ import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.VersionUtilities; import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; import org.skyscreamer.jsonassert.JSONAssert; @@ -54,6 +57,7 @@ public static Given given() { public static class Given { private IRepository repository; + private EngineInitializationContext engineInitializationContext; private EvaluationSettings evaluationSettings; private IGenerateProcessor generateProcessor; private IPackageProcessor packageProcessor; @@ -62,12 +66,20 @@ public static class Given { public Given repository(IRepository repository) { this.repository = repository; + this.engineInitializationContext = new EngineInitializationContext( + this.repository, + NpmPackageLoader.DEFAULT, + Optional.ofNullable(this.evaluationSettings).orElse(EvaluationSettings.getDefault())); return this; } public Given repositoryFor(FhirContext fhirContext, String repositoryPath) { this.repository = new IgRepository( fhirContext, Path.of(getResourcePath(this.getClass()) + "/" + CLASS_PATH + "/" + repositoryPath)); + this.engineInitializationContext = new EngineInitializationContext( + this.repository, + NpmPackageLoader.DEFAULT, + Optional.ofNullable(this.evaluationSettings).orElse(EvaluationSettings.getDefault())); return this; } @@ -111,6 +123,7 @@ public QuestionnaireProcessor buildProcessor(IRepository repository) { return new QuestionnaireProcessor( repository, evaluationSettings, + engineInitializationContext, generateProcessor, packageProcessor, dataRequirementsProcessor, @@ -118,12 +131,13 @@ public QuestionnaireProcessor buildProcessor(IRepository repository) { } public When when() { - return new When(repository, buildProcessor(repository)); + return new When(repository, engineInitializationContext, buildProcessor(repository)); } } public static class When { private final IRepository repository; + private final EngineInitializationContext engineInitializationContext; private final QuestionnaireProcessor processor; private IPrimitiveType questionnaireUrl; private IIdType questionnaireId; @@ -137,8 +151,12 @@ public static class When { private Boolean isPut; private IIdType profileId; - When(IRepository repository, QuestionnaireProcessor processor) { + When( + IRepository repository, + EngineInitializationContext engineInitializationContext, + QuestionnaireProcessor processor) { this.repository = repository; + this.engineInitializationContext = engineInitializationContext; this.processor = processor; useServerData = true; } @@ -155,7 +173,7 @@ private PopulateRequest buildRequest() { launchContext, parameters, data, - new LibraryEngine(repository, processor.evaluationSettings), + new LibraryEngine(repository, processor.evaluationSettings, engineInitializationContext), processor.modelResolver); } @@ -252,7 +270,7 @@ public GeneratedQuestionnaire thenGenerate() { processor.resolveStructureDefinition(Eithers.for3(null, profileId, null)), false, true, - new LibraryEngine(repository, processor.evaluationSettings), + new LibraryEngine(repository, processor.evaluationSettings, engineInitializationContext), processor.modelResolver); return new GeneratedQuestionnaire(repository, request, processor.generateQuestionnaire(request, null)); } diff --git a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaireresponse/TestQuestionnaireResponse.java b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaireresponse/TestQuestionnaireResponse.java index 93f8459814..820addc9e5 100644 --- a/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaireresponse/TestQuestionnaireResponse.java +++ b/cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/questionnaireresponse/TestQuestionnaireResponse.java @@ -18,11 +18,13 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.opencds.cqf.cql.engine.model.ModelResolver; +import org.opencds.cqf.fhir.cql.Engines.EngineInitializationContext; import org.opencds.cqf.fhir.cql.EvaluationSettings; import org.opencds.cqf.fhir.cr.questionnaireresponse.extract.IExtractProcessor; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.model.FhirModelResolverCache; import org.opencds.cqf.fhir.utility.monad.Eithers; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; import org.opencds.cqf.fhir.utility.repository.ig.IgRepository; import org.skyscreamer.jsonassert.JSONAssert; @@ -53,10 +55,13 @@ public static Given given() { public static class Given { private IRepository repository; + private EngineInitializationContext engineInitializationContext; private IExtractProcessor extractProcessor; public Given repository(IRepository repository) { this.repository = repository; + this.engineInitializationContext = new EngineInitializationContext( + repository, NpmPackageLoader.DEFAULT, EvaluationSettings.getDefault()); return this; } @@ -72,7 +77,8 @@ public Given extractProcessor(IExtractProcessor extractProcessor) { } private QuestionnaireResponseProcessor buildProcessor() { - return new QuestionnaireResponseProcessor(repository, EvaluationSettings.getDefault(), extractProcessor); + return new QuestionnaireResponseProcessor( + repository, EvaluationSettings.getDefault(), engineInitializationContext, extractProcessor); } public When when() { diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/CrossPackageSource.tgz b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/CrossPackageSource.tgz new file mode 100644 index 0000000000..e9dda41ef4 Binary files /dev/null and b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/CrossPackageSource.tgz differ diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/CrossPackageTarget.tgz b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/CrossPackageTarget.tgz new file mode 100644 index 0000000000..dc09086b11 Binary files /dev/null and b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/CrossPackageTarget.tgz differ diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageSource1.tgz b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageSource1.tgz new file mode 100644 index 0000000000..8de5427d4f Binary files /dev/null and b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageSource1.tgz differ diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageSource2.tgz b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageSource2.tgz new file mode 100644 index 0000000000..7057eacfcf Binary files /dev/null and b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageSource2.tgz differ diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageTarget1A.tgz b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageTarget1A.tgz new file mode 100644 index 0000000000..31ecb2ade5 Binary files /dev/null and b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageTarget1A.tgz differ diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageTarget2A.tgz b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageTarget2A.tgz new file mode 100644 index 0000000000..7fe5db7c70 Binary files /dev/null and b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageTarget2A.tgz differ diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageTargetB.tgz b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageTargetB.tgz new file mode 100644 index 0000000000..2daecdc3bc Binary files /dev/null and b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/MultiLibCrossPackageTargetB.tgz differ diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/SimpleAlpha.tgz b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/SimpleAlpha.tgz new file mode 100644 index 0000000000..f1b6bd857b Binary files /dev/null and b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/SimpleAlpha.tgz differ diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/SimpleBravo.tgz b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/SimpleBravo.tgz new file mode 100644 index 0000000000..588f194880 Binary files /dev/null and b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/SimpleBravo.tgz differ diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/WithDerivedLibrary.tgz b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/WithDerivedLibrary.tgz new file mode 100644 index 0000000000..e6973991a9 Binary files /dev/null and b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/WithDerivedLibrary.tgz differ diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/WithTwoLayersDerivedLibraries.tgz b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/WithTwoLayersDerivedLibraries.tgz new file mode 100644 index 0000000000..633a504538 Binary files /dev/null and b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/npm/WithTwoLayersDerivedLibraries.tgz differ diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1914-planned-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1914-planned-encounter-1.json new file mode 100644 index 0000000000..fd07558b1c --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1914-planned-encounter-1.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "female-1914-planned-encounter-1", + "status": "planned", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/female-1914" + }, + "period": { + "start": "2020-01-16T20:00:00Z", + "end": "2020-01-16T21:00:00Z" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1931-finished-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1931-finished-encounter-1.json new file mode 100644 index 0000000000..8439cbb9a6 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1931-finished-encounter-1.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "female-1931-finished-encounter-1", + "status": "finished", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/female-1931" + }, + "period": { + "start": "2024-01-16T08:30:00-07:00", + "end": "2024-01-20T08:30:00-07:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1944-finished-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1944-finished-encounter-1.json new file mode 100644 index 0000000000..05ed2d47ea --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1944-finished-encounter-1.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "female-1944-finished-encounter-1", + "status": "finished", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/female-1944" + }, + "period": { + "start": "2024-01-16T08:30:00-07:00", + "end": "2024-01-20T08:30:00-07:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1988-2-finished-encounter-invalid-period.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1988-2-finished-encounter-invalid-period.json new file mode 100644 index 0000000000..4801217b3a --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1988-2-finished-encounter-invalid-period.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "female-1988-2-finished-encounter-invalid-period", + "status": "finished", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/female-1988-2" + }, + "period": { + "start": "2024-01-29T08:30:00-07:00", + "end": "2024-01-28T08:30:00-07:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1988-finished-encounter-2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1988-finished-encounter-2.json new file mode 100644 index 0000000000..2afb2161bb --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1988-finished-encounter-2.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "female-1988-finished-encounter-2", + "status": "finished", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/female-1988" + }, + "period": { + "start": "2024-01-16T08:30:00-07:00", + "end": "2024-01-28T08:30:00-07:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1988-planned-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1988-planned-encounter-1.json new file mode 100644 index 0000000000..c3a7e35133 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-1988-planned-encounter-1.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "female-1988-planned-encounter-1", + "status": "planned", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/female-1988" + }, + "period": { + "start": "2024-01-16T08:30:00-07:00", + "end": "2024-01-20T08:30:00-07:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-2021-finished-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-2021-finished-encounter-1.json new file mode 100644 index 0000000000..8ab6ac637a --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/female-2021-finished-encounter-1.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "female-2021-finished-encounter-1", + "status": "finished", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/female-2021" + }, + "period": { + "start": "2024-01-16T08:30:00-07:00", + "end": "2024-01-20T08:30:00-07:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/male-1931-planned-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/male-1931-planned-encounter-1.json new file mode 100644 index 0000000000..c53f485e5b --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/male-1931-planned-encounter-1.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "male-1931-planned-encounter-1", + "status": "planned", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/male-1931" + }, + "period": { + "start": "2024-01-16T08:30:00-07:00", + "end": "2024-01-20T08:30:00-07:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/male-1944-finished-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/male-1944-finished-encounter-1.json new file mode 100644 index 0000000000..43cf70889a --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/male-1944-finished-encounter-1.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "male-1944-finished-encounter-1", + "status": "finished", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/male-1944" + }, + "period": { + "start": "2024-01-16T08:30:00-07:00", + "end": "2024-01-20T08:30:00-07:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/male-1988-finished-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/male-1988-finished-encounter-1.json new file mode 100644 index 0000000000..6d96091c49 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/male-1988-finished-encounter-1.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "male-1988-finished-encounter-1", + "status": "finished", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/male-1988" + }, + "period": { + "start": "2024-01-16T08:30:00-07:00", + "end": "2024-01-20T08:30:00-07:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/male-2022-finished-encounter-1.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/male-2022-finished-encounter-1.json new file mode 100644 index 0000000000..cc08621b04 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/encounter/male-2022-finished-encounter-1.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Encounter", + "id": "male-2022-finished-encounter-1", + "status": "finished", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "IMP", + "display": "inpatient encounter" + }, + "type": [ { + "coding": [ { + "system": "http://snomed.info/sct", + "code": "32485007", + "display": "Hospital admission (procedure)" + } ] + } ], + "subject": { + "reference": "Patient/male-2022" + }, + "period": { + "start": "2024-01-16T08:30:00-07:00", + "end": "2024-01-20T08:30:00-07:00" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1914.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1914.json new file mode 100644 index 0000000000..8b6a33dc9a --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1914.json @@ -0,0 +1,9 @@ +{ + "resourceType": "Patient", + "id": "female-1914", + "gender": "female", + "birthDate": "1914-01-01", + "managingOrganization" : { + "reference": "Organization/organization-linked-by-managingOrganization" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1931.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1931.json new file mode 100644 index 0000000000..c316ec3f5d --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1931.json @@ -0,0 +1,9 @@ +{ + "resourceType": "Patient", + "id": "female-1931", + "gender": "female", + "birthDate": "1931-01-01", + "managingOrganization" : { + "reference": "Organization/organization-linked-by-partOf" + } +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1944.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1944.json new file mode 100644 index 0000000000..90da651fed --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1944.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "female-1944", + "gender": "female", + "birthDate": "1944-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1988-2.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1988-2.json new file mode 100644 index 0000000000..34121bfb79 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1988-2.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "female-1988-2", + "gender": "female", + "birthDate": "1988-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1988.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1988.json new file mode 100644 index 0000000000..309e71daa9 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-1988.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "female-1988", + "gender": "female", + "birthDate": "1988-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-2021.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-2021.json new file mode 100644 index 0000000000..50a7826f6d --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/female-2021.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "female-2021", + "gender": "female", + "birthDate": "2021-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/male-1931.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/male-1931.json new file mode 100644 index 0000000000..1745f67d6f --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/male-1931.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "male-1931", + "gender": "male", + "birthDate": "1931-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/male-1944.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/male-1944.json new file mode 100644 index 0000000000..080d86bb64 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/male-1944.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "male-1944", + "gender": "male", + "birthDate": "1944-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/male-1988.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/male-1988.json new file mode 100644 index 0000000000..eb88176d97 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/male-1988.json @@ -0,0 +1,6 @@ +{ + "resourceType": "Patient", + "id": "male-1988", + "gender": "male", + "birthDate": "1988-01-01" +} \ No newline at end of file diff --git a/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/male-2022.json b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/male-2022.json new file mode 100644 index 0000000000..e0cdd4ff54 --- /dev/null +++ b/cqf-fhir-cr/src/test/resources/org/opencds/cqf/fhir/cr/measure/r4/BasicNpmPackages/input/tests/patient/male-2022.json @@ -0,0 +1,8 @@ +{ + "resourceType": "Patient", + "id": "male-2022", + "gender": "male", + "birthDate": "2022-01-01", + "generalPractitioner": + {"reference": "Practitioner/tester"} +} \ No newline at end of file diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IAdapter.java index 34f4c0a36d..20ecec3a08 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IAdapter.java @@ -32,11 +32,11 @@ public interface IAdapter { */ T get(); - public FhirContext fhirContext(); + FhirContext fhirContext(); - public ModelResolver getModelResolver(); + ModelResolver getModelResolver(); - public default void setExtension(List> extensions) { + default void setExtension(List> extensions) { try { getModelResolver().setValue(get(), "extension", null); getModelResolver().setValue(get(), "extension", extensions); @@ -46,7 +46,7 @@ public default void setExtension(List> extensions } } - public default > void addExtension(E extension) { + default > void addExtension(E extension) { try { getModelResolver().setValue(get(), "extension", Collections.singletonList(extension)); } catch (Exception e) { @@ -55,33 +55,33 @@ public default void setExtension(List> extensions } } - public default boolean hasExtension() { + default boolean hasExtension() { return !getExtension().isEmpty(); } - public default boolean hasExtension(String url) { + default boolean hasExtension(String url) { return hasExtension(get(), url); } - public default > List getExtension() { + default > List getExtension() { return getExtension(get()); } - public default > E getExtensionByUrl(String url) { + default > E getExtensionByUrl(String url) { return getExtensionByUrl(get(), url); } - public default > List getExtensionsByUrl(String url) { + default > List getExtensionsByUrl(String url) { return getExtensionsByUrl(get(), url); } @SuppressWarnings("unchecked") - public default > List getExtension(IBase base) { + default > List getExtension(IBase base) { return resolvePathList(base, "extension").stream().map(e -> (E) e).collect(Collectors.toList()); } @SuppressWarnings("unchecked") - public default > List getExtensionsByUrl(IBase base, String url) { + default > List getExtensionsByUrl(IBase base, String url) { return getExtension(base).stream() .filter(e -> e.getUrl().equals(url)) .map(e -> (E) e) @@ -89,29 +89,29 @@ public default boolean hasExtension(String url) { } @SuppressWarnings("unchecked") - public default > E getExtensionByUrl(IBase base, String url) { + default > E getExtensionByUrl(IBase base, String url) { return getExtensionsByUrl(base, url).stream() .map(e -> (E) e) .findFirst() .orElse(null); } - public default Boolean hasExtension(IBase base, String url) { + default Boolean hasExtension(IBase base, String url) { return getExtension(base).stream().anyMatch(e -> e.getUrl().equals(url)); } @SuppressWarnings("unchecked") - public default List resolvePathList(IBase base, String path) { + default List resolvePathList(IBase base, String path) { var pathResult = getModelResolver().resolvePath(base, path); return pathResult instanceof List ? (List) pathResult : new ArrayList<>(); } @SuppressWarnings("unchecked") - public default List resolvePathList(IBase base, String path, Class clazz) { + default List resolvePathList(IBase base, String path, Class clazz) { return resolvePathList(base, path).stream().map(i -> (B) i).collect(Collectors.toList()); } - public default String resolvePathString(IBase base, String path) { + default String resolvePathString(IBase base, String path) { var result = resolvePath(base, path); if (result == null) { return null; @@ -127,96 +127,72 @@ public default String resolvePathString(IBase base, String path) { } } - public default IBase resolvePath(IBase base, String path) { + default IBase resolvePath(IBase base, String path) { return (IBase) getModelResolver().resolvePath(base, path); } @SuppressWarnings("unchecked") - public default B resolvePath(IBase base, String path, Class clazz) { + default B resolvePath(IBase base, String path, Class clazz) { return (B) resolvePath(base, path); } @SuppressWarnings("unchecked") static T newPeriod(FhirVersionEnum version) { - switch (version) { - case DSTU3: - return (T) new org.hl7.fhir.dstu3.model.Period(); - case R4: - return (T) new org.hl7.fhir.r4.model.Period(); - case R5: - return (T) new org.hl7.fhir.r5.model.Period(); - default: - throw new UnprocessableEntityException(UNSUPPORTED_VERSION.formatted(version.toString())); - } + return switch (version) { + case DSTU3 -> (T) new org.hl7.fhir.dstu3.model.Period(); + case R4 -> (T) new org.hl7.fhir.r4.model.Period(); + case R5 -> (T) new org.hl7.fhir.r5.model.Period(); + default -> throw new UnprocessableEntityException(String.format(UNSUPPORTED_VERSION, version.toString())); + }; } @SuppressWarnings("unchecked") static > T newStringType(FhirVersionEnum version, String string) { - switch (version) { - case DSTU3: - return (T) new org.hl7.fhir.dstu3.model.StringType(string); - case R4: - return (T) new org.hl7.fhir.r4.model.StringType(string); - case R5: - return (T) new org.hl7.fhir.r5.model.StringType(string); - default: - throw new UnprocessableEntityException(UNSUPPORTED_VERSION.formatted(version.toString())); - } + return switch (version) { + case DSTU3 -> (T) new org.hl7.fhir.dstu3.model.StringType(string); + case R4 -> (T) new org.hl7.fhir.r4.model.StringType(string); + case R5 -> (T) new org.hl7.fhir.r5.model.StringType(string); + default -> throw new UnprocessableEntityException(String.format(UNSUPPORTED_VERSION, version.toString())); + }; } @SuppressWarnings("unchecked") static > T newUriType(FhirVersionEnum version, String string) { - switch (version) { - case DSTU3: - return (T) new org.hl7.fhir.dstu3.model.UriType(string); - case R4: - return (T) new org.hl7.fhir.r4.model.UriType(string); - case R5: - return (T) new org.hl7.fhir.r5.model.UriType(string); - default: - throw new UnprocessableEntityException(UNSUPPORTED_VERSION.formatted(version.toString())); - } + return switch (version) { + case DSTU3 -> (T) new org.hl7.fhir.dstu3.model.UriType(string); + case R4 -> (T) new org.hl7.fhir.r4.model.UriType(string); + case R5 -> (T) new org.hl7.fhir.r5.model.UriType(string); + default -> throw new UnprocessableEntityException(String.format(UNSUPPORTED_VERSION, version.toString())); + }; } @SuppressWarnings("unchecked") static > T newUrlType(FhirVersionEnum version, String string) { - switch (version) { - case DSTU3: - return (T) new org.hl7.fhir.dstu3.model.UriType(string); - case R4: - return (T) new org.hl7.fhir.r4.model.UrlType(string); - case R5: - return (T) new org.hl7.fhir.r5.model.UrlType(string); - default: - throw new UnprocessableEntityException(UNSUPPORTED_VERSION.formatted(version.toString())); - } + return switch (version) { + case DSTU3 -> (T) new org.hl7.fhir.dstu3.model.UriType(string); + case R4 -> (T) new org.hl7.fhir.r4.model.UrlType(string); + case R5 -> (T) new org.hl7.fhir.r5.model.UrlType(string); + default -> throw new UnprocessableEntityException(String.format(UNSUPPORTED_VERSION, version.toString())); + }; } @SuppressWarnings("unchecked") static > T newDateType(FhirVersionEnum version, Date date) { - switch (version) { - case DSTU3: - return (T) new org.hl7.fhir.dstu3.model.DateType(date); - case R4: - return (T) new org.hl7.fhir.r4.model.DateType(date); - case R5: - return (T) new org.hl7.fhir.r5.model.DateType(date); - default: - throw new UnprocessableEntityException(UNSUPPORTED_VERSION.formatted(version.toString())); - } + return switch (version) { + case DSTU3 -> (T) new org.hl7.fhir.dstu3.model.DateType(date); + case R4 -> (T) new org.hl7.fhir.r4.model.DateType(date); + case R5 -> (T) new org.hl7.fhir.r5.model.DateType(date); + default -> throw new UnprocessableEntityException(String.format(UNSUPPORTED_VERSION, version.toString())); + }; } @SuppressWarnings("unchecked") static > T newDateTimeType(FhirVersionEnum version, Date date) { - switch (version) { - case DSTU3: - return (T) new org.hl7.fhir.dstu3.model.DateTimeType(date); - case R4: - return (T) new org.hl7.fhir.r4.model.DateTimeType(date); - case R5: - return (T) new org.hl7.fhir.r5.model.DateTimeType(date); - default: - throw new UnprocessableEntityException(UNSUPPORTED_VERSION.formatted(version.toString())); - } + return switch (version) { + case DSTU3 -> (T) new org.hl7.fhir.dstu3.model.DateTimeType(date); + case R4 -> (T) new org.hl7.fhir.r4.model.DateTimeType(date); + case R5 -> (T) new org.hl7.fhir.r5.model.DateTimeType(date); + default -> throw new UnprocessableEntityException(String.format(UNSUPPORTED_VERSION, version.toString())); + }; } } diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IAdapterFactory.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IAdapterFactory.java index 83139a37b0..294de64fec 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IAdapterFactory.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IAdapterFactory.java @@ -58,6 +58,14 @@ static IResourceAdapter createAdapterForResource(IBaseResource resource) { */ ILibraryAdapter createLibrary(IBaseResource library); + /** + * Creates an adapter that exposes common Measure operations across multiple versions of FHIR + * + * @param measure a FHIR Measure Resource + * @return an adapter exposing common api calls + */ + IMeasureAdapter createMeasure(IBaseResource measure); + /** * Creates an adapter that exposes common PlanDefinition operations across multiple versions of FHIR * diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IKnowledgeArtifactAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IKnowledgeArtifactAdapter.java index 7aa013a6f2..f7abc34325 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IKnowledgeArtifactAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IKnowledgeArtifactAdapter.java @@ -33,8 +33,8 @@ import org.opencds.cqf.fhir.utility.Canonicals; import org.opencds.cqf.fhir.utility.Constants; import org.opencds.cqf.fhir.utility.SearchHelper; -import org.opencds.cqf.fhir.utility.VersionComparator; import org.opencds.cqf.fhir.utility.VersionUtilities; +import org.opencds.cqf.fhir.utility.Versions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -338,12 +338,11 @@ static boolean isSupportedMetadataResource(IBaseResource resource) { } static Optional findLatestVersion(IBaseBundle bundle) { - var versionComparator = new VersionComparator(); var sorted = BundleHelper.getEntryResources(bundle).stream() .filter(IKnowledgeArtifactAdapter::isSupportedMetadataResource) .map(r -> (IKnowledgeArtifactAdapter) IAdapterFactory.forFhirVersion(r.getStructureFhirVersionEnum()) .createResource(r)) - .sorted((a, b) -> versionComparator.compare(a.getVersion(), b.getVersion())) + .sorted((a, b) -> Versions.compareVersions(a.getVersion(), b.getVersion())) .toList(); if (!sorted.isEmpty()) { return Optional.of(sorted.get(sorted.size() - 1).get()); diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IMeasureAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IMeasureAdapter.java index 2ce32ad4ac..794e5af087 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IMeasureAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/IMeasureAdapter.java @@ -1,6 +1,10 @@ package org.opencds.cqf.fhir.utility.adapter; +import java.util.List; + /** * This interface exposes common functionality across all FHIR Questionnaire versions. */ -public interface IMeasureAdapter extends IKnowledgeArtifactAdapter {} +public interface IMeasureAdapter extends IKnowledgeArtifactAdapter { + List getLibrary(); +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/AdapterFactory.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/AdapterFactory.java index 0eaa9bc11d..6223c3d0f4 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/AdapterFactory.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/AdapterFactory.java @@ -28,6 +28,7 @@ import org.opencds.cqf.fhir.utility.adapter.IGraphDefinitionAdapter; import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter; import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.opencds.cqf.fhir.utility.adapter.IMeasureAdapter; import org.opencds.cqf.fhir.utility.adapter.IParametersAdapter; import org.opencds.cqf.fhir.utility.adapter.IParametersParameterComponentAdapter; import org.opencds.cqf.fhir.utility.adapter.IPlanDefinitionAdapter; @@ -86,6 +87,11 @@ public ILibraryAdapter createLibrary(IBaseResource library) { return new LibraryAdapter((IDomainResource) library); } + @Override + public IMeasureAdapter createMeasure(IBaseResource measure) { + return new MeasureAdapter((IDomainResource) measure); + } + @Override public IAttachmentAdapter createAttachment(ICompositeType attachment) { return new AttachmentAdapter(attachment); diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/MeasureAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/MeasureAdapter.java index d20187b024..5fb47d8a44 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/MeasureAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/dstu3/MeasureAdapter.java @@ -49,6 +49,11 @@ public Measure copy() { private Library effectiveDataRequirements; private LibraryAdapter effectiveDataRequirementsAdapter; + @Override + public List getLibrary() { + return getMeasure().getLibrary().stream().map(Reference::getReference).toList(); + } + private String getEdrReferenceString(Extension edrExtension) { return edrExtension.getUrl().contains("cqfm") ? ((Reference) edrExtension.getValue()).getReference() diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/AdapterFactory.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/AdapterFactory.java index 1b31be7080..1bc8dd701a 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/AdapterFactory.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/AdapterFactory.java @@ -28,6 +28,7 @@ import org.opencds.cqf.fhir.utility.adapter.IGraphDefinitionAdapter; import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter; import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.opencds.cqf.fhir.utility.adapter.IMeasureAdapter; import org.opencds.cqf.fhir.utility.adapter.IParametersAdapter; import org.opencds.cqf.fhir.utility.adapter.IParametersParameterComponentAdapter; import org.opencds.cqf.fhir.utility.adapter.IPlanDefinitionAdapter; @@ -86,6 +87,11 @@ public ILibraryAdapter createLibrary(IBaseResource library) { return new LibraryAdapter((IDomainResource) library); } + @Override + public IMeasureAdapter createMeasure(IBaseResource measure) { + return new MeasureAdapter((IDomainResource) measure); + } + @Override public IAttachmentAdapter createAttachment(ICompositeType attachment) { return new AttachmentAdapter(attachment); diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/MeasureAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/MeasureAdapter.java index 2077a50338..87f62083a5 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/MeasureAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r4/MeasureAdapter.java @@ -10,6 +10,7 @@ import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.Library; import org.hl7.fhir.r4.model.Measure; +import org.hl7.fhir.r4.model.PrimitiveType; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.RelatedArtifact; import org.hl7.fhir.r4.model.UriType; @@ -50,6 +51,13 @@ public Measure copy() { private Library effectiveDataRequirements; private LibraryAdapter effectiveDataRequirementsAdapter; + @Override + public List getLibrary() { + return getMeasure().getLibrary().stream() + .map(PrimitiveType::getValueAsString) + .toList(); + } + private String getEdrReferenceString(Extension edrExtension) { return edrExtension.getUrl().contains("cqfm") ? ((Reference) edrExtension.getValue()).getReference() diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/AdapterFactory.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/AdapterFactory.java index ef9c59b7b2..7e7cf7e9b5 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/AdapterFactory.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/AdapterFactory.java @@ -28,6 +28,7 @@ import org.opencds.cqf.fhir.utility.adapter.IGraphDefinitionAdapter; import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter; import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.opencds.cqf.fhir.utility.adapter.IMeasureAdapter; import org.opencds.cqf.fhir.utility.adapter.IParametersAdapter; import org.opencds.cqf.fhir.utility.adapter.IParametersParameterComponentAdapter; import org.opencds.cqf.fhir.utility.adapter.IPlanDefinitionAdapter; @@ -86,6 +87,11 @@ public ILibraryAdapter createLibrary(IBaseResource library) { return new LibraryAdapter((IDomainResource) library); } + @Override + public IMeasureAdapter createMeasure(IBaseResource measure) { + return new MeasureAdapter((IDomainResource) measure); + } + @Override public IAttachmentAdapter createAttachment(ICompositeType attachment) { return new AttachmentAdapter(attachment); diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/MeasureAdapter.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/MeasureAdapter.java index 615cea4d8e..5aa4479ac3 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/MeasureAdapter.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/adapter/r5/MeasureAdapter.java @@ -10,6 +10,7 @@ import org.hl7.fhir.r5.model.Extension; import org.hl7.fhir.r5.model.Library; import org.hl7.fhir.r5.model.Measure; +import org.hl7.fhir.r5.model.PrimitiveType; import org.hl7.fhir.r5.model.Reference; import org.hl7.fhir.r5.model.RelatedArtifact; import org.hl7.fhir.r5.model.UriType; @@ -50,6 +51,13 @@ public Measure copy() { private Library effectiveDataRequirements; private LibraryAdapter effectiveDataRequirementsAdapter; + @Override + public List getLibrary() { + return getMeasure().getLibrary().stream() + .map(PrimitiveType::getValueAsString) + .toList(); + } + private String getEdrReferenceString(Extension edrExtension) { return edrExtension.getUrl().contains("cqfm") ? ((Reference) edrExtension.getValue()).getReference() diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/MeasureOrNpmResourceHolder.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/MeasureOrNpmResourceHolder.java new file mode 100644 index 0000000000..d8a74aaa0e --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/MeasureOrNpmResourceHolder.java @@ -0,0 +1,131 @@ +package org.opencds.cqf.fhir.utility.npm; + +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import jakarta.annotation.Nullable; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.CanonicalType; +import org.hl7.fhir.r4.model.Measure; +import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; + +/** + * This class supports polymorphic handling of a Measure that was either retrieved from the FHIR + * DB or an NPM package. + */ +public final class MeasureOrNpmResourceHolder { + + @Nullable + private final Measure measure; + + private final NpmResourceHolder npmResourceHolder; + + public boolean isNpm() { + return !NpmResourceHolder.EMPTY.equals(npmResourceHolder); + } + + public static MeasureOrNpmResourceHolder measureOnly(Measure measure) { + return new MeasureOrNpmResourceHolder(measure, NpmResourceHolder.EMPTY); + } + + public static MeasureOrNpmResourceHolder npmOnly(NpmResourceHolder npmResourceHolder) { + return new MeasureOrNpmResourceHolder(null, npmResourceHolder); + } + + private MeasureOrNpmResourceHolder(@Nullable Measure measure, NpmResourceHolder npmResourceHolder) { + if (measure == null && (NpmResourceHolder.EMPTY == npmResourceHolder)) { + throw new InternalErrorException("Measure and NpmResourceHolder cannot both be null"); + } + this.measure = measure; + this.npmResourceHolder = npmResourceHolder; + } + + public boolean hasNpmLibrary() { + return Optional.ofNullable(npmResourceHolder) + .flatMap(NpmResourceHolder::getOptMainLibrary) + .isPresent(); + } + + public boolean hasLibrary() { + if (measure == null && (NpmResourceHolder.EMPTY == npmResourceHolder)) { + throw new InvalidRequestException("Measure and NpmResourceHolder cannot both be null"); + } + + if (measure != null) { + return measure.hasLibrary(); + } + + return npmResourceHolder.getOptMainLibrary().isPresent(); + } + + public Optional getMainLibraryUrl() { + if (measure == null && (NpmResourceHolder.EMPTY == npmResourceHolder)) { + throw new InvalidRequestException("Measure and NpmResourceHolder cannot both be null"); + } + + if (measure != null) { + final List libraryUrls = measure.getLibrary(); + + if (libraryUrls.isEmpty()) { + return Optional.empty(); + } + + return Optional.ofNullable(libraryUrls.get(0).asStringValue()); + } + + return npmResourceHolder.getOptMainLibrary().map(ILibraryAdapter::getUrl); + } + + public IIdType getMeasureIdElement() { + return getMeasure().getIdElement(); + } + + public boolean hasMeasureUrl() { + return getMeasure().hasUrl(); + } + + public String getMeasureUrl() { + return getMeasure().getUrl(); + } + + public Measure getMeasure() { + var optMeasureFromNpm = Optional.ofNullable(npmResourceHolder).flatMap(NpmResourceHolder::getMeasure); + + if (optMeasureFromNpm.isPresent() && optMeasureFromNpm.get().get() instanceof Measure measureFromNpm) { + return measureFromNpm; + } + + return measure; + } + + public NpmResourceHolder npmResourceHolder() { + return npmResourceHolder; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + var that = (MeasureOrNpmResourceHolder) obj; + return Objects.equals(this.measure, that.measure) + && Objects.equals(this.npmResourceHolder, that.npmResourceHolder); + } + + @Override + public int hashCode() { + return Objects.hash(measure, npmResourceHolder); + } + + @Override + public String toString() { + return "MeasurePlusNpmResourceHolder[" + "measure=" + + measure + ", " + "npmResourceHolders=" + + npmResourceHolder + ']'; + } +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/MeasureOrNpmResourceHolderList.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/MeasureOrNpmResourceHolderList.java new file mode 100644 index 0000000000..dd3b62ce5f --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/MeasureOrNpmResourceHolderList.java @@ -0,0 +1,103 @@ +package org.opencds.cqf.fhir.utility.npm; + +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import java.util.List; +import java.util.Objects; +import org.hl7.fhir.r4.model.Measure; + +/** + * Effectively a list of {@link MeasureOrNpmResourceHolder} with some convenience methods + */ +public final class MeasureOrNpmResourceHolderList { + + private final List measuresPlusNpmResourceHolders; + + public static MeasureOrNpmResourceHolderList of(MeasureOrNpmResourceHolder measureOrNpmResourceHolder) { + return new MeasureOrNpmResourceHolderList(List.of(measureOrNpmResourceHolder)); + } + + public static MeasureOrNpmResourceHolderList of(List measureOrNpmResourceHolders) { + return new MeasureOrNpmResourceHolderList(measureOrNpmResourceHolders); + } + + public static MeasureOrNpmResourceHolderList of(Measure measure) { + return new MeasureOrNpmResourceHolderList(List.of(MeasureOrNpmResourceHolder.measureOnly(measure))); + } + + public static MeasureOrNpmResourceHolderList ofMeasures(List measures) { + return new MeasureOrNpmResourceHolderList( + measures.stream().map(MeasureOrNpmResourceHolder::measureOnly).toList()); + } + + private MeasureOrNpmResourceHolderList(List measuresPlusNpmResourceHolders) { + this.measuresPlusNpmResourceHolders = measuresPlusNpmResourceHolders; + } + + public List getMeasuresOrNpmResourceHolders() { + return measuresPlusNpmResourceHolders; + } + + List measures() { + return this.measuresPlusNpmResourceHolders.stream() + .map(MeasureOrNpmResourceHolder::getMeasure) + .toList(); + } + + public List npmResourceHolders() { + return this.measuresPlusNpmResourceHolders.stream() + .map(MeasureOrNpmResourceHolder::npmResourceHolder) + .toList(); + } + + public List getMeasures() { + return measuresPlusNpmResourceHolders.stream() + .map(MeasureOrNpmResourceHolder::getMeasure) + .toList(); + } + + public void checkMeasureLibraries() { + for (MeasureOrNpmResourceHolder measureOrNpmResourceHolder : measuresPlusNpmResourceHolders) { + if (!measureOrNpmResourceHolder.hasLibrary()) { + throw new InvalidRequestException("Measure %s does not have a primary library specified" + .formatted(measureOrNpmResourceHolder.getMeasureUrl())); + } + } + } + + public int size() { + return measuresPlusNpmResourceHolders.size(); + } + + public List measuresOrNpmResourceHolders() { + return measuresPlusNpmResourceHolders; + } + + public List getMeasureUrls() { + return this.measuresPlusNpmResourceHolders.stream() + .map(MeasureOrNpmResourceHolder::getMeasureUrl) + .toList(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + var that = (MeasureOrNpmResourceHolderList) obj; + return Objects.equals(this.measuresPlusNpmResourceHolders, that.measuresPlusNpmResourceHolders); + } + + @Override + public int hashCode() { + return Objects.hash(measuresPlusNpmResourceHolders); + } + + @Override + public String toString() { + return "MeasurePlusNpmResourceHolderList[" + "measuresPlusNpmResourceHolders=" + measuresPlusNpmResourceHolders + + ']'; + } +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmConfigDependencySubstitutor.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmConfigDependencySubstitutor.java new file mode 100644 index 0000000000..b7197d46aa --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmConfigDependencySubstitutor.java @@ -0,0 +1,24 @@ +package org.opencds.cqf.fhir.utility.npm; + +import java.util.Optional; + +/** + * This class is meant to be used from Spring configuration classes, in the case of any missing + * NpmPackageLoader bean definitions, which Spring will inject as empty Optionals. + *

+ * Helps implement a migration from the old world of FHIR/Repository based resources for Libraries, + * Measures and eventually other clinical intelligence resources (such as PlanDefinitions or + * ValueSets), and the new world where they're derived from NPM packages. + * If Spring config is missing an instance of {@link NpmPackageLoader}, then * return the default + * instance. + */ +public class NpmConfigDependencySubstitutor { + + private NpmConfigDependencySubstitutor() { + // static utility class + } + + public static NpmPackageLoader substituteNpmPackageLoaderIfEmpty(Optional optNpmPackageLoader) { + return NpmPackageLoader.getDefaultIfEmpty(optNpmPackageLoader.orElse(null)); + } +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmNamespaceManager.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmNamespaceManager.java new file mode 100644 index 0000000000..c983a2d076 --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmNamespaceManager.java @@ -0,0 +1,16 @@ +package org.opencds.cqf.fhir.utility.npm; + +import java.util.List; +import org.hl7.cql.model.NamespaceInfo; + +/** + * Load all {@link NamespaceInfo}s capturing package ID to URL mappings associated with the NPM + * packages maintained for clinical-reasoning NPM package users to be used to resolve cross-package + * Library/CQL dependencies. See {@link NpmPackageLoader}. + */ +public interface NpmNamespaceManager { + + NpmNamespaceManager DEFAULT = List::of; + + List getAllNamespaceInfos(); +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmPackageLoader.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmPackageLoader.java new file mode 100644 index 0000000000..ed151d9b1e --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmPackageLoader.java @@ -0,0 +1,171 @@ +package org.opencds.cqf.fhir.utility.npm; + +import ca.uhn.fhir.repository.IRepository; +import jakarta.annotation.Nullable; +import java.util.List; +import java.util.Optional; +import org.cqframework.cql.cql2elm.LibraryManager; +import org.cqframework.cql.cql2elm.LibrarySourceProvider; +import org.hl7.cql.model.ModelIdentifier; +import org.hl7.cql.model.NamespaceInfo; +import org.hl7.cql.model.NamespaceManager; +import org.hl7.elm.r1.VersionedIdentifier; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * FHIR version agnostic Interface for loading NPM resources including Measures, Libraries and + * NpmPackages as captured within {@link NpmResourceHolder}. + *

+ * This javadoc documents the entire NPM package feature in the clinical-reasoning project. Please + * read below: + *

+ * A downstream app from clinical-reasoning will be able to maintain Measures and Libraries loaded + * from NPM packages. Such Measures and Libraries will, for those clients implementing this + * feature, no longer be maintained in {@link IRepository} storage, unlike all other FHIR resources, + * such as Patients. + *

+ * Downstream apps are responsible for loading and retrieving such packages from implementations + * of the below interface. Additionally, they must map all package IDs to package URLs via a + * List of {@link NamespaceInfo}s. This is done via the + * {@link #initNamespaceMappings(LibraryManager)}, as due to how CQL libraries are loaded, it + * won't work automatically. + *

+ * The {@link NpmResourceHolder} class is used to capture the results of query the NPM + * package with a given measure URL. It's effectively a container for the Measure, its directly + * associated Library, and its NPM package information. In theory, there could be more than one + * NPM package for a given Measure. When CQL runs and calls a custom {@link LibrarySourceProvider}, + * it will first check to see if the directly associated Library matches the provided + * {@link VersionedIdentifier}. If not, it will query all NPM packages within the + * R4NpmResourceInfoForCql to find the Library. And if there is still no match, it will pass the + * VersionedIdentifier, and build a URL from the system and ID before calling + * {@link #loadLibraryByUrl(String)} to load that Library from another package, with the + * VersionedIdentifier already resolved correctly with the help of the NamespaceInfos provided above. + * The implementor is responsible for implementing loadLibraryByUrl to properly return the Library + * from any packages maintained by the application. + *

+ * The above should also work with multiple layers of includes across packages. + *

+ * This workflow is meant to be triggered by a new Measure operation provider: + * $evaluate-measure-by-url, which takes a canonical measure URL instead of a measure ID like + * $evaluate-measure. + *

+ * Example: Package with ID X and URL ... contains Measure ABC + * is associated with Library 123, which contains CQL that includes Library 456 from NPM Package + * with ID Y and URL ..., which contains both the Library and + * its CQL. When resolve the CQL include pointing to Package ID Y, the CQL engine must be able + * to read the namespace info and resolve ID Y to URL .... This + * can only be accomplished via an explicit mapping. + *

+ * Note that there is the real possibility of Measures corresponding to the same canonical URL + * among multiple NPM packages. As such, clients who unintentionally add Measures with the same + * URL in at least two different packages may see the Measure they're not expecting during an + * $evaluate-measure-by-url, and may file production issues accordingly. This may be mitigated + * by new APIs in IHapiPackageCacheManager. + */ +public interface NpmPackageLoader { + Logger logger = LoggerFactory.getLogger(NpmPackageLoader.class); + String LIBRARY_URL_TEMPLATE = "%s/Library/%s"; + + // effectively a no-op implementation + NpmPackageLoader DEFAULT = new NpmPackageLoader() { + + @Override + public NpmNamespaceManager getNamespaceManager() { + return NpmNamespaceManager.DEFAULT; + } + + @Override + public NpmResourceHolder loadNpmResources(IPrimitiveType measureUrl) { + return NpmResourceHolder.EMPTY; + } + + @Override + public Optional loadLibraryByUrl(String libraryUrl) { + return Optional.empty(); + } + }; + + /** + * @param measureUrl The Measure URL provided by the caller, corresponding to a Measure contained + * withing one of the stored NPM packages. + * @return The Measure corresponding to the URL. + */ + NpmResourceHolder loadNpmResources(IPrimitiveType measureUrl); + + /** + * Hackish: Either the downstream app injected this or we default to a NO-OP implementation. + * + * @param npmPackageLoader The NpmPackageLoader, if injected by the downstream app, + * otherwise null. + * @return Either the downstream app's NpmPackageLoaderor a no-op implementation. + */ + static NpmPackageLoader getDefaultIfEmpty(@Nullable NpmPackageLoader npmPackageLoader) { + return Optional.ofNullable(npmPackageLoader).orElse(NpmPackageLoader.DEFAULT); + } + + /** + * Ensure the passed Library gets initialized with the NPM namespace mappings belonging + * to this instance of NpmPackageLoader. + * + * @param libraryManager from the CQL Engine being used for an evaluation + */ + default void initNamespaceMappings(LibraryManager libraryManager) { + final List allNamespaceInfos = getAllNamespaceInfos(); + final NamespaceManager namespaceManager = libraryManager.getNamespaceManager(); + + for (NamespaceInfo namespaceInfo : allNamespaceInfos) { + // if we do this more than one time it won't error out subsequent times + namespaceManager.ensureNamespaceRegistered(namespaceInfo); + } + } + + /** + * @return All NamespaceInfos to map package IDs to package URLs for all NPM Packages maintained + * for clinical-reasoning NPM package to be used to resolve cross-package Library/CQL + * dependencies. + */ + default List getAllNamespaceInfos() { + return getNamespaceManager().getAllNamespaceInfos(); + } + + /** + * It's up to implementors to maintain the NamespaceManager that maintains the NamespaceInfos. + */ + NpmNamespaceManager getNamespaceManager(); + + default Optional findMatchingLibrary(VersionedIdentifier versionedIdentifier) { + return findLibraryFromUnrelatedNpmPackage(versionedIdentifier); + } + + default Optional findMatchingLibrary(ModelIdentifier modelIdentifier) { + return findLibraryFromUnrelatedNpmPackage(modelIdentifier); + } + + default Optional findLibraryFromUnrelatedNpmPackage(VersionedIdentifier versionedIdentifier) { + return loadLibraryByUrl(getUrl(versionedIdentifier)); + } + + default Optional findLibraryFromUnrelatedNpmPackage(ModelIdentifier modelIdentifier) { + return loadLibraryByUrl(getUrl(modelIdentifier)); + } + + private String getUrl(VersionedIdentifier versionedIdentifier) { + // We need this case because the CQL engine will do the right thing and populate the system + // in the cross-package target case + return LIBRARY_URL_TEMPLATE.formatted(versionedIdentifier.getSystem(), versionedIdentifier.getId()); + } + + static String getUrl(ModelIdentifier modelIdentifier) { + return LIBRARY_URL_TEMPLATE.formatted(modelIdentifier.getSystem(), modelIdentifier.getId()); + } + + /** + * @param libraryUrl The Library URL converted from a given + * withing one of the stored NPM packages. + * @return The Measure corresponding to the URL. + */ + Optional loadLibraryByUrl(String libraryUrl); +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmPackageLoaderInMemory.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmPackageLoaderInMemory.java new file mode 100644 index 0000000000..94fc0e4eaf --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmPackageLoaderInMemory.java @@ -0,0 +1,351 @@ +package org.opencds.cqf.fhir.utility.npm; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.regex.Pattern; +import org.hl7.cql.model.NamespaceInfo; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.utilities.npm.NpmPackage; +import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; +import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.opencds.cqf.fhir.utility.adapter.IMeasureAdapter; +import org.opencds.cqf.fhir.utility.adapter.IResourceAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Simplistic implementation of {@link NpmPackageLoader} that loads NpmPackages from the classpath + * and stores {@link NpmResourceHolder}s in a Map. This class is recommended for testing + * and NOT for production. + *

measureUrlToResourceInfo = new HashMap<>(); + private final Map libraryUrlToPackage = new HashMap<>(); + private final NpmNamespaceManager npmNamespaceManager; + + public static NpmPackageLoaderInMemory fromNpmPackageAbsolutePath(List tgzPaths) { + return fromNpmPackageAbsolutePath(null, tgzPaths); + } + + public static NpmPackageLoaderInMemory fromNpmPackageAbsolutePath( + NpmNamespaceManager npmNamespaceManager, List tgzPaths) { + final List npmPackages = buildNpmPackagesFromAbsolutePath(tgzPaths); + + return new NpmPackageLoaderInMemory(npmPackages, npmNamespaceManager); + } + + public static NpmPackageLoaderInMemory fromNpmPackageClasspath(Class clazz, Path... tgzPaths) { + return fromNpmPackageClasspath(null, clazz, tgzPaths); + } + + public static NpmPackageLoaderInMemory fromNpmPackageClasspath( + @Nullable NpmNamespaceManager npmNamespaceManager, Class clazz, Path... tgzPaths) { + return fromNpmPackageClasspath(npmNamespaceManager, clazz, Arrays.asList(tgzPaths)); + } + + public static NpmPackageLoaderInMemory fromNpmPackageClasspath(Class clazz, List tgzPaths) { + return fromNpmPackageClasspath(null, clazz, tgzPaths); + } + + public static NpmPackageLoaderInMemory fromNpmPackageClasspath( + @Nullable NpmNamespaceManager npmNamespaceManager, Class clazz, List tgzPaths) { + final List npmPackages = buildNpmPackageFromClasspath(clazz, tgzPaths); + + return new NpmPackageLoaderInMemory(npmPackages, npmNamespaceManager); + } + + record UrlAndVersion(String url, @Nullable String version) { + + static UrlAndVersion fromCanonical(String canonical) { + final String[] parts = PATTERN_PIPE.split(canonical); + if (parts.length > 2) { + throw new IllegalArgumentException("Invalid canonical URL: " + canonical); + } + if (parts.length == 1) { + return new UrlAndVersion(parts[0], null); + } + return new UrlAndVersion(parts[0], parts[1]); + } + + static UrlAndVersion fromCanonicalAndVersion(String canonical, @Nullable String version) { + if (version == null) { + return new UrlAndVersion(canonical, null); + } + + return new UrlAndVersion(canonical, version); + } + + @Override + @Nonnull + public String toString() { + return url + "|" + version; + } + } + + @Override + public NpmResourceHolder loadNpmResources(IPrimitiveType measureUrl) { + return measureUrlToResourceInfo.entrySet().stream() + .filter(entry -> doUrlAndVersionMatch(measureUrl, entry)) + .map(Map.Entry::getValue) + .findFirst() + .orElse(NpmResourceHolder.EMPTY); + } + + @Override + public Optional loadLibraryByUrl(String url) { + for (NpmPackage npmPackage : libraryUrlToPackage.values()) { + final FhirContext fhirContext = getFhirContext(npmPackage); + try (InputStream libraryInputStream = npmPackage.loadByCanonical(url)) { + if (libraryInputStream != null) { + final IResourceAdapter resourceAdapter = IAdapterFactory.createAdapterForResource( + fhirContext.newJsonParser().parseResource(libraryInputStream)); + if (resourceAdapter instanceof ILibraryAdapter libraryAdapter) { + return Optional.of(libraryAdapter); + } + } + } catch (IOException exception) { + throw new InternalErrorException(exception); + } + } + return Optional.empty(); + } + + @Override + public NpmNamespaceManager getNamespaceManager() { + return npmNamespaceManager; + } + + @Nonnull + private static List buildNpmPackagesFromAbsolutePath(List tgzPaths) { + return tgzPaths.stream() + .map(NpmPackageLoaderInMemory::getNpmPackageFromAbsolutePaths) + .toList(); + } + + @Nonnull + private static List buildNpmPackageFromClasspath(Class clazz, List tgzPaths) { + return tgzPaths.stream() + .map(path -> getNpmPackageFromClasspath(clazz, path)) + .toList(); + } + + @Nonnull + private static NpmPackage getNpmPackageFromAbsolutePaths(Path tgzPath) { + try (final InputStream npmStream = Files.newInputStream(tgzPath)) { + return NpmPackage.fromPackage(npmStream); + } catch (IOException exception) { + throw new InvalidRequestException(FAILED_TO_LOAD_RESOURCE_TEMPLATE.formatted(tgzPath), exception); + } + } + + @Nonnull + private static NpmPackage getNpmPackageFromClasspath(Class clazz, Path tgzClasspathPath) { + try (final InputStream simpleAlphaStream = clazz.getResourceAsStream(tgzClasspathPath.toString())) { + if (simpleAlphaStream == null) { + throw new InvalidRequestException(FAILED_TO_LOAD_RESOURCE_TEMPLATE.formatted(tgzClasspathPath)); + } + + return NpmPackage.fromPackage(simpleAlphaStream); + } catch (IOException exception) { + throw new InvalidRequestException(FAILED_TO_LOAD_RESOURCE_TEMPLATE.formatted(tgzClasspathPath), exception); + } + } + + private NpmPackageLoaderInMemory(List npmPackages, @Nullable NpmNamespaceManager npmNamespaceManager) { + + if (npmNamespaceManager == null) { + var namespaceInfos = npmPackages.stream() + .map(npmPackage -> new NamespaceInfo(npmPackage.name(), npmPackage.canonical())) + .toList(); + + this.npmNamespaceManager = new NpmNamespaceManagerFromList(namespaceInfos); + } else { + this.npmNamespaceManager = npmNamespaceManager; + } + + npmPackages.forEach(this::setup); + } + + private void setup(NpmPackage npmPackage) { + try { + trySetup(npmPackage); + } catch (Exception e) { + throw new InternalErrorException("Failed to setup NpmPackage: " + npmPackage.name(), e); + } + } + + private void trySetup(NpmPackage npmPackage) throws IOException { + final FhirContext fhirContext = getFhirContext(npmPackage); + + final Optional optPackageFolder = npmPackage.getFolders().entrySet().stream() + .filter(entry -> "package".equals(entry.getKey())) + .map(Map.Entry::getValue) + .findFirst(); + + if (optPackageFolder.isPresent()) { + setupNpmPackageInfo(npmPackage, optPackageFolder.get(), fhirContext); + } + } + + private void setupNpmPackageInfo( + NpmPackage npmPackage, NpmPackage.NpmPackageFolder packageFolder, FhirContext fhirContext) + throws IOException { + + final List resources = findResources(packageFolder, fhirContext); + + final Optional optMeasure = findMeasure(resources); + final List libraries = findLibraries(resources); + + storeResources(npmPackage, optMeasure.orElse(null), libraries); + } + + private List findResources(NpmPackage.NpmPackageFolder packageFolder, FhirContext fhirContext) + throws IOException { + + final Map> types = packageFolder.getTypes(); + final List resources = new ArrayList<>(); + + for (Map.Entry> typeToFiles : types.entrySet()) { + for (String nextFile : typeToFiles.getValue()) { + final String fileContents = new String(packageFolder.fetchFile(nextFile), StandardCharsets.UTF_8); + + if (nextFile.toLowerCase().endsWith(".json")) { + final IResourceAdapter resourceAdapter = IAdapterFactory.createAdapterForResource( + fhirContext.newJsonParser().parseResource(fileContents)); + + resources.add(resourceAdapter); + } + } + } + + return resources; + } + + private Optional findMeasure(List resources) { + return resources.stream() + .filter(IMeasureAdapter.class::isInstance) + .map(IMeasureAdapter.class::cast) + .findFirst(); + } + + private List findLibraries(List resources) { + return resources.stream() + .filter(ILibraryAdapter.class::isInstance) + .map(ILibraryAdapter.class::cast) + .toList(); + } + + private void storeResources( + NpmPackage npmPackage, @Nullable IMeasureAdapter measure, List libraries) { + if (measure != null) { + measureUrlToResourceInfo.put( + UrlAndVersion.fromCanonicalAndVersion(measure.getUrl(), measure.getVersion()), + new NpmResourceHolder(measure, findMatchingLibrary(measure, libraries), List.of(npmPackage))); + } + + for (ILibraryAdapter library : libraries) { + libraryUrlToPackage.put( + UrlAndVersion.fromCanonicalAndVersion(library.getUrl(), library.getVersion()), npmPackage); + } + } + + private ILibraryAdapter findMatchingLibrary(IMeasureAdapter measure, List libraries) { + return libraries.stream() + .filter(library -> measure.getLibrary().stream() + .anyMatch(measureLibraryUrl -> doMeasureUrlAndLibraryMatch(measureLibraryUrl, library))) + .findFirst() + .orElse(null); + } + + private static boolean doUrlAndVersionMatch( + IPrimitiveType measureUrl, Map.Entry entry) { + + if (entry.getKey().equals(UrlAndVersion.fromCanonical(measureUrl.getValueAsString()))) { + return true; + } + + return entry.getKey().url.equals(measureUrl.getValueAsString()); + } + + private static boolean doMeasureUrlAndLibraryMatch(String measureLibraryUrl, ILibraryAdapter library) { + final String[] split = PATTERN_PIPE.split(measureLibraryUrl); + + if (split.length == 1) { + return library.getUrl().equals(measureLibraryUrl); + } + + if (split.length == 2) { + return library.getUrl().equals(split[0]) && library.getVersion().equals(split[1]); + } + + throw new InternalErrorException("bad measureUrl: " + measureLibraryUrl); + } + + private FhirContext getFhirContext(NpmPackage npmPackage) { + return FhirContext.forCached(FhirVersionEnum.forVersionString(npmPackage.fhirVersion())); + } + + /** + * Meant to test various scenarios involving missing of faulty NamespaceInfo data. + */ + public static class NpmNamespaceManagerFromList implements NpmNamespaceManager { + + private final List namespaceInfos; + + public NpmNamespaceManagerFromList(List namespaceInfos) { + this.namespaceInfos = List.copyOf(namespaceInfos); + } + + @Override + public List getAllNamespaceInfos() { + return namespaceInfos; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + NpmNamespaceManagerFromList that = (NpmNamespaceManagerFromList) o; + return Objects.equals(namespaceInfos, that.namespaceInfos); + } + + @Override + public int hashCode() { + return Objects.hashCode(namespaceInfos); + } + + @Override + public String toString() { + return new StringJoiner(", ", NpmNamespaceManagerFromList.class.getSimpleName() + "[", "]") + .add("namespaceInfos=" + namespaceInfos) + .toString(); + } + } +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmPackageLoaderWithCache.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmPackageLoaderWithCache.java new file mode 100644 index 0000000000..bbf7b63ea7 --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmPackageLoaderWithCache.java @@ -0,0 +1,110 @@ +package org.opencds.cqf.fhir.utility.npm; + +import java.util.List; +import java.util.Optional; +import org.hl7.cql.model.ModelIdentifier; +import org.hl7.cql.model.NamespaceInfo; +import org.hl7.elm.r1.VersionedIdentifier; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; + +/** + * Support operations against a set of NPM resources that will either operate on the already + * retrieved resources, or delegate to another {@link NpmPackageLoader} if the retrieved data + * does not return results for a given query. + */ +public class NpmPackageLoaderWithCache implements NpmPackageLoader { + + private final List npmResourceHolders; + private final NpmPackageLoader npmPackageLoader; + + public static NpmPackageLoaderWithCache of(NpmResourceHolder npmResourceHolder, NpmPackageLoader npmPackageLoader) { + return new NpmPackageLoaderWithCache(List.of(npmResourceHolder), npmPackageLoader); + } + + public static NpmPackageLoaderWithCache of( + List npmResourceHolders, NpmPackageLoader npmPackageLoader) { + return new NpmPackageLoaderWithCache(npmResourceHolders, npmPackageLoader); + } + + private NpmPackageLoaderWithCache(List npmResourceHolders, NpmPackageLoader npmPackageLoader) { + this.npmResourceHolders = npmResourceHolders; + this.npmPackageLoader = npmPackageLoader; + } + + @Override + public NpmNamespaceManager getNamespaceManager() { + return npmPackageLoader.getNamespaceManager(); + } + + @Override + public NpmResourceHolder loadNpmResources(IPrimitiveType measureUrl) { + return npmResourceHolders.stream() + .filter(npmResourceHolder -> isMeasureUrlMatch(npmResourceHolder, measureUrl)) + .findFirst() + .orElseGet(() -> npmPackageLoader.loadNpmResources(measureUrl)); + } + + @Override + public Optional findMatchingLibrary(VersionedIdentifier versionedIdentifier) { + var optLibrary = npmResourceHolders.stream() + .map(npmResourceHolder -> npmResourceHolder.findMatchingLibrary(versionedIdentifier)) + .flatMap(Optional::stream) + .findFirst(); + + if (optLibrary.isPresent()) { + return optLibrary; + } + + return findLibraryFromUnrelatedNpmPackage(versionedIdentifier); + } + + @Override + public Optional findMatchingLibrary(ModelIdentifier modelIdentifier) { + var optLibrary = npmResourceHolders.stream() + .map(npmResourceHolder -> npmResourceHolder.findMatchingLibrary(modelIdentifier)) + .flatMap(Optional::stream) + .findFirst(); + + if (optLibrary.isPresent()) { + return optLibrary; + } + + return findLibraryFromUnrelatedNpmPackage(modelIdentifier); + } + + @Override + public List getAllNamespaceInfos() { + return npmPackageLoader.getAllNamespaceInfos(); + } + + @Override + public Optional loadLibraryByUrl(String libraryUrl) { + + var optLibrary = npmResourceHolders.stream() + .filter(npmResourceHolder -> isLibraryUrlMatch(npmResourceHolder, libraryUrl)) + .map(NpmResourceHolder::getOptMainLibrary) + .flatMap(Optional::stream) + .findFirst(); + + if (optLibrary.isPresent()) { + return optLibrary; + } + + return npmPackageLoader.loadLibraryByUrl(libraryUrl); + } + + private static boolean isMeasureUrlMatch(NpmResourceHolder npmResourceHolder, IPrimitiveType measureUrl) { + return npmResourceHolder + .getMeasure() + .map(measure -> measure.getUrl().equals(measureUrl.getValue())) + .orElse(false); + } + + private static boolean isLibraryUrlMatch(NpmResourceHolder npmResourceHolder, String libraryUrl) { + return npmResourceHolder + .getMeasure() + .map(library -> library.getUrl().equals(libraryUrl)) + .orElse(false); + } +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmResourceHolder.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmResourceHolder.java new file mode 100644 index 0000000000..351354f982 --- /dev/null +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/npm/NpmResourceHolder.java @@ -0,0 +1,235 @@ +package org.opencds.cqf.fhir.utility.npm; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import com.google.common.annotations.VisibleForTesting; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.StringJoiner; +import org.hl7.cql.model.ModelIdentifier; +import org.hl7.cql.model.NamespaceInfo; +import org.hl7.elm.r1.VersionedIdentifier; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.utilities.npm.NpmPackage; +import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; +import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.opencds.cqf.fhir.utility.adapter.IMeasureAdapter; + +/** + * FHIR version agnostic container for Measures and Libraries, and a List of associated {@link NpmPackage}s. + * Encapsulate the NpmPackages by only exposing the {@link NamespaceInfo}s and derived + *Libraries not directly associated with a given Measure. + */ +public class NpmResourceHolder { + + public static final NpmResourceHolder EMPTY = new NpmResourceHolder(null, null, List.of()); + + private static final String TEXT_CQL = "text/cql"; + + @Nullable + private final IMeasureAdapter measure; + + @Nullable + private final ILibraryAdapter mainLibrary; + + // In theory, it's possible to have more than one associated NpmPackage + private final List npmPackages; + + @Nullable + private final IAdapterFactory adapterFactory; + + public NpmResourceHolder( + @Nullable IMeasureAdapter measure, @Nullable ILibraryAdapter mainLibrary, List npmPackages) { + this.measure = measure; + this.mainLibrary = mainLibrary; + this.npmPackages = npmPackages; + + adapterFactory = Optional.ofNullable(measure) + .map(measureNonNull -> IAdapterFactory.forFhirVersion( + measureNonNull.fhirContext().getVersion().getVersion())) + .orElse(null); + } + + public Optional getMeasure() { + return Optional.ofNullable(measure); + } + + public Optional getOptMainLibrary() { + return Optional.ofNullable(mainLibrary); + } + + @VisibleForTesting + List getNpmPackages() { + return npmPackages; + } + + public Optional findMatchingLibrary(VersionedIdentifier versionedIdentifier) { + + final Optional optMainLibrary = getOptMainLibrary(); + + if (doesLibraryMatch(versionedIdentifier)) { + return optMainLibrary; + } + + return loadNpmLibrary(versionedIdentifier); + } + + public Optional findMatchingLibrary(ModelIdentifier modelIdentifier) { + + final Optional optMainLibrary = getOptMainLibrary(); + + if (doesLibraryMatch(modelIdentifier)) { + return optMainLibrary; + } + + return loadNpmLibrary(modelIdentifier); + } + + @Override + public boolean equals(Object object) { + if (object == null || getClass() != object.getClass()) { + return false; + } + final NpmResourceHolder that = (NpmResourceHolder) object; + return Objects.equals(measure, that.measure) + && Objects.equals(mainLibrary, that.mainLibrary) + && Objects.equals(npmPackages, that.npmPackages); + } + + @Override + public int hashCode() { + return Objects.hash(measure, mainLibrary, npmPackages); + } + + @Override + public String toString() { + return new StringJoiner(", ", NpmResourceHolder.class.getSimpleName() + "[", "]") + .add("measure=" + measure) + .add("mainLibrary=" + mainLibrary) + .add("npmPackages=" + npmPackages) + .toString(); + } + + private Optional loadNpmLibrary(VersionedIdentifier versionedIdentifier) { + return npmPackages.stream() + .map(npmPackage -> loadLibraryInputStreamContext(npmPackage, versionedIdentifier)) + .filter(Optional::isPresent) + .map(Optional::get) + .map(this::convertContextToLibrary) + .flatMap(Optional::stream) + .findFirst(); + } + + public List getNamespaceInfos() { + return npmPackages.stream().map(this::getNamespaceInfo).toList(); + } + + @Nonnull + private NamespaceInfo getNamespaceInfo(NpmPackage npmPackage) { + return new NamespaceInfo(npmPackage.name(), npmPackage.canonical()); + } + + // Note that this code hasn't actually been tested and is not needed at the present time. + // If this should change, the code will need to be tested and possibly modified. + private Optional loadNpmLibrary(ModelIdentifier modelIdentifier) { + return npmPackages.stream() + .map(npmPackage -> loadLibraryInputStreamContext(npmPackage, modelIdentifier)) + .filter(Optional::isPresent) + .map(Optional::get) + .map(this::convertContextToLibrary) + .flatMap(Optional::stream) + .findFirst(); + } + + private boolean doesLibraryMatch(VersionedIdentifier versionedIdentifier) { + return doesLibraryMatch(versionedIdentifier.getId()); + } + + private boolean doesLibraryMatch(ModelIdentifier modelIdentifier) { + return doesLibraryMatch(modelIdentifier.getId()); + } + + private boolean doesLibraryMatch(String id) { + if (mainLibrary == null || adapterFactory == null) { + return false; + } + + if (mainLibrary.getId().getIdPart().equals(id)) { + return mainLibrary.getContent().stream() + .map(adapterFactory::createAttachment) + .anyMatch(attachment -> TEXT_CQL.equals(attachment.getContentType())); + } + + return false; + } + + record LibraryInputStreamContext(FhirVersionEnum fhirVersionEnum, InputStream libraryInputStream) {} + + Optional loadLibraryInputStreamContext( + NpmPackage npmPackage, VersionedIdentifier libraryIdentifier) { + return loadLibraryAsInputStream(npmPackage, libraryIdentifier) + .map(inputStream -> new LibraryInputStreamContext( + FhirVersionEnum.forVersionString(npmPackage.fhirVersion()), inputStream)); + } + + Optional loadLibraryInputStreamContext( + NpmPackage npmPackage, ModelIdentifier modelIdentifier) { + return loadLibraryAsInputStream(npmPackage, modelIdentifier) + .map(inputStream -> new LibraryInputStreamContext( + FhirVersionEnum.forVersionString(npmPackage.fhirVersion()), inputStream)); + } + + private Optional convertContextToLibrary(LibraryInputStreamContext context) { + try { + final IBaseResource resource = FhirContext.forCached(context.fhirVersionEnum) + .newJsonParser() + .parseResource(context.libraryInputStream); + + if (adapterFactory != null) { + return Optional.of(adapterFactory.createLibrary(resource)); + } + + return Optional.empty(); + } catch (Exception exception) { + throw new InternalErrorException("Failed to load library as input stream", exception); + } + } + + private static Optional loadLibraryAsInputStream( + NpmPackage npmPackage, VersionedIdentifier libraryIdentifier) { + + try { + return Optional.ofNullable(npmPackage.loadByCanonicalVersion( + buildUrl(npmPackage, libraryIdentifier), libraryIdentifier.getVersion())); + } catch (IOException exception) { + throw new InternalErrorException("Failed to load NPM package: " + libraryIdentifier.getId(), exception); + } + } + + private static Optional loadLibraryAsInputStream( + NpmPackage npmPackage, ModelIdentifier modelIdentifier) { + + try { + return Optional.ofNullable(npmPackage.loadByCanonicalVersion( + buildUrl(npmPackage, modelIdentifier), modelIdentifier.getVersion())); + } catch (IOException exception) { + throw new InternalErrorException("Failed to load NPM package: " + modelIdentifier.getId(), exception); + } + } + + @Nonnull + private static String buildUrl(NpmPackage npmPackage, VersionedIdentifier libraryIdentifier) { + return "%s/Library/%s".formatted(npmPackage.canonical(), libraryIdentifier.getId()); + } + + @Nonnull + private static String buildUrl(NpmPackage npmPackage, ModelIdentifier modelIdentifier) { + return "%s/Library/%s-ModelInfo".formatted(npmPackage.canonical(), modelIdentifier.getId()); + } +} diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/repository/ig/IgRepository.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/repository/ig/IgRepository.java index 0beb44f755..40a83d75b7 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/repository/ig/IgRepository.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/repository/ig/IgRepository.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableTable; import com.google.common.collect.Multimap; import com.google.common.collect.Table; +import jakarta.annotation.Nonnull; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; @@ -41,6 +42,8 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.opencds.cqf.fhir.utility.Ids; import org.opencds.cqf.fhir.utility.matcher.ResourceMatcher; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoader; +import org.opencds.cqf.fhir.utility.npm.NpmPackageLoaderInMemory; import org.opencds.cqf.fhir.utility.repository.Repositories; import org.opencds.cqf.fhir.utility.repository.ig.EncodingBehavior.PreserveEncoding; import org.opencds.cqf.fhir.utility.repository.ig.IgConventions.CategoryLayout; @@ -124,6 +127,10 @@ public class IgRepository implements IRepository { private final Path root; private final IgConventions conventions; private final ResourceMatcher resourceMatcher; + private final NpmPackageLoader npmPackageLoader; + private final Path npmJsonPath; + private final List npmTgzPaths; + private IRepositoryOperationProvider operationProvider; private final Cache> resourceCache = @@ -237,6 +244,73 @@ public IgRepository( this.conventions = requireNonNull(conventions, "conventions cannot be null"); this.resourceMatcher = Repositories.getResourceMatcher(this.fhirContext); this.operationProvider = operationProvider; + this.npmJsonPath = buildNpmJsonPath().orElse(null); + this.npmTgzPaths = buildTgzPaths(); + this.npmPackageLoader = buildNpmPackageLoader(npmTgzPaths); + } + + public NpmPackageLoader getNpmPackageLoader() { + return npmPackageLoader; + } + + public Path getJson() { + return this.npmJsonPath; + } + + public boolean hasNpm() { + return NpmPackageLoader.DEFAULT != this.npmPackageLoader; + } + + @Nonnull + public List getNpmTgzPaths() { + return npmTgzPaths; + } + + public Path getRootPath() { + return this.root; + } + + private NpmPackageLoader buildNpmPackageLoader(List npmTgzPaths) { + if (npmTgzPaths.isEmpty()) { + return NpmPackageLoader.DEFAULT; + } + return NpmPackageLoaderInMemory.fromNpmPackageAbsolutePath(npmTgzPaths); + } + + private Optional buildNpmJsonPath() { + final Path npmDir = resolveNpmPath(); + + // More often than not, the npm directory will not exist in an IgRepository + if (!Files.exists(npmDir) || !Files.isDirectory(npmDir)) { + return Optional.empty(); + } + + try (Stream npmSubPaths = Files.list(npmDir)) { + return npmSubPaths + .filter(Files::isRegularFile) + .filter(file -> file.getFileName().toString().endsWith(".json")) + .findFirst(); + } catch (IOException exception) { + throw new IllegalStateException("Could not resolve NPM JSON file", exception); + } + } + + private List buildTgzPaths() { + final Path npmDir = resolveNpmPath(); + + // More often than not, the npm directory will not exist in an IgRepository + if (!Files.exists(npmDir) || !Files.isDirectory(npmDir)) { + return List.of(); + } + + try (Stream npmSubPaths = Files.list(npmDir)) { + return npmSubPaths + .filter(Files::isRegularFile) + .filter(file -> file.getFileName().toString().endsWith(".tgz")) + .toList(); + } catch (IOException exception) { + throw new IllegalStateException("Could not resolve NPM namespace tgz files", exception); + } } public void setOperationProvider(IRepositoryOperationProvider operationProvider) { @@ -251,6 +325,11 @@ public void clearCache(Iterable paths) { this.resourceCache.invalidate(paths); } + @Nonnull + private Path resolveNpmPath() { + return root.resolve("input/npm"); + } + private boolean isExternalPath(Path path) { return path.getParent() != null && path.getParent().toString().toLowerCase().endsWith(EXTERNAL_DIRECTORY); @@ -361,7 +440,7 @@ protected Stream directoriesForCategory( Class resourceType, IgRepositoryCompartment igRepositoryCompartment) { var category = ResourceCategory.forType(resourceType.getSimpleName()); var categoryPaths = TYPE_DIRECTORIES.rowMap().get(this.conventions.categoryLayout()).get(category).stream() - .map(path -> this.root.resolve(path)); + .map(this.root::resolve); if (category == ResourceCategory.DATA && this.conventions.compartmentLayout() == CompartmentLayout.DIRECTORY_PER_COMPARTMENT) { var compartmentPath = pathForCompartment(resourceType, this.fhirContext, igRepositoryCompartment); diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/MeasureAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/MeasureAdapterTest.java index c4ccc164b3..79f19bf13a 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/MeasureAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/dstu3/MeasureAdapterTest.java @@ -15,6 +15,7 @@ import java.time.LocalDate; import java.util.Date; import java.util.List; +import java.util.stream.Stream; import org.hl7.fhir.dstu3.model.CodeableConcept; import org.hl7.fhir.dstu3.model.Coding; import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus; @@ -26,6 +27,9 @@ import org.hl7.fhir.dstu3.model.RelatedArtifact; import org.hl7.fhir.dstu3.model.RelatedArtifact.RelatedArtifactType; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.opencds.cqf.fhir.utility.Constants; import org.opencds.cqf.fhir.utility.adapter.TestVisitor; @@ -271,4 +275,21 @@ void adapter_get_all_dependencies_with_non_depends_on_related_artifacts() { assertTrue(dependencies.contains(dep.getReference())); }); } + + private static Stream getLibraryParams() { + return Stream.of( + Arguments.of(List.of(), List.of()), + Arguments.of( + List.of(new Reference("library1"), new Reference("library2")), + List.of("library1", "library2"))); + } + + @ParameterizedTest + @MethodSource("getLibraryParams") + void getLibrary(List measureLibraries, List expectedAdapterLibraries) { + var measure = new Measure().setLibrary(measureLibraries); + var measureAdapter = adapterFactory.createMeasure(measure); + + assertEquals(expectedAdapterLibraries, measureAdapter.getLibrary()); + } } diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/MeasureAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/MeasureAdapterTest.java index eaea70a5e4..ee05483cd9 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/MeasureAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r4/MeasureAdapterTest.java @@ -15,6 +15,7 @@ import java.time.LocalDate; import java.util.Date; import java.util.List; +import java.util.stream.Stream; import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Coding; @@ -28,6 +29,9 @@ import org.hl7.fhir.r4.model.RelatedArtifact; import org.hl7.fhir.r4.model.RelatedArtifact.RelatedArtifactType; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.opencds.cqf.fhir.utility.Constants; import org.opencds.cqf.fhir.utility.adapter.TestVisitor; @@ -277,4 +281,21 @@ void adapter_get_all_dependencies_with_non_depends_on_related_artifacts() { assertTrue(dependencies.indexOf(dep.getReference()) >= 0); }); } + + private static Stream getLibraryParams() { + return Stream.of( + Arguments.of(List.of(), List.of()), + Arguments.of( + List.of(new CanonicalType("library1"), new CanonicalType("library2")), + List.of("library1", "library2"))); + } + + @ParameterizedTest + @MethodSource("getLibraryParams") + void getLibrary(List measureLibraries, List expectedAdapterLibraries) { + var measure = new Measure().setLibrary(measureLibraries); + var measureAdapter = adapterFactory.createMeasure(measure); + + assertEquals(expectedAdapterLibraries, measureAdapter.getLibrary()); + } } diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/MeasureAdapterTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/MeasureAdapterTest.java index 42baf21fbe..def27cc2cf 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/MeasureAdapterTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/adapter/r5/MeasureAdapterTest.java @@ -15,6 +15,7 @@ import java.time.LocalDate; import java.util.Date; import java.util.List; +import java.util.stream.Stream; import org.hl7.fhir.r5.model.CanonicalType; import org.hl7.fhir.r5.model.CodeableConcept; import org.hl7.fhir.r5.model.Coding; @@ -28,6 +29,9 @@ import org.hl7.fhir.r5.model.RelatedArtifact; import org.hl7.fhir.r5.model.RelatedArtifact.RelatedArtifactType; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.opencds.cqf.fhir.utility.Constants; import org.opencds.cqf.fhir.utility.adapter.TestVisitor; @@ -249,4 +253,21 @@ void adapter_get_all_dependencies_with_non_depends_on_related_artifacts() { assertTrue(dependencies.contains(dep.getReference())); }); } + + private static Stream getLibraryParams() { + return Stream.of( + Arguments.of(List.of(), List.of()), + Arguments.of( + List.of(new CanonicalType("library1"), new CanonicalType("library2")), + List.of("library1", "library2"))); + } + + @ParameterizedTest + @MethodSource("getLibraryParams") + void getLibrary(List measureLibraries, List expectedAdapterLibraries) { + var measure = new Measure().setLibrary(measureLibraries); + var measureAdapter = adapterFactory.createMeasure(measure); + + assertEquals(expectedAdapterLibraries, measureAdapter.getLibrary()); + } } diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/BaseNpmResourceInfoForCqlTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/BaseNpmResourceInfoForCqlTest.java new file mode 100644 index 0000000000..2ad2cf8eb8 --- /dev/null +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/BaseNpmResourceInfoForCqlTest.java @@ -0,0 +1,432 @@ +package org.opencds.cqf.fhir.utility.npm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ca.uhn.fhir.context.FhirVersionEnum; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; +import org.hl7.cql.model.NamespaceInfo; +import org.hl7.elm.r1.VersionedIdentifier; +import org.hl7.fhir.instance.model.api.ICompositeType; +import org.hl7.fhir.r4.model.CanonicalType; +import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory; +import org.opencds.cqf.fhir.utility.adapter.IAttachmentAdapter; +import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter; +import org.opencds.cqf.fhir.utility.adapter.IMeasureAdapter; + +public abstract class BaseNpmResourceInfoForCqlTest { + + protected static final String DOT_TGZ = ".tgz"; + + protected static final String SIMPLE_ALPHA_LOWER = "simplealpha"; + protected static final String SIMPLE_ALPHA_MIXED = "SimpleAlpha"; + protected static final String SIMPLE_BRAVO_LOWER = "simplebravo"; + protected static final String SIMPLE_BRAVO_MIXED = "SimpleBravo"; + protected static final String WITH_DERIVED_LIBRARY_LOWER = "withderivedlibrary"; + protected static final String WITH_DERIVED_LIBRARY_MIXED = "WithDerivedLibrary"; + protected static final String DERIVED_LIBRARY_ID = "DerivedLibrary"; + protected static final String DERIVED_LIBRARY = DERIVED_LIBRARY_ID; + + protected static final String NAMESPACE_PREFIX = "opencds."; + + protected static final String WITH_TWO_LAYERS_DERIVED_LIBRARIES = "withtwolayersderivedlibraries"; + protected static final String WITH_TWO_LAYERS_DERIVED_LIBRARIES_UPPER = "WithTwoLayersDerivedLibraries"; + + protected static final String SIMPLE_ALPHA_NAMESPACE = NAMESPACE_PREFIX + SIMPLE_ALPHA_LOWER; + protected static final String SIMPLE_BRAVO_NAMESPACE = NAMESPACE_PREFIX + SIMPLE_BRAVO_LOWER; + protected static final String WITH_DERIVED_NAMESPACE = NAMESPACE_PREFIX + WITH_DERIVED_LIBRARY_LOWER; + protected static final String WITH_TWO_LAYERS_NAMESPACE = NAMESPACE_PREFIX + WITH_TWO_LAYERS_DERIVED_LIBRARIES; + + protected static final String DERIVED_LAYER_1_A = "DerivedLayer1a"; + protected static final String DERIVED_LAYER_1_B = "DerivedLayer1b"; + protected static final String DERIVED_LAYER_2_A = "DerivedLayer2a"; + protected static final String DERIVED_LAYER_2_B = "DerivedLayer2b"; + protected static final String CROSS_PACKAGE_SOURCE = "crosspackagesource"; + protected static final String CROSS_PACKAGE_SOURCE_ID = "CrossPackageSource"; + protected static final String CROSS_PACKAGE_TARGET = "crosspackagetarget"; + protected static final String CROSS_PACKAGE_TARGET_ID = "CrossPackageTarget"; + + protected static final String SIMPLE_ALPHA_TGZ = SIMPLE_ALPHA_LOWER + DOT_TGZ; + protected static final String SIMPLE_BRAVO_TGZ = SIMPLE_BRAVO_LOWER + DOT_TGZ; + protected static final Path WITH_DERIVED_LIBRARY_TGZ = Paths.get(WITH_DERIVED_LIBRARY_LOWER + DOT_TGZ); + protected static final Path WITH_TWO_LAYERS_DERIVED_LIBRARIES_TGZ = + Paths.get(WITH_TWO_LAYERS_DERIVED_LIBRARIES + DOT_TGZ); + protected static final Path CROSS_PACKAGE_SOURCE_TGZ = Paths.get(CROSS_PACKAGE_SOURCE + DOT_TGZ); + protected static final Path CROSS_PACKAGE_TARGET_TGZ = Paths.get(CROSS_PACKAGE_TARGET + DOT_TGZ); + + protected static final String SLASH_MEASURE_SLASH = "/Measure/"; + protected static final String SLASH_LIBRARY_SLASH = "/Library/"; + + private static final String PIPE = "|"; + private static final String VERSION_0_1 = "0.1"; + private static final String VERSION_0_2 = "0.2"; + private static final String VERSION_0_4 = "0.4"; + private static final String VERSION_0_5 = "0.5"; + + protected static final String SIMPLE_ALPHA_NAMESPACE_URL = "http://simplealpha.npm.opencds.org"; + protected static final String SIMPLE_BRAVO_NAMESPACE_URL = "http://simplebravo.npm.opencds.org"; + protected static final String WITH_DERIVED_URL = "http://withderivedlibrary.npm.opencds.org"; + protected static final String WITH_DERIVED_TWO_LAYERS_URL = "http://withtwolayersderivedlibraries.npm.opencds.org"; + protected static final String CROSS_PACKAGE_SOURCE_URL = "http://crosspackagesource.npm.opencds.org"; + protected static final String CROSS_PACKAGE_TARGET_URL = "http://crosspackagetarget.npm.opencds.org"; + + protected static final String MEASURE_URL_ALPHA = + SIMPLE_ALPHA_NAMESPACE_URL + SLASH_MEASURE_SLASH + SIMPLE_ALPHA_MIXED; + protected static final String MEASURE_URL_BRAVO = + SIMPLE_BRAVO_NAMESPACE_URL + SLASH_MEASURE_SLASH + SIMPLE_BRAVO_MIXED; + protected static final String MEASURE_URL_WITH_DERIVED_LIBRARY = + WITH_DERIVED_URL + SLASH_MEASURE_SLASH + WITH_DERIVED_LIBRARY_MIXED; + protected static final String MEASURE_URL_WITH_DERIVED_LIBRARY_WITH_VERSION = + MEASURE_URL_WITH_DERIVED_LIBRARY + PIPE + VERSION_0_4; + protected static final String MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES = + WITH_DERIVED_TWO_LAYERS_URL + SLASH_MEASURE_SLASH + WITH_TWO_LAYERS_DERIVED_LIBRARIES_UPPER; + protected static final String MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_WITH_VERSION = + MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES + PIPE + VERSION_0_5; + + protected static final String MEASURE_URL_CROSS_PACKAGE_SOURCE = + CROSS_PACKAGE_SOURCE_URL + SLASH_MEASURE_SLASH + CROSS_PACKAGE_SOURCE_ID; + protected static final String MEASURE_URL_CROSS_PACKAGE_SOURCE_WITH_VERSION = + MEASURE_URL_CROSS_PACKAGE_SOURCE + PIPE + VERSION_0_2; + + protected static final String LIBRARY_URL_ALPHA_NO_VERSION = + SIMPLE_ALPHA_NAMESPACE_URL + SLASH_LIBRARY_SLASH + SIMPLE_ALPHA_MIXED; + protected static final String LIBRARY_URL_ALPHA_WITH_VERSION = LIBRARY_URL_ALPHA_NO_VERSION + PIPE + VERSION_0_1; + protected static final String LIBRARY_URL_BRAVO_NO_VERSION = + SIMPLE_BRAVO_NAMESPACE_URL + SLASH_LIBRARY_SLASH + SIMPLE_BRAVO_MIXED; + protected static final String LIBRARY_URL_BRAVO_WITH_VERSION = LIBRARY_URL_BRAVO_NO_VERSION + PIPE + VERSION_0_1; + + protected static final String LIBRARY_URL_WITH_DERIVED_LIBRARY_NO_VERSION = + WITH_DERIVED_URL + SLASH_LIBRARY_SLASH + WITH_DERIVED_LIBRARY_MIXED; + protected static final String LIBRARY_URL_WITH_DERIVED_LIBRARY_WITH_VERSION = + WITH_DERIVED_URL + SLASH_LIBRARY_SLASH + WITH_DERIVED_LIBRARY_MIXED + PIPE + VERSION_0_4; + protected static final String LIBRARY_URL_DERIVED_LIBRARY = + WITH_DERIVED_URL + SLASH_LIBRARY_SLASH + DERIVED_LIBRARY; + + protected static final String LIBRARY_URL_CROSS_PACKAGE_SOURCE = + CROSS_PACKAGE_SOURCE_URL + SLASH_LIBRARY_SLASH + CROSS_PACKAGE_SOURCE_ID; + protected static final String LIBRARY_URL_CROSS_PACKAGE_SOURCE_WITH_VERSION = + LIBRARY_URL_CROSS_PACKAGE_SOURCE + PIPE + VERSION_0_2; + protected static final String LIBRARY_URL_CROSS_PACKAGE_TARGET = + CROSS_PACKAGE_TARGET_URL + SLASH_LIBRARY_SLASH + CROSS_PACKAGE_TARGET_ID; + + protected static final String LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_NO_VERSION = + WITH_DERIVED_TWO_LAYERS_URL + SLASH_LIBRARY_SLASH + WITH_TWO_LAYERS_DERIVED_LIBRARIES_UPPER; + protected static final String LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_WITH_VERSION = + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_NO_VERSION + PIPE + VERSION_0_5; + + protected static final String LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1A = + WITH_DERIVED_TWO_LAYERS_URL + SLASH_LIBRARY_SLASH + DERIVED_LAYER_1_A; + protected static final String LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1B = + WITH_DERIVED_TWO_LAYERS_URL + SLASH_LIBRARY_SLASH + DERIVED_LAYER_1_B; + protected static final String LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2A = + WITH_DERIVED_TWO_LAYERS_URL + SLASH_LIBRARY_SLASH + DERIVED_LAYER_2_A; + protected static final String LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2B = + WITH_DERIVED_TWO_LAYERS_URL + SLASH_LIBRARY_SLASH + DERIVED_LAYER_2_B; + + protected abstract FhirVersionEnum getExpectedFhirVersion(); + + protected void simpleAlpha( + Path tgzPath, + String measureUrl, + String expectedLibraryUrlFromMeasure, + String expectedLibraryUrlWithinLibrary, + String expectedCql) { + + simpleCommon( + tgzPath, + List.of(new NamespaceInfo(SIMPLE_ALPHA_NAMESPACE, SIMPLE_ALPHA_NAMESPACE_URL)), + measureUrl, + expectedLibraryUrlFromMeasure, + expectedLibraryUrlWithinLibrary, + expectedCql); + } + + protected void simpleBravo( + Path tgzPath, + String measureUrl, + String expectedLibraryUrlFromMeasure, + String expectedLibraryUrlWithinLibrary, + String expectedCql) { + + simpleCommon( + tgzPath, + List.of(new NamespaceInfo(SIMPLE_BRAVO_NAMESPACE, SIMPLE_BRAVO_NAMESPACE_URL)), + measureUrl, + expectedLibraryUrlFromMeasure, + expectedLibraryUrlWithinLibrary, + expectedCql); + } + + protected void simpleCommon( + Path tgzPath, + List expectedNamespaceInfos, + String measureUrl, + String expectedLibraryUrlFromMeasure, + String expectedLibraryUrlWithinLibrary, + String expectedCql) { + final NpmPackageLoaderInMemory loader = setup(tgzPath); + + final NpmResourceHolder npmResourceHolder = + loader.loadNpmResources(new org.hl7.fhir.r5.model.CanonicalType(measureUrl)); + + sanityCheckNpmResourceHolder(npmResourceHolder); + + assertEquals(expectedNamespaceInfos, npmResourceHolder.getNamespaceInfos()); + + verifyMeasure(measureUrl, expectedLibraryUrlFromMeasure, npmResourceHolder); + verifyLibrary( + expectedLibraryUrlWithinLibrary, + expectedCql, + npmResourceHolder.getOptMainLibrary().orElse(null)); + } + + protected void multiplePackages(String expectedCqlAlpha, String expectedCqlBravo) { + final NpmPackageLoaderInMemory loader = setup( + Stream.of(SIMPLE_ALPHA_TGZ, SIMPLE_BRAVO_TGZ).map(Paths::get).toArray(Path[]::new)); + + final NpmResourceHolder resourceInfoAlpha = + loader.loadNpmResources(new org.hl7.fhir.r5.model.CanonicalType(MEASURE_URL_ALPHA)); + final NpmResourceHolder resourceInfoBravo = + loader.loadNpmResources(new org.hl7.fhir.r5.model.CanonicalType(MEASURE_URL_BRAVO)); + + assertEquals( + List.of(new NamespaceInfo(SIMPLE_ALPHA_NAMESPACE, SIMPLE_ALPHA_NAMESPACE_URL)), + resourceInfoAlpha.getNamespaceInfos()); + assertEquals( + List.of(new NamespaceInfo(SIMPLE_BRAVO_NAMESPACE, SIMPLE_BRAVO_NAMESPACE_URL)), + resourceInfoBravo.getNamespaceInfos()); + + verifyMeasure(MEASURE_URL_ALPHA, LIBRARY_URL_ALPHA_WITH_VERSION, resourceInfoAlpha); + verifyLibrary( + LIBRARY_URL_ALPHA_NO_VERSION, + expectedCqlAlpha, + resourceInfoAlpha.getOptMainLibrary().orElse(null)); + + verifyMeasure(MEASURE_URL_BRAVO, LIBRARY_URL_BRAVO_WITH_VERSION, resourceInfoBravo); + verifyLibrary( + LIBRARY_URL_BRAVO_NO_VERSION, + expectedCqlBravo, + resourceInfoBravo.getOptMainLibrary().orElse(null)); + } + + protected void derivedLibrary(String expectedCql, String expectedCqlDerived) { + + final NpmPackageLoaderInMemory loader = setup(WITH_DERIVED_LIBRARY_TGZ); + + final NpmResourceHolder resourceInfoWithNoVersion = + loader.loadNpmResources(new CanonicalType(MEASURE_URL_WITH_DERIVED_LIBRARY)); + + sanityCheckNpmResourceHolder(resourceInfoWithNoVersion); + + var expectedNamespaceInfos = List.of(new NamespaceInfo(WITH_DERIVED_NAMESPACE, WITH_DERIVED_URL)); + + assertEquals(expectedNamespaceInfos, resourceInfoWithNoVersion.getNamespaceInfos()); + + verifyMeasure( + MEASURE_URL_WITH_DERIVED_LIBRARY, + LIBRARY_URL_WITH_DERIVED_LIBRARY_WITH_VERSION, + resourceInfoWithNoVersion); + + final NpmResourceHolder resourceInfoWithVersion = + loader.loadNpmResources(new CanonicalType(MEASURE_URL_WITH_DERIVED_LIBRARY_WITH_VERSION)); + sanityCheckNpmResourceHolder(resourceInfoWithVersion); + assertEquals(expectedNamespaceInfos, resourceInfoWithVersion.getNamespaceInfos()); + + verifyMeasure( + MEASURE_URL_WITH_DERIVED_LIBRARY, + LIBRARY_URL_WITH_DERIVED_LIBRARY_WITH_VERSION, + resourceInfoWithVersion); + verifyLibrary( + LIBRARY_URL_WITH_DERIVED_LIBRARY_NO_VERSION, + expectedCql, + resourceInfoWithVersion.getOptMainLibrary().orElse(null)); + + final ILibraryAdapter derivedLibraryFromNoVersion = resourceInfoWithVersion + .findMatchingLibrary(new VersionedIdentifier().withId(DERIVED_LIBRARY_ID)) + .orElse(null); + + verifyLibrary(LIBRARY_URL_DERIVED_LIBRARY, expectedCqlDerived, derivedLibraryFromNoVersion); + + final ILibraryAdapter derivedLibraryFromVersion = resourceInfoWithVersion + .findMatchingLibrary( + new VersionedIdentifier().withId(DERIVED_LIBRARY_ID).withVersion("0.4")) + .orElse(null); + + verifyLibrary(LIBRARY_URL_DERIVED_LIBRARY, expectedCqlDerived, derivedLibraryFromVersion); + + final ILibraryAdapter derivedLibraryFromBadVersion = resourceInfoWithVersion + .findMatchingLibrary( + new VersionedIdentifier().withId(DERIVED_LIBRARY_ID).withVersion("bad")) + .orElse(null); + + assertNull(derivedLibraryFromBadVersion); + } + + protected void derivedLibraryTwoLayers( + String expectedCql, + String expectedCqlDerived1a, + String expectedCqlDerived1b, + String expectedCqlDerived2a, + String expectedCqlDerived2b) { + + final NpmPackageLoaderInMemory loader = setup(WITH_TWO_LAYERS_DERIVED_LIBRARIES_TGZ); + + var expectedNamespaceInfos = List.of(new NamespaceInfo(WITH_TWO_LAYERS_NAMESPACE, WITH_DERIVED_TWO_LAYERS_URL)); + + final NpmResourceHolder resourceInfoNoVersion = + loader.loadNpmResources(new CanonicalType(MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES)); + sanityCheckNpmResourceHolder(resourceInfoNoVersion); + assertEquals(expectedNamespaceInfos, resourceInfoNoVersion.getNamespaceInfos()); + verifyMeasure( + MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES, + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_WITH_VERSION, + resourceInfoNoVersion); + verifyLibrary( + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_NO_VERSION, + expectedCql, + resourceInfoNoVersion.getOptMainLibrary().orElse(null)); + + final NpmResourceHolder resourceInfoWithVersion = + loader.loadNpmResources(new CanonicalType(MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_WITH_VERSION)); + sanityCheckNpmResourceHolder(resourceInfoWithVersion); + assertEquals(expectedNamespaceInfos, resourceInfoWithVersion.getNamespaceInfos()); + verifyMeasure( + MEASURE_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES, + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_WITH_VERSION, + resourceInfoWithVersion); + verifyLibrary( + LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARIES_NO_VERSION, + expectedCql, + resourceInfoWithVersion.getOptMainLibrary().orElse(null)); + + final ILibraryAdapter derivedLibrary1a = resourceInfoWithVersion + .findMatchingLibrary(new VersionedIdentifier() + .withId(DERIVED_LAYER_1_A) + .withVersion(VERSION_0_5) + .withSystem(WITH_DERIVED_TWO_LAYERS_URL)) + .orElse(null); + + verifyLibrary(LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1A, expectedCqlDerived1a, derivedLibrary1a); + + final ILibraryAdapter derivedLibrary1b = resourceInfoWithVersion + .findMatchingLibrary( + new VersionedIdentifier().withId(DERIVED_LAYER_1_B).withVersion(VERSION_0_5)) + .orElse(null); + + verifyLibrary(LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_1B, expectedCqlDerived1b, derivedLibrary1b); + + final ILibraryAdapter derivedLibrary2a = resourceInfoWithVersion + .findMatchingLibrary( + new VersionedIdentifier().withId(DERIVED_LAYER_2_A).withVersion(VERSION_0_5)) + .orElse(null); + + verifyLibrary(LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2A, expectedCqlDerived2a, derivedLibrary2a); + + final ILibraryAdapter derivedLibrary2b = resourceInfoWithVersion + .findMatchingLibrary( + new VersionedIdentifier().withId(DERIVED_LAYER_2_B).withVersion(VERSION_0_5)) + .orElse(null); + + verifyLibrary(LIBRARY_URL_WITH_TWO_LAYERS_DERIVED_LIBRARY_2B, expectedCqlDerived2b, derivedLibrary2b); + } + + protected void crossPackage(String expectedCqlSource, String expectedCqlTarget) { + + final NpmPackageLoaderInMemory loader = setup(CROSS_PACKAGE_SOURCE_TGZ, CROSS_PACKAGE_TARGET_TGZ); + + final NpmResourceHolder resourceInfoWithNoVersion = + loader.loadNpmResources(new CanonicalType(MEASURE_URL_CROSS_PACKAGE_SOURCE)); + sanityCheckNpmResourceHolder(resourceInfoWithNoVersion); + verifyMeasure( + MEASURE_URL_CROSS_PACKAGE_SOURCE, + LIBRARY_URL_CROSS_PACKAGE_SOURCE_WITH_VERSION, + resourceInfoWithNoVersion); + final NpmResourceHolder resourceInfoWithVersion = + loader.loadNpmResources(new CanonicalType(MEASURE_URL_CROSS_PACKAGE_SOURCE_WITH_VERSION)); + sanityCheckNpmResourceHolder(resourceInfoWithVersion); + verifyMeasure( + MEASURE_URL_CROSS_PACKAGE_SOURCE, + LIBRARY_URL_CROSS_PACKAGE_SOURCE_WITH_VERSION, + resourceInfoWithVersion); + + verifyLibrary( + LIBRARY_URL_CROSS_PACKAGE_SOURCE, + expectedCqlSource, + resourceInfoWithVersion.getOptMainLibrary().orElse(null)); + + final Optional matchingLibraryWithSourceResult = + resourceInfoWithVersion.findMatchingLibrary(new VersionedIdentifier().withId(CROSS_PACKAGE_TARGET_ID)); + + // We expect NOT to find the target Library here since it's not in the source package at all + assertTrue(matchingLibraryWithSourceResult.isEmpty()); + + // On the other hand, we can load the Library directly from the loader by URL: + final Optional optLibraryTarget = loader.loadLibraryByUrl(LIBRARY_URL_CROSS_PACKAGE_TARGET); + + assertTrue(optLibraryTarget.isPresent()); + verifyLibrary(LIBRARY_URL_CROSS_PACKAGE_TARGET, expectedCqlTarget, optLibraryTarget.get()); + } + + private void verifyLibrary(String expectedLibraryUrl, String expectedCql, @Nullable ILibraryAdapter library) { + assertNotNull(library); + + assertEquals( + getExpectedFhirVersion(), library.fhirContext().getVersion().getVersion()); + + assertEquals(expectedLibraryUrl, library.getUrl()); + + final List attachments = library.getContent(); + + assertEquals(1, attachments.size()); + + final ICompositeType attachment = attachments.get(0); + + final IAdapterFactory adapterFactory = IAdapterFactory.forFhirVersion( + library.fhirContext().getVersion().getVersion()); + + final IAttachmentAdapter adaptedAttachment = adapterFactory.createAttachment(attachment); + + assertEquals("text/cql", adaptedAttachment.getContentType()); + final byte[] attachmentData = adaptedAttachment.getData(); + final String cql = new String(attachmentData, StandardCharsets.UTF_8); + + assertEquals(expectedCql, cql); + } + + private void verifyMeasure(String measureUrl, String expectedLibraryUrl, NpmResourceHolder npmResourceHolder) { + + final Optional optMeasure = npmResourceHolder.getMeasure(); + assertTrue(optMeasure.isPresent(), "Could not find measure with url: %s".formatted(measureUrl)); + + final IMeasureAdapter measure = optMeasure.get(); + assertEquals( + getExpectedFhirVersion(), measure.fhirContext().getVersion().getVersion()); + assertEquals(measureUrl, measure.getUrl()); + + final List libraryUrls = measure.getLibrary(); + assertEquals(1, libraryUrls.size()); + final String libraryUrl = libraryUrls.get(0); + assertEquals(expectedLibraryUrl, libraryUrl); + } + + @Nonnull + private NpmPackageLoaderInMemory setup(Path... tgzPaths) { + return NpmPackageLoaderInMemory.fromNpmPackageClasspath(getClass(), tgzPaths); + } + + private static void sanityCheckNpmResourceHolder(NpmResourceHolder npmResourceHolder) { + assertNotNull(npmResourceHolder); + assertFalse(npmResourceHolder.getNpmPackages().isEmpty()); + assertFalse(npmResourceHolder.getNamespaceInfos().isEmpty()); + } +} diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/MeasureOrNpmResourceHolderListTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/MeasureOrNpmResourceHolderListTest.java new file mode 100644 index 0000000000..ae3914ea67 --- /dev/null +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/MeasureOrNpmResourceHolderListTest.java @@ -0,0 +1,154 @@ +package org.opencds.cqf.fhir.utility.npm; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import java.util.List; +import org.hl7.fhir.r4.model.Measure; +import org.junit.jupiter.api.Test; + +class MeasureOrNpmResourceHolderListTest { + + @Test + void shouldCreateFromSingleMeasureOrNpmResourceHolder() { + var measure = new Measure().setUrl("http://example.org/Measure/test"); + var holder = MeasureOrNpmResourceHolder.measureOnly(measure); + + var holderList = MeasureOrNpmResourceHolderList.of(holder); + + assertEquals(1, holderList.size()); + assertEquals(List.of(holder), holderList.getMeasuresOrNpmResourceHolders()); + } + + @Test + void shouldCreateFromListOfMeasureOrNpmResourceHolders() { + var measure1 = (Measure) new Measure().setUrl("http://example.org/Measure/test1"); + var measure2 = (Measure) new Measure().setUrl("http://example.org/Measure/test2"); + + MeasureOrNpmResourceHolder holder1 = MeasureOrNpmResourceHolder.measureOnly(measure1); + MeasureOrNpmResourceHolder holder2 = MeasureOrNpmResourceHolder.measureOnly(measure2); + + MeasureOrNpmResourceHolderList list = MeasureOrNpmResourceHolderList.of(List.of(holder1, holder2)); + + assertEquals(2, list.size()); + assertEquals(List.of(holder1, holder2), list.getMeasuresOrNpmResourceHolders()); + } + + @Test + void shouldCreateFromSingleMeasure() { + var measure = new Measure().setUrl("http://example.org/Measure/test"); + + var holderList = MeasureOrNpmResourceHolderList.of(measure); + + // Assert + assertEquals(1, holderList.size()); + assertEquals(measure, holderList.getMeasures().get(0)); + } + + @Test + void shouldCreateFromListOfMeasures() { + var measure1 = new Measure().setUrl("http://example.org/Measure/test1"); + var measure2 = new Measure().setUrl("http://example.org/Measure/test2"); + + var holderList = MeasureOrNpmResourceHolderList.ofMeasures(List.of(measure1, measure2)); + + assertEquals(2, holderList.size()); + assertEquals(List.of(measure1, measure2), holderList.getMeasures()); + } + + @Test + void shouldReturnCorrectNpmResourceHolders() { + var measure1 = new Measure(); + var measure2 = new Measure(); + + var holder1 = MeasureOrNpmResourceHolder.measureOnly(measure1); + var holder2 = MeasureOrNpmResourceHolder.measureOnly(measure2); + + var holderList = MeasureOrNpmResourceHolderList.of(List.of(holder1, holder2)); + + var npmResourceHolders = holderList.npmResourceHolders(); + + assertEquals(2, npmResourceHolders.size()); + assertEquals(List.of(NpmResourceHolder.EMPTY, NpmResourceHolder.EMPTY), npmResourceHolders); + } + + @Test + void shouldReturnCorrectMeasureUrls() { + var measure1 = new Measure().setUrl("http://example.org/Measure/test1"); + var measure2 = new Measure().setUrl("http://example.org/Measure/test2"); + + var holder1 = MeasureOrNpmResourceHolder.measureOnly(measure1); + var holder2 = MeasureOrNpmResourceHolder.measureOnly(measure2); + + var holderList = MeasureOrNpmResourceHolderList.of(List.of(holder1, holder2)); + + var measureUrls = holderList.getMeasureUrls(); + + assertEquals(List.of("http://example.org/Measure/test1", "http://example.org/Measure/test2"), measureUrls); + } + + @Test + void shouldCheckMeasureLibrariesSuccessfully() { + var measure = new Measure() + .setUrl("http://example.org/Measure/test") + .addLibrary("http://example.org/Library/testLib"); + + var holder = MeasureOrNpmResourceHolder.measureOnly(measure); + var holderList = MeasureOrNpmResourceHolderList.of(holder); + + assertDoesNotThrow(holderList::checkMeasureLibraries); + } + + @Test + void shouldThrowExceptionWhenMeasureHasNoLibrary() { + // Arrange + var measure = new Measure().setUrl("http://example.org/Measure/test"); + + var holder = MeasureOrNpmResourceHolder.measureOnly(measure); + var holderList = MeasureOrNpmResourceHolderList.of(holder); + + var exception = assertThrows(InvalidRequestException.class, holderList::checkMeasureLibraries); + assertEquals( + "Measure http://example.org/Measure/test does not have a primary library specified", + exception.getMessage()); + } + + @Test + void shouldImplementEqualsAndHashCodeCorrectly() { + var measure1 = new Measure().setUrl("http://example.org/Measure/test1"); + var measure2 = new Measure().setUrl("http://example.org/Measure/test2"); + + var holder1 = MeasureOrNpmResourceHolder.measureOnly(measure1); + var holder2 = MeasureOrNpmResourceHolder.measureOnly(measure2); + + // Act + var holderList1 = MeasureOrNpmResourceHolderList.of(List.of(holder1, holder2)); + var holderList2 = MeasureOrNpmResourceHolderList.of(List.of(holder1, holder2)); + var holderList3 = MeasureOrNpmResourceHolderList.of(List.of(holder1)); + + // Assert + assertEquals(holderList1, holderList2); + assertEquals(holderList1.hashCode(), holderList2.hashCode()); + assertNotEquals(holderList1, holderList3); + assertNotEquals(holderList1.hashCode(), holderList3.hashCode()); + } + + @Test + void shouldHaveCorrectToStringRepresentation() { + // Arrange + var measure = new Measure().setUrl("http://example.org/Measure/test"); + var holder = MeasureOrNpmResourceHolder.measureOnly(measure); + var holderList = MeasureOrNpmResourceHolderList.of(holder); + + // Act + var toStringResult = holderList.toString(); + + // Assert + assertTrue(toStringResult.contains("MeasurePlusNpmResourceHolderList")); + assertTrue(toStringResult.contains("measuresPlusNpmResourceHolders")); + } +} diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/MeasureOrNpmResourceHolderTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/MeasureOrNpmResourceHolderTest.java new file mode 100644 index 0000000000..bb5709c6e9 --- /dev/null +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/MeasureOrNpmResourceHolderTest.java @@ -0,0 +1,94 @@ +package org.opencds.cqf.fhir.utility.npm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Optional; +import org.hl7.fhir.r4.model.Measure; +import org.junit.jupiter.api.Test; + +class MeasureOrNpmResourceHolderTest { + + @Test + void shouldCreateMeasureOnlyInstance() { + var measure = new Measure(); + var holder = MeasureOrNpmResourceHolder.measureOnly(measure); + + assertEquals(measure, holder.getMeasure()); + assertEquals(NpmResourceHolder.EMPTY, holder.npmResourceHolder()); + } + + @Test + void hasLibraryShouldReturnTrueWhenMeasureHasLibrary() { + var measure = new Measure(); + measure.addLibrary("http://example.org/Library/123"); + + var holder = MeasureOrNpmResourceHolder.measureOnly(measure); + + assertTrue(holder.hasLibrary()); + } + + @Test + void hasLibraryShouldReturnFalseWhenMeasureHasNoLibrary() { + var measure = new Measure(); + + var holder = MeasureOrNpmResourceHolder.measureOnly(measure); + + assertFalse(holder.hasLibrary()); + } + + @Test + void getMainLibraryUrlShouldReturnUrlFromMeasure() { + var measure = new Measure().addLibrary("http://example.org/Library/123"); + + var holder = MeasureOrNpmResourceHolder.measureOnly(measure); + + assertEquals(Optional.of("http://example.org/Library/123"), holder.getMainLibraryUrl()); + } + + @Test + void getMainLibraryUrlShouldReturnEmptyWhenMeasureHasNoLibrary() { + var measure = new Measure(); + + var holder = MeasureOrNpmResourceHolder.measureOnly(measure); + + assertEquals(Optional.empty(), holder.getMainLibraryUrl()); + } + + @Test + void getMeasureIdElementShouldReturnIdFromMeasure() { + var measure = (Measure) new Measure().setId("measure-123"); + + var holder = MeasureOrNpmResourceHolder.measureOnly(measure); + + assertEquals("measure-123", holder.getMeasureIdElement().getIdPart()); + } + + @Test + void hasMeasureUrlShouldReturnTrueWhenMeasureHasUrl() { + var measure = new Measure().setUrl("http://example.org/Measure/123"); + + var holder = MeasureOrNpmResourceHolder.measureOnly(measure); + + assertTrue(holder.hasMeasureUrl()); + } + + @Test + void hasMeasureUrlShouldReturnFalseWhenMeasureHasNoUrl() { + var measure = new Measure(); + + var holder = MeasureOrNpmResourceHolder.measureOnly(measure); + + assertFalse(holder.hasMeasureUrl()); + } + + @Test + void getMeasureUrlShouldReturnUrlFromMeasure() { + var measure = new Measure().setUrl("http://example.org/Measure/123"); + + var holder = MeasureOrNpmResourceHolder.measureOnly(measure); + + assertEquals("http://example.org/Measure/123", holder.getMeasureUrl()); + } +} diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/NpmConfigDependencySubstitutorTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/NpmConfigDependencySubstitutorTest.java new file mode 100644 index 0000000000..1bac236b3e --- /dev/null +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/NpmConfigDependencySubstitutorTest.java @@ -0,0 +1,29 @@ +package org.opencds.cqf.fhir.utility.npm; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; + +import java.util.Optional; +import org.junit.jupiter.api.Test; + +class NpmConfigDependencySubstitutorTest { + + @Test + void present() { + var npmPackageLoader = mock(NpmPackageLoader.class); + + var npmPackageLoaderFromSubstitutor = + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(Optional.of(npmPackageLoader)); + + assertNotEquals(NpmPackageLoader.DEFAULT, npmPackageLoaderFromSubstitutor); + assertEquals(npmPackageLoader, npmPackageLoaderFromSubstitutor); + } + + @Test + void empty() { + var npmPackageLoaderFromSubstitutor = + NpmConfigDependencySubstitutor.substituteNpmPackageLoaderIfEmpty(Optional.empty()); + + assertEquals(NpmPackageLoader.DEFAULT, npmPackageLoaderFromSubstitutor); + } +} diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/r4/NpmResourceHolderR4Test.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/r4/NpmResourceHolderR4Test.java new file mode 100644 index 0000000000..f57cc84d72 --- /dev/null +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/r4/NpmResourceHolderR4Test.java @@ -0,0 +1,273 @@ +package org.opencds.cqf.fhir.utility.npm.r4; + +import ca.uhn.fhir.context.FhirVersionEnum; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.utility.npm.BaseNpmResourceInfoForCqlTest; + +@SuppressWarnings("squid:S2699") +class NpmResourceHolderR4Test extends BaseNpmResourceInfoForCqlTest { + + protected FhirVersionEnum fhirVersion = FhirVersionEnum.R4; + + private static final String EXPECTED_CQL_ALPHA = + """ + library opencds.simplealpha.SimpleAlpha + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + parameter "Measurement Period" Interval + default Interval[@2021-01-01T00:00:00.0-06:00, @2022-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists ("Encounter Finished") + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + """; + + private static final String EXPECTED_CQL_BRAVO = + """ + library opencds.simplealpha.SimpleBravo + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + parameter "Measurement Period" Interval + default Interval[@2024-01-01T00:00:00.0-06:00, @2025-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists ("Encounter Planned") + + define "Encounter Planned": + [Encounter] E + where E.status = 'planned' + """; + + private static final String EXPECTED_CQL_WITH_DERIVED = + """ + library opencds.withderivedlibrary WithDerivedLibrary version '0.4' + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + include DerivedLibrary version '0.4' + + parameter "Measurement Period" Interval + default Interval[@2021-01-01T00:00:00.0-06:00, @2022-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists (DerivedLibrary."Encounter Finished") + """; + + private static final String EXPECTED_CQL_DERIVED = + """ + library opencds.withderivedlibrary.DerivedLibrary version '0.4' + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + context Patient + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + """; + + private static final String EXPECTED_CQL_DERIVED_TWO_LAYERS = + """ + library opencds.withtwolayersderivedlibraries.WithTwoLayersDerivedLibraries version '0.5' + + using FHIR version '4.0.1' + + include DerivedLayer1a version '0.5' + include DerivedLayer1b version '0.5' + + parameter "Measurement Period" Interval + default Interval[@2022-01-01T00:00:00.0-06:00, @2023-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + DerivedLayer1a."Initial Population" + + define "Denominator": + DerivedLayer1b."Denominator" + + define "Numerator": + DerivedLayer1b."Numerator" + """; + + private static final String EXPECTED_CQL_DERIVED_1_A = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer1a version '0.5' + + using FHIR version '4.0.1' + + include DerivedLayer2a version '0.5' + include DerivedLayer2b version '0.5' + + context Patient + + define "Initial Population": + DerivedLayer2a."Initial Population" + """; + + private static final String EXPECTED_CQL_DERIVED_1_B = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer1b version '0.5' + + using FHIR version '4.0.1' + + include DerivedLayer2a version '0.5' + include DerivedLayer2b version '0.5' + + context Patient + + define "Denominator": + DerivedLayer2a."Denominator" + + define "Numerator": + DerivedLayer2b."Numerator" + """; + + private static final String EXPECTED_CQL_DERIVED_2_A = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer2a version '0.5' + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + context Patient + + define "Initial Population": + exists ("Encounter Finished") + + define "Denominator": + exists ("Encounter Planned") + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + + define "Encounter Planned": + [Encounter] E + where E.status = 'planned' + """; + + private static final String EXPECTED_CQL_DERIVED_2_B = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer2b version '0.5' + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + parameter "Measurement Period" Interval + default Interval[@2021-01-01T00:00:00.0-06:00, @2022-01-01T00:00:00.0-06:00) + + context Patient + + define "Numerator": + exists ("Encounter Triaged") + + define "Encounter Triaged": + [Encounter] E + where E.status = 'triaged' + """; + + private static final String EXPECTED_CQL_CROSS_SOURCE = + """ + library opencds.crosspackagesource.CrossPackageSource version '0.2' + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + include opencds.crosspackagetarget.CrossPackageTarget version '0.3' called CrossPackageTarget + + parameter "Measurement Period" Interval + default Interval[@2020-01-01T00:00:00.0-06:00, @2021-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists (CrossPackageTarget."Encounter Finished") + """; + + private static final String EXPECTED_CQL_CROSS_TARGET = + """ + library opencds.crosspackagetarget.CrossPackageTarget version '0.3' + + using FHIR version '4.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + context Patient + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + """; + + @Override + protected FhirVersionEnum getExpectedFhirVersion() { + return FhirVersionEnum.R4; + } + + @Test + void simpleAlpha() { + simpleAlpha( + Path.of(SIMPLE_ALPHA_TGZ), + MEASURE_URL_ALPHA, + LIBRARY_URL_ALPHA_WITH_VERSION, + LIBRARY_URL_ALPHA_NO_VERSION, + EXPECTED_CQL_ALPHA); + } + + @Test + void simpleBravo() { + simpleBravo( + Path.of(SIMPLE_BRAVO_TGZ), + MEASURE_URL_BRAVO, + LIBRARY_URL_BRAVO_WITH_VERSION, + LIBRARY_URL_BRAVO_NO_VERSION, + EXPECTED_CQL_BRAVO); + } + + @Test + void multiplePackages() { + multiplePackages(EXPECTED_CQL_ALPHA, EXPECTED_CQL_BRAVO); + } + + @Test + void derivedLibrary() { + derivedLibrary(EXPECTED_CQL_WITH_DERIVED, EXPECTED_CQL_DERIVED); + } + + @Test + void derivedLibraryTwoLayers() { + derivedLibraryTwoLayers( + EXPECTED_CQL_DERIVED_TWO_LAYERS, + EXPECTED_CQL_DERIVED_1_A, + EXPECTED_CQL_DERIVED_1_B, + EXPECTED_CQL_DERIVED_2_A, + EXPECTED_CQL_DERIVED_2_B); + } + + @Test + void crossPackage() { + crossPackage(EXPECTED_CQL_CROSS_SOURCE, EXPECTED_CQL_CROSS_TARGET); + } +} diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/r5/NpmResourceHolderR5Test.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/r5/NpmResourceHolderR5Test.java new file mode 100644 index 0000000000..5c8cb61ed1 --- /dev/null +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/npm/r5/NpmResourceHolderR5Test.java @@ -0,0 +1,270 @@ +package org.opencds.cqf.fhir.utility.npm.r5; + +import ca.uhn.fhir.context.FhirVersionEnum; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.opencds.cqf.fhir.utility.npm.BaseNpmResourceInfoForCqlTest; + +@SuppressWarnings("squid:S2699") +class NpmResourceHolderR5Test extends BaseNpmResourceInfoForCqlTest { + + protected FhirVersionEnum fhirVersion = FhirVersionEnum.R5; + + private static final String EXPECTED_CQL_ALPHA = + """ + library opencds.simplealpha.SimpleAlpha + + using FHIR version '5.0.1' + + include FHIRHelpers version '5.0.1' called FHIRHelpers + + parameter "Measurement Period" Interval + default Interval[@2021-01-01T00:00:00.0-06:00, @2022-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists ("Encounter Finished") + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + """; + private static final String EXPECTED_CQL_BRAVO = + """ + library opencds.simplealpha.SimpleBravo + + using FHIR version '5.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + + parameter "Measurement Period" Interval + default Interval[@2024-01-01T00:00:00.0-06:00, @2025-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists ("Encounter Planned") + + define "Encounter Planned": + [Encounter] E + where E.status = 'planned' + """; + private static final String EXPECTED_CQL_WITH_DERIVED = + """ + library opencds.withderivedlibrary WithDerivedLibrary version '0.4' + + using FHIR version '5.0.1' + + include FHIRHelpers version '5.0.1' called FHIRHelpers + include DerivedLibrary version '0.4' + + parameter "Measurement Period" Interval + default Interval[@2021-01-01T00:00:00.0-06:00, @2022-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists (DerivedLibrary."Encounter Finished") + """; + private static final String EXPECTED_CQL_DERIVED = + """ + library opencds.withderivedlibrary.DerivedLibrary version '0.4' + + using FHIR version '5.0.1' + + include FHIRHelpers version '5.0.1' called FHIRHelpers + + context Patient + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + """; + + private static final String EXPECTED_CQL_DERIVED_TWO_LAYERS = + """ + library opencds.withtwolayersderivedlibraries.WithTwoLayersDerivedLibraries version '0.5' + + using FHIR version '5.0.1' + + include DerivedLayer1a version '0.5' + include DerivedLayer1b version '0.5' + + parameter "Measurement Period" Interval + default Interval[@2022-01-01T00:00:00.0-06:00, @2023-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + DerivedLayer1a."Initial Population" + + define "Denominator": + DerivedLayer1b."Denominator" + + define "Numerator": + DerivedLayer1b."Numerator" + """; + + private static final String EXPECTED_CQL_DERIVED_1_A = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer1a version '0.5' + + using FHIR version '5.0.1' + + include DerivedLayer2a version '0.5' + include DerivedLayer2b version '0.5' + + context Patient + + define "Initial Population": + DerivedLayer2a."Initial Population" + """; + + private static final String EXPECTED_CQL_DERIVED_1_B = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer1b version '0.5' + + using FHIR version '5.0.1' + + include DerivedLayer2a version '0.5' + include DerivedLayer2b version '0.5' + + context Patient + + define "Denominator": + DerivedLayer2a."Denominator" + + define "Numerator": + DerivedLayer2b."Numerator" + """; + + private static final String EXPECTED_CQL_DERIVED_2_A = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer2a version '0.5' + + using FHIR version '5.0.1' + + include FHIRHelpers version '5.0.1' called FHIRHelpers + + context Patient + + define "Initial Population": + exists ("Encounter Finished") + + define "Denominator": + exists ("Encounter Planned") + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + + define "Encounter Planned": + [Encounter] E + where E.status = 'planned' + """; + + private static final String EXPECTED_CQL_DERIVED_2_B = + """ + library opencds.withtwolayersderivedlibraries.DerivedLayer2b version '0.5' + + using FHIR version '5.0.1' + + include FHIRHelpers version '5.0.1' called FHIRHelpers + + parameter "Measurement Period" Interval + default Interval[@2021-01-01T00:00:00.0-06:00, @2022-01-01T00:00:00.0-06:00) + + context Patient + + define "Numerator": + exists ("Encounter Triaged") + + define "Encounter Triaged": + [Encounter] E + where E.status = 'triaged' + """; + + private static final String EXPECTED_CQL_CROSS_SOURCE = + """ + library opencds.crosspackagesource.CrossPackageSource version '0.2' + + using FHIR version '5.0.1' + + include FHIRHelpers version '4.0.1' called FHIRHelpers + include opencds.crosspackagetarget.CrossPackageTarget version '0.3' called CrossPackageTarget + + parameter "Measurement Period" Interval + default Interval[@2020-01-01T00:00:00.0-06:00, @2021-01-01T00:00:00.0-06:00) + + context Patient + + define "Initial Population": + exists (CrossPackageTarget."Encounter Finished") + """; + + private static final String EXPECTED_CQL_CROSS_TARGET = + """ + library opencds.crosspackagetarget.CrossPackageTarget version '0.3' + + using FHIR version '5.0.1' + + include FHIRHelpers version '5.0.1' called FHIRHelpers + + context Patient + + define "Encounter Finished": + [Encounter] E + where E.status = 'finished' + """; + + @Override + protected FhirVersionEnum getExpectedFhirVersion() { + return FhirVersionEnum.R5; + } + + @Test + void simpleAlpha() { + simpleAlpha( + Path.of(SIMPLE_ALPHA_TGZ), + MEASURE_URL_ALPHA, + LIBRARY_URL_ALPHA_WITH_VERSION, + LIBRARY_URL_ALPHA_NO_VERSION, + EXPECTED_CQL_ALPHA); + } + + @Test + void simpleBravo() { + simpleBravo( + Path.of(SIMPLE_BRAVO_TGZ), + MEASURE_URL_BRAVO, + LIBRARY_URL_BRAVO_WITH_VERSION, + LIBRARY_URL_BRAVO_NO_VERSION, + EXPECTED_CQL_BRAVO); + } + + @Test + void multiplePackages() { + multiplePackages(EXPECTED_CQL_ALPHA, EXPECTED_CQL_BRAVO); + } + + @Test + void derivedLibrary() { + derivedLibrary(EXPECTED_CQL_WITH_DERIVED, EXPECTED_CQL_DERIVED); + } + + @Test + void derivedLibraryTwoLayers() { + derivedLibraryTwoLayers( + EXPECTED_CQL_DERIVED_TWO_LAYERS, + EXPECTED_CQL_DERIVED_1_A, + EXPECTED_CQL_DERIVED_1_B, + EXPECTED_CQL_DERIVED_2_A, + EXPECTED_CQL_DERIVED_2_B); + } + + @Test + void crossPackage() { + crossPackage(EXPECTED_CQL_CROSS_SOURCE, EXPECTED_CQL_CROSS_TARGET); + } +} diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/crosspackagesource.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/crosspackagesource.tgz new file mode 100644 index 0000000000..79954af46a Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/crosspackagesource.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/crosspackagetarget.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/crosspackagetarget.tgz new file mode 100644 index 0000000000..080274ad5f Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/crosspackagetarget.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/simplealpha.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/simplealpha.tgz new file mode 100644 index 0000000000..6fdbe3d7ba Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/simplealpha.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/simplebravo.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/simplebravo.tgz new file mode 100644 index 0000000000..2ea1d03cfc Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/simplebravo.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/withderivedlibrary.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/withderivedlibrary.tgz new file mode 100644 index 0000000000..7a6dd561ef Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/withderivedlibrary.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/withtwolayersderivedlibraries.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/withtwolayersderivedlibraries.tgz new file mode 100644 index 0000000000..8d91473cc4 Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r4/withtwolayersderivedlibraries.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/crosspackagesource.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/crosspackagesource.tgz new file mode 100644 index 0000000000..595519d9de Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/crosspackagesource.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/crosspackagetarget.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/crosspackagetarget.tgz new file mode 100644 index 0000000000..d1904430e8 Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/crosspackagetarget.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/simplealpha.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/simplealpha.tgz new file mode 100644 index 0000000000..a31cb42b49 Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/simplealpha.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/simplebravo.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/simplebravo.tgz new file mode 100644 index 0000000000..ca650e7fb5 Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/simplebravo.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/withderivedlibrary.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/withderivedlibrary.tgz new file mode 100644 index 0000000000..201a3c71b1 Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/withderivedlibrary.tgz differ diff --git a/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/withtwolayersderivedlibraries.tgz b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/withtwolayersderivedlibraries.tgz new file mode 100644 index 0000000000..fd1580aa6c Binary files /dev/null and b/cqf-fhir-utility/src/test/resources/org/opencds/cqf/fhir/utility/npm/r5/withtwolayersderivedlibraries.tgz differ