Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d2f0b73
Add all utilities classes as well as tests to support NPM. Test tgzs…
lukedegruchy Aug 29, 2025
533dba6
All R4 tests working.
lukedegruchy Aug 29, 2025
008137c
Start setting up R5.
lukedegruchy Aug 29, 2025
b9a2fa5
More R5 done.
lukedegruchy Aug 29, 2025
49d391a
All NPM tests working.
lukedegruchy Aug 29, 2025
3cfeec3
Delete tgzs that on normal OSs are mixed case instead of the lowercas…
lukedegruchy Aug 29, 2025
4cebd93
Add back files in lowercase.
lukedegruchy Aug 29, 2025
52cc66d
Address sonar feedback.
lukedegruchy Aug 29, 2025
7704709
Increase test coverage.
lukedegruchy Sep 2, 2025
fbc36c2
More test coverage.
lukedegruchy Sep 2, 2025
6fd4a08
Introduce the concept of an EngineInitializationContext, and use it t…
lukedegruchy Sep 2, 2025
4505eb7
Add code to actually add NPM functionality to the CqlEngine evaluatio…
lukedegruchy Sep 2, 2025
0e0f405
Add unit test for NpmLibraryProvider.
lukedegruchy Sep 2, 2025
42898e1
Merge remote-tracking branch 'origin/master' into ld-20250829-npm-for…
lukedegruchy Sep 2, 2025
0c0e42b
Spotless.
lukedegruchy Sep 2, 2025
f12eaf2
Sonar.
lukedegruchy Sep 2, 2025
3f2a29a
Add R4RepositoryOrNpmResourceProvider, which is meant to polymorphica…
lukedegruchy Sep 3, 2025
2d92ace
Add NPM measure evaluation tests, which directly hijack the feature f…
lukedegruchy Sep 3, 2025
e4ef9af
Spotless and Sonar.
lukedegruchy Sep 3, 2025
b1cd62c
More Sonar and support Repository or NPM queries for CareGaps.
lukedegruchy Sep 3, 2025
f64042a
Sonar and spotless.
lukedegruchy Sep 3, 2025
837433d
Add Spring bean for R4MultiMeasureServiceFactory. Fix checkstyle. G…
lukedegruchy Sep 3, 2025
389ea82
Cleanup TODOs. Spotless.
lukedegruchy Sep 4, 2025
03ed37d
Add more convenience methods for Engines and R4RepositoryOrNpmResourc…
lukedegruchy Sep 5, 2025
e646d80
Fix small flaw that broke at least one unit test.
lukedegruchy Sep 8, 2025
e338a3b
Add tests for new methods in R4RepositoryOrNpmResourceProvider and sm…
lukedegruchy Sep 8, 2025
11cc2af
Sonar.
lukedegruchy Sep 8, 2025
690bbb1
Merge remote-tracking branch 'origin/master' into ld-20250829-npm-for…
lukedegruchy Sep 8, 2025
f6474fa
Fix fallout from merge from master.
lukedegruchy Sep 8, 2025
c103f7c
Add more tests.
lukedegruchy Sep 8, 2025
bb22591
Tweak Either logic.
lukedegruchy Sep 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand All @@ -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;
}

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -100,17 +102,20 @@ 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("%s/%s/%s".formatted(getResourcePath(this.getClass()), CLASS_PATH, repositoryPath)));
this.npmPackageLoader = NpmPackageLoader.DEFAULT;
return this;
}

Expand All @@ -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();
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@
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;
import org.hl7.fhir.instance.model.api.IBaseParameters;
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 {
Expand Down Expand Up @@ -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() {
Expand Down
86 changes: 69 additions & 17 deletions cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/Engines.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,45 +32,48 @@
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);
}

private static Environment buildEnvironment(
IRepository repository,
EvaluationSettings settings,
TerminologyProvider terminologyProvider,
Map<String, DataProvider> dataProviders) {
Map<String, DataProvider> dataProviders,
NpmPackageLoader npmPackageLoader) {

var modelManager =
settings.getModelCache() != null ? new ModelManager(settings.getModelCache()) : new ModelManager();
Expand All @@ -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);
}
Expand Down Expand Up @@ -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<String> keys = new HashSet<String>();
Set<String> uris = new HashSet<String>();
Set<String> keys = new HashSet<>();
Set<String> uris = new HashSet<>();
for (var n : npmProcessor.getNamespaces()) {
if (!keys.contains(n.getName()) && !uris.contains(n.getUri())) {
libraryManager.getNamespaceManager().addNamespace(n);
Expand Down Expand Up @@ -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.
* <p>
* 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);
}
}
}
Loading
Loading