CQL hardening and Dstu3 unit tests (#2450)

* WIP for elm caching

* Bug fixes and a working DSTU3 unit test

* More tests, Address review feedback, add initial docs

* Update to cql-evaluator 1.2.0

* Fixes for documentation
This commit is contained in:
JP 2021-04-29 12:52:13 -06:00 committed by GitHub
parent fc1cef24ae
commit 239d2496ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 8091 additions and 774 deletions

View File

@ -70,6 +70,9 @@ page.server_jpa_mdm.mdm_operations=MDM Operations
page.server_jpa_mdm.mdm_details=MDM Technical Details page.server_jpa_mdm.mdm_details=MDM Technical Details
page.server_jpa_mdm.mdm_expansion=MDM Search Expansion page.server_jpa_mdm.mdm_expansion=MDM Search Expansion
section.server_jpa_cql.title=JPA Server: CQL
page.server_jpa_cql.cql=CQL Getting Started
section.server_jpa_partitioning.title=JPA Server: Partitioning and Multitenancy section.server_jpa_partitioning.title=JPA Server: Partitioning and Multitenancy
page.server_jpa_partitioning.partitioning=Partitioning and Multitenancy page.server_jpa_partitioning.partitioning=Partitioning and Multitenancy
page.server_jpa_partitioning.partition_interceptor_examples=Partition Interceptor Examples page.server_jpa_partitioning.partition_interceptor_examples=Partition Interceptor Examples

View File

@ -0,0 +1,38 @@
# CQL Getting Started
## Introduction
Clinical Quality Language (CQL) is a high-level, domain-specific language focused on clinical quality and targeted at measure and decision support artifact authors. HAPI embeds a [CQL engine](https://github.com/DBCG/cql_engine) allowing the evaluation of clinical knowledge artifacts that use CQL to describe their logic.
A more detailed description of CQL is available at the [CQL Specification Implementation Guide](https://cql.hl7.org/)
The FHIR [Clinical Reasoning module](http://www.hl7.org/fhir/clinicalreasoning-module.html) defines a set of resources, profiles, operations, etc. that can be used to work with clinical knowledge within FHIR. HAPI provides implementation for some of those operations, described in more detail below.
## Working Example
A complete working example of HAPI CQL can be found in the [JPA Server Starter](/hapi-fhir/docs/server_jpa/get_started.html) project. You may wish to browse its source to see how it is set up.
## Overview
To get up and running with HAPI CQL, you can enable it using the `hapi.properties` file in the JPA Server Starter by setting `hapi.fhir.enable_cql` key to `true`. If you are running your own server follow the instructions below to [enable it in HAPI FHIR directly](#cql-settings).
Once you've enabled CQL processing, the next step is to load the appropriate knowledge artifact resources into your server.
## CQL Settings
There are two Spring beans available that add CQL processing to HAPI. You can enable CQL processing by importing the appropriate version for your server configuration.
* `ca.uhn.fhir.cql.config.CqlDstu3Config`
* `ca.uhn.fhir.cql.config.CqlR4Config`
## Operations
HAPI provides implementations for some Measure operations for DSTU3 and R4
### $evaluate-measure
The [$evaluate-measure](http://hl7.org/fhir/measure-operation-evaluate-measure.html) operation allows the evaluation of a clinical quality measure. This operation is invoked on an instance of a Measure resource:
`http://base/Measure/measureId/$evaluate-measure?subject=124&periodStart=2014-01&periodend=2014-03`
The Measure will be evaluated, including any CQL that is referenced. The CQL evaluation requires that all the supporting knowledge artifacts for a given Measure be loaded on the HAPI server, including `Libaries` and `ValueSets`.

View File

@ -22,14 +22,15 @@ package ca.uhn.fhir.cql.common.provider;
import org.cqframework.cql.cql2elm.FhirLibrarySourceProvider; import org.cqframework.cql.cql2elm.FhirLibrarySourceProvider;
import org.hl7.elm.r1.VersionedIdentifier; import org.hl7.elm.r1.VersionedIdentifier;
import org.opencds.cqf.cql.evaluator.cql2elm.content.LibraryContentType;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.util.function.Function; import java.util.function.Function;
public class LibrarySourceProvider<LibraryType, AttachmentType> public class LibraryContentProvider<LibraryType, AttachmentType>
implements org.cqframework.cql.cql2elm.LibrarySourceProvider { implements org.opencds.cqf.cql.evaluator.cql2elm.content.LibraryContentProvider {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LibrarySourceProvider.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LibraryContentProvider.class);
private FhirLibrarySourceProvider innerProvider; private FhirLibrarySourceProvider innerProvider;
private LibraryResolutionProvider<LibraryType> provider; private LibraryResolutionProvider<LibraryType> provider;
@ -37,7 +38,7 @@ public class LibrarySourceProvider<LibraryType, AttachmentType>
private Function<AttachmentType, String> getContentType; private Function<AttachmentType, String> getContentType;
private Function<AttachmentType, byte[]> getContent; private Function<AttachmentType, byte[]> getContent;
public LibrarySourceProvider(LibraryResolutionProvider<LibraryType> provider, public LibraryContentProvider(LibraryResolutionProvider<LibraryType> provider,
Function<LibraryType, Iterable<AttachmentType>> getAttachments, Function<LibraryType, Iterable<AttachmentType>> getAttachments,
Function<AttachmentType, String> getContentType, Function<AttachmentType, byte[]> getContent) { Function<AttachmentType, String> getContentType, Function<AttachmentType, byte[]> getContent) {
@ -50,7 +51,13 @@ public class LibrarySourceProvider<LibraryType, AttachmentType>
} }
@Override @Override
public InputStream getLibrarySource(VersionedIdentifier versionedIdentifier) { public InputStream getLibraryContent(VersionedIdentifier versionedIdentifier, LibraryContentType libraryContentType){
// TODO: Support loading ELM
if (libraryContentType != LibraryContentType.CQL) {
return null;
}
try { try {
LibraryType lib = this.provider.resolveLibraryByName(versionedIdentifier.getId(), LibraryType lib = this.provider.resolveLibraryByName(versionedIdentifier.getId(),
versionedIdentifier.getVersion()); versionedIdentifier.getVersion());

View File

@ -27,6 +27,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.cqframework.cql.cql2elm.model.Model; import org.cqframework.cql.cql2elm.model.Model;
import org.cqframework.cql.elm.execution.Library;
import org.hl7.elm.r1.VersionedIdentifier; import org.hl7.elm.r1.VersionedIdentifier;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -46,4 +47,9 @@ public abstract class BaseCqlConfig {
Map<VersionedIdentifier, Model> globalModelCache() { Map<VersionedIdentifier, Model> globalModelCache() {
return new ConcurrentHashMap<VersionedIdentifier, Model>(); return new ConcurrentHashMap<VersionedIdentifier, Model>();
} }
@Bean(name="globalLibraryCache")
Map<org.cqframework.cql.elm.execution.VersionedIdentifier, Library> globalLibraryCache() {
return new ConcurrentHashMap<org.cqframework.cql.elm.execution.VersionedIdentifier, Library>();
}
} }

View File

@ -26,12 +26,20 @@ import ca.uhn.fhir.cql.common.provider.EvaluationProviderFactory;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider; import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.cql.dstu3.evaluation.ProviderFactory; import ca.uhn.fhir.cql.dstu3.evaluation.ProviderFactory;
import ca.uhn.fhir.cql.dstu3.helper.LibraryHelper; import ca.uhn.fhir.cql.dstu3.helper.LibraryHelper;
import ca.uhn.fhir.cql.dstu3.listener.ElmCacheResourceChangeListener;
import ca.uhn.fhir.cql.dstu3.provider.JpaTerminologyProvider; import ca.uhn.fhir.cql.dstu3.provider.JpaTerminologyProvider;
import ca.uhn.fhir.cql.dstu3.provider.LibraryResolutionProviderImpl; import ca.uhn.fhir.cql.dstu3.provider.LibraryResolutionProviderImpl;
import ca.uhn.fhir.cql.dstu3.provider.MeasureOperationsProvider; import ca.uhn.fhir.cql.dstu3.provider.MeasureOperationsProvider;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
import ca.uhn.fhir.jpa.term.api.ITermReadSvcDstu3; import ca.uhn.fhir.jpa.term.api.ITermReadSvcDstu3;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import org.cqframework.cql.cql2elm.CqlTranslatorOptions;
import org.cqframework.cql.cql2elm.model.Model; import org.cqframework.cql.cql2elm.model.Model;
import org.cqframework.cql.elm.execution.Library;
import org.hl7.elm.r1.VersionedIdentifier; import org.hl7.elm.r1.VersionedIdentifier;
import org.opencds.cqf.cql.engine.fhir.model.Dstu3FhirModelResolver; import org.opencds.cqf.cql.engine.fhir.model.Dstu3FhirModelResolver;
import org.opencds.cqf.cql.engine.model.ModelResolver; import org.opencds.cqf.cql.engine.model.ModelResolver;
@ -60,7 +68,7 @@ public class CqlDstu3Config extends BaseCqlConfig {
@Lazy @Lazy
@Bean @Bean
LibraryResolutionProvider libraryResolutionProvider() { LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> libraryResolutionProvider() {
return new LibraryResolutionProviderImpl(); return new LibraryResolutionProviderImpl();
} }
@ -70,13 +78,28 @@ public class CqlDstu3Config extends BaseCqlConfig {
return new MeasureOperationsProvider(); return new MeasureOperationsProvider();
} }
@Lazy
@Bean @Bean
public ModelResolver fhirModelResolver() { public ModelResolver fhirModelResolver() {
return new CachingModelResolverDecorator(new Dstu3FhirModelResolver()); return new CachingModelResolverDecorator(new Dstu3FhirModelResolver());
} }
@Lazy
@Bean @Bean
public LibraryHelper libraryHelper(Map<VersionedIdentifier, Model> globalModelCache) { public LibraryHelper libraryHelper(Map<VersionedIdentifier, Model> globalModelCache, Map<org.cqframework.cql.elm.execution.VersionedIdentifier, Library> globalLibraryCache, CqlTranslatorOptions cqlTranslatorOptions) {
return new LibraryHelper(globalModelCache); return new LibraryHelper(globalModelCache, globalLibraryCache, cqlTranslatorOptions);
}
@Bean
public CqlTranslatorOptions cqlTranslatorOptions() {
return CqlTranslatorOptions.defaultOptions().withCompatibilityLevel("1.3");
}
@Bean
public ElmCacheResourceChangeListener elmCacheResourceChangeListener(IResourceChangeListenerRegistry resourceChangeListenerRegistry, IFhirResourceDao<org.hl7.fhir.dstu3.model.Library> libraryDao, Map<org.cqframework.cql.elm.execution.VersionedIdentifier, Library> globalLibraryCache) {
ElmCacheResourceChangeListener listener = new ElmCacheResourceChangeListener(libraryDao, globalLibraryCache);
resourceChangeListenerRegistry.registerResourceResourceChangeListener("Library", new SearchParameterMap(), listener, 1000);
return listener;
} }
} }

View File

@ -27,12 +27,20 @@ import ca.uhn.fhir.cql.common.provider.EvaluationProviderFactory;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider; import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.cql.r4.evaluation.ProviderFactory; import ca.uhn.fhir.cql.r4.evaluation.ProviderFactory;
import ca.uhn.fhir.cql.r4.helper.LibraryHelper; import ca.uhn.fhir.cql.r4.helper.LibraryHelper;
import ca.uhn.fhir.cql.r4.listener.ElmCacheResourceChangeListener;
import ca.uhn.fhir.cql.r4.provider.JpaTerminologyProvider; import ca.uhn.fhir.cql.r4.provider.JpaTerminologyProvider;
import ca.uhn.fhir.cql.r4.provider.LibraryResolutionProviderImpl; import ca.uhn.fhir.cql.r4.provider.LibraryResolutionProviderImpl;
import ca.uhn.fhir.cql.r4.provider.MeasureOperationsProvider; import ca.uhn.fhir.cql.r4.provider.MeasureOperationsProvider;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
import ca.uhn.fhir.jpa.term.api.ITermReadSvcR4; import ca.uhn.fhir.jpa.term.api.ITermReadSvcR4;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import org.cqframework.cql.cql2elm.CqlTranslatorOptions;
import org.cqframework.cql.cql2elm.model.Model; import org.cqframework.cql.cql2elm.model.Model;
import org.cqframework.cql.elm.execution.Library;
import org.hl7.elm.r1.VersionedIdentifier; import org.hl7.elm.r1.VersionedIdentifier;
import org.opencds.cqf.cql.engine.fhir.model.R4FhirModelResolver; import org.opencds.cqf.cql.engine.fhir.model.R4FhirModelResolver;
import org.opencds.cqf.cql.engine.model.ModelResolver; import org.opencds.cqf.cql.engine.model.ModelResolver;
@ -55,19 +63,21 @@ public class CqlR4Config extends BaseCqlConfig {
@Lazy @Lazy
@Bean @Bean
TerminologyProvider terminologyProvider(ITermReadSvcR4 theITermReadSvc, DaoRegistry theDaoRegistry, IValidationSupport theValidationSupport) { TerminologyProvider terminologyProvider(ITermReadSvcR4 theITermReadSvc, DaoRegistry theDaoRegistry,
IValidationSupport theValidationSupport) {
return new JpaTerminologyProvider(theITermReadSvc, theDaoRegistry, theValidationSupport); return new JpaTerminologyProvider(theITermReadSvc, theDaoRegistry, theValidationSupport);
} }
@Lazy @Lazy
@Bean @Bean
EvaluationProviderFactory evaluationProviderFactory(FhirContext theFhirContext, DaoRegistry theDaoRegistry, TerminologyProvider theLocalSystemTerminologyProvider, ModelResolver modelResolver) { EvaluationProviderFactory evaluationProviderFactory(FhirContext theFhirContext, DaoRegistry theDaoRegistry,
TerminologyProvider theLocalSystemTerminologyProvider, ModelResolver modelResolver) {
return new ProviderFactory(theFhirContext, theDaoRegistry, theLocalSystemTerminologyProvider, modelResolver); return new ProviderFactory(theFhirContext, theDaoRegistry, theLocalSystemTerminologyProvider, modelResolver);
} }
@Lazy @Lazy
@Bean @Bean
LibraryResolutionProvider libraryResolutionProvider() { LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResolutionProvider() {
return new LibraryResolutionProviderImpl(); return new LibraryResolutionProviderImpl();
} }
@ -77,13 +87,30 @@ public class CqlR4Config extends BaseCqlConfig {
return new MeasureOperationsProvider(); return new MeasureOperationsProvider();
} }
@Lazy
@Bean @Bean
public ModelResolver fhirModelResolver() { public ModelResolver fhirModelResolver() {
return new CachingModelResolverDecorator(new R4FhirModelResolver()); return new CachingModelResolverDecorator(new R4FhirModelResolver());
} }
@Lazy
@Bean @Bean
public LibraryHelper libraryHelper(Map<VersionedIdentifier, Model> globalModelCache) { public LibraryHelper libraryHelper(Map<VersionedIdentifier, Model> globalModelCache,
return new LibraryHelper(globalModelCache); Map<org.cqframework.cql.elm.execution.VersionedIdentifier, Library> globalLibraryCache,
CqlTranslatorOptions cqlTranslatorOptions) {
return new LibraryHelper(globalModelCache, globalLibraryCache, cqlTranslatorOptions);
}
@Lazy
@Bean
public CqlTranslatorOptions cqlTranslatorOptions() {
return CqlTranslatorOptions.defaultOptions();
}
@Bean
public ElmCacheResourceChangeListener elmCacheResourceChangeListener(IResourceChangeListenerRegistry resourceChangeListenerRegistry, IFhirResourceDao<org.hl7.fhir.r4.model.Library> libraryDao, Map<org.cqframework.cql.elm.execution.VersionedIdentifier, Library> globalLibraryCache) {
ElmCacheResourceChangeListener listener = new ElmCacheResourceChangeListener(libraryDao, globalLibraryCache);
resourceChangeListenerRegistry.registerResourceResourceChangeListener("Library", new SearchParameterMap(), listener, 1000);
return listener;
} }
} }

View File

@ -39,6 +39,7 @@ import org.hl7.fhir.dstu3.model.Measure;
import org.hl7.fhir.dstu3.model.MeasureReport; import org.hl7.fhir.dstu3.model.MeasureReport;
import org.hl7.fhir.dstu3.model.Observation; import org.hl7.fhir.dstu3.model.Observation;
import org.hl7.fhir.dstu3.model.Patient; import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.dstu3.model.Quantity;
import org.hl7.fhir.dstu3.model.Reference; import org.hl7.fhir.dstu3.model.Reference;
import org.hl7.fhir.dstu3.model.Resource; import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.dstu3.model.StringType;
@ -677,7 +678,7 @@ public class MeasureEvaluation {
.setValue(new StringType(sdeKey)); .setValue(new StringType(sdeKey));
obsExtension.addExtension(extExtPop); obsExtension.addExtension(extExtPop);
obs.addExtension(obsExtension); obs.addExtension(obsExtension);
obs.setValue(new IntegerType(sdeAccumulatorValue)); obs.setValue(new Quantity(sdeAccumulatorValue));
if(!isSingle) { if(!isSingle) {
valueCoding.setCode(sdeAccumulatorKey); valueCoding.setCode(sdeAccumulatorKey);
obsCodeableConcept.setCoding(Collections.singletonList(valueCoding)); obsCodeableConcept.setCoding(Collections.singletonList(valueCoding));

View File

@ -20,11 +20,12 @@ package ca.uhn.fhir.cql.dstu3.helper;
* #L% * #L%
*/ */
import ca.uhn.fhir.cql.common.evaluation.LibraryLoader;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider; import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.cql.common.provider.LibrarySourceProvider;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.cqframework.cql.cql2elm.LibraryManager; import org.cqframework.cql.cql2elm.LibraryManager;
import ca.uhn.fhir.cql.common.provider.LibraryContentProvider;
import org.cqframework.cql.cql2elm.CqlTranslatorOptions;
import org.cqframework.cql.cql2elm.ModelManager; import org.cqframework.cql.cql2elm.ModelManager;
import org.cqframework.cql.cql2elm.model.Model; import org.cqframework.cql.cql2elm.model.Model;
import org.cqframework.cql.elm.execution.Library; import org.cqframework.cql.elm.execution.Library;
@ -36,11 +37,13 @@ import org.hl7.fhir.dstu3.model.Reference;
import org.hl7.fhir.dstu3.model.RelatedArtifact; import org.hl7.fhir.dstu3.model.RelatedArtifact;
import org.hl7.fhir.dstu3.model.Resource; import org.hl7.fhir.dstu3.model.Resource;
import org.opencds.cqf.cql.evaluator.cql2elm.model.CacheAwareModelManager; import org.opencds.cqf.cql.evaluator.cql2elm.model.CacheAwareModelManager;
import org.opencds.cqf.cql.evaluator.engine.execution.PrivateCachingLibraryLoaderDecorator;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.opencds.cqf.cql.evaluator.engine.execution.CacheAwareLibraryLoaderDecorator;
import org.opencds.cqf.cql.evaluator.engine.execution.TranslatingLibraryLoader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -48,22 +51,24 @@ public class LibraryHelper {
private static final Logger ourLog = LoggerFactory.getLogger(LibraryHelper.class); private static final Logger ourLog = LoggerFactory.getLogger(LibraryHelper.class);
private final Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache; private final Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache;
private Map<VersionedIdentifier, Library> libraryCache;
private CqlTranslatorOptions translatorOptions;
public LibraryHelper(Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache) {
public LibraryHelper(Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache, Map<VersionedIdentifier, Library> libraryCache, CqlTranslatorOptions translatorOptions) {
this.modelCache = modelCache; this.modelCache = modelCache;
this.libraryCache = libraryCache;
this.translatorOptions = translatorOptions;
} }
public org.opencds.cqf.cql.engine.execution.LibraryLoader createLibraryLoader( public org.opencds.cqf.cql.engine.execution.LibraryLoader createLibraryLoader(
LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> provider) { LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> provider) {
ModelManager modelManager = new CacheAwareModelManager(this.modelCache); ModelManager modelManager = new CacheAwareModelManager(this.modelCache);
LibraryManager libraryManager = new LibraryManager(modelManager);
libraryManager.getLibrarySourceLoader().clearProviders();
libraryManager.getLibrarySourceLoader().registerProvider( List<org.opencds.cqf.cql.evaluator.cql2elm.content.LibraryContentProvider> contentProviders = Collections.singletonList(new LibraryContentProvider<org.hl7.fhir.dstu3.model.Library, Attachment>(
new LibrarySourceProvider<org.hl7.fhir.dstu3.model.Library, Attachment>(
provider, x -> x.getContent(), x -> x.getContentType(), x -> x.getData())); provider, x -> x.getContent(), x -> x.getContentType(), x -> x.getData()));
return new PrivateCachingLibraryLoaderDecorator(new LibraryLoader(libraryManager, modelManager)); return new CacheAwareLibraryLoaderDecorator(new TranslatingLibraryLoader(modelManager, contentProviders, translatorOptions), libraryCache);
} }
public List<Library> loadLibraries(Measure measure, public List<Library> loadLibraries(Measure measure,
@ -115,11 +120,13 @@ public class LibraryHelper {
if (artifact.hasType() && artifact.getType().equals(RelatedArtifact.RelatedArtifactType.DEPENDSON) && artifact.hasResource() && artifact.getResource().hasReference()) { if (artifact.hasType() && artifact.getType().equals(RelatedArtifact.RelatedArtifactType.DEPENDSON) && artifact.hasResource() && artifact.getResource().hasReference()) {
if (artifact.getResource().getReferenceElement().getResourceType().equals("Library")) { if (artifact.getResource().getReferenceElement().getResourceType().equals("Library")) {
org.hl7.fhir.dstu3.model.Library library = libraryResourceProvider.resolveLibraryById(artifact.getResource().getReferenceElement().getIdPart()); org.hl7.fhir.dstu3.model.Library library = libraryResourceProvider.resolveLibraryById(artifact.getResource().getReferenceElement().getIdPart());
if (library != null) {
if (library != null && isLogicLibrary(library)) { if (isLogicLibrary(library)) {
libraries.add( libraries.add(libraryLoader
libraryLoader.load(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion())) .load(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion())));
); } else {
ourLog.warn("Library {} not included as part of evaluation context. Only Libraries with the 'logic-library' type are included.", library.getId());
}
} }
} }
} }

View File

@ -0,0 +1,72 @@
package ca.uhn.fhir.cql.dstu3.listener;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.cqframework.cql.elm.execution.Library;
import org.cqframework.cql.elm.execution.VersionedIdentifier;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.cache.IResourceChangeEvent;
import ca.uhn.fhir.jpa.cache.IResourceChangeListener;
public class ElmCacheResourceChangeListener implements IResourceChangeListener {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ElmCacheResourceChangeListener.class);
private IFhirResourceDao<org.hl7.fhir.dstu3.model.Library> libraryDao;
private Map<VersionedIdentifier, Library> globalLibraryCache;
public ElmCacheResourceChangeListener(IFhirResourceDao<org.hl7.fhir.dstu3.model.Library> libraryDao, Map<VersionedIdentifier, Library> globalLibraryCache) {
this.libraryDao = libraryDao;
this.globalLibraryCache = globalLibraryCache;
}
@Override
public void handleInit(Collection<IIdType> theResourceIds) {
// Intentionally empty. Only cache ELM on eval request
}
@Override
public void handleChange(IResourceChangeEvent theResourceChangeEvent) {
if (theResourceChangeEvent == null) {
return;
}
this.invalidateCacheByIds(theResourceChangeEvent.getDeletedResourceIds());
this.invalidateCacheByIds(theResourceChangeEvent.getUpdatedResourceIds());
}
private void invalidateCacheByIds(List<IIdType> theIds) {
if (theIds == null) {
return;
}
for (IIdType id : theIds) {
this.invalidateCacheById(id);
}
}
private void invalidateCacheById(IIdType theId) {
if (!theId.getResourceType().equals("Library")) {
return;
}
try {
org.hl7.fhir.dstu3.model.Library library = this.libraryDao.read(theId);
this.globalLibraryCache.remove(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion()));
}
// This happens when a Library is deleted entirely so it's impossible to look up name and version.
catch (Exception e) {
// TODO: This needs to be smarter... the issue is that ELM is cached with library name and version as the key since
// that's the access path the CQL engine uses, but change notifications occur with the resource Id, which is not
// necessarily tied to the resource name. In any event, if a unknown resource is deleted, clear all libraries as a workaround.
// One option is to maintain a cache with multiple indices.
ourLog.debug("Failed to locate resource {} to look up name and version. Clearing all libraries from cache.", theId.getValueAsString());
this.globalLibraryCache.clear();
}
}
}

View File

@ -40,8 +40,6 @@ import org.hl7.fhir.dstu3.model.MeasureReport;
import org.hl7.fhir.dstu3.model.StringType; import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRException;
import org.opencds.cqf.cql.engine.execution.LibraryLoader; import org.opencds.cqf.cql.engine.execution.LibraryLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -54,8 +52,6 @@ import org.springframework.stereotype.Component;
*/ */
@Component @Component
public class MeasureOperationsProvider { public class MeasureOperationsProvider {
private static final Logger logger = LoggerFactory.getLogger(MeasureOperationsProvider.class);
@Autowired @Autowired
private LibraryResolutionProvider<Library> libraryResolutionProvider; private LibraryResolutionProvider<Library> libraryResolutionProvider;
@Autowired @Autowired

View File

@ -93,8 +93,7 @@ public class MeasureEvaluation {
} }
boolean isSingle = true; boolean isSingle = true;
return evaluate(measure, context, return evaluate(measure, context, patient == null ? Collections.emptyList() : Collections.singletonList(patient),
patient == null ? Collections.emptyList() : Collections.singletonList(patient),
MeasureReport.MeasureReportType.INDIVIDUAL, isSingle); MeasureReport.MeasureReportType.INDIVIDUAL, isSingle);
} }
@ -148,7 +147,8 @@ public class MeasureEvaluation {
} }
} }
private Resource evaluateObservationCriteria(Context context, Patient patient, Resource resource, Measure.MeasureGroupPopulationComponent pop, MeasureReport report) { private Resource evaluateObservationCriteria(Context context, Patient patient, Resource resource,
Measure.MeasureGroupPopulationComponent pop, MeasureReport report) {
if (pop == null || !pop.hasCriteria()) { if (pop == null || !pop.hasCriteria()) {
return null; return null;
} }
@ -160,7 +160,8 @@ public class MeasureEvaluation {
String observationName = pop.getCriteria().getExpression(); String observationName = pop.getCriteria().getExpression();
ExpressionDef ed = context.resolveExpressionRef(observationName); ExpressionDef ed = context.resolveExpressionRef(observationName);
if (!(ed instanceof FunctionDef)) { if (!(ed instanceof FunctionDef)) {
throw new IllegalArgumentException(String.format("Measure observation %s does not reference a function definition", observationName)); throw new IllegalArgumentException(
String.format("Measure observation %s does not reference a function definition", observationName));
} }
Object result = null; Object result = null;
@ -168,8 +169,7 @@ public class MeasureEvaluation {
try { try {
context.push(new Variable().withName(((FunctionDef) ed).getOperand().get(0).getName()).withValue(resource)); context.push(new Variable().withName(((FunctionDef) ed).getOperand().get(0).getName()).withValue(resource));
result = ed.getExpression().evaluate(context); result = ed.getExpression().evaluate(context);
} } finally {
finally {
context.popWindow(); context.popWindow();
} }
@ -184,13 +184,10 @@ public class MeasureEvaluation {
cc.setText(observationName); cc.setText(observationName);
obs.setCode(cc); obs.setCode(cc);
Extension obsExtension = new Extension().setUrl("http://hl7.org/fhir/StructureDefinition/cqf-measureInfo"); Extension obsExtension = new Extension().setUrl("http://hl7.org/fhir/StructureDefinition/cqf-measureInfo");
Extension extExtMeasure = new Extension() Extension extExtMeasure = new Extension().setUrl("measure")
.setUrl("measure")
.setValue(new CanonicalType("http://hl7.org/fhir/us/cqfmeasures/" + report.getMeasure())); .setValue(new CanonicalType("http://hl7.org/fhir/us/cqfmeasures/" + report.getMeasure()));
obsExtension.addExtension(extExtMeasure); obsExtension.addExtension(extExtMeasure);
Extension extExtPop = new Extension() Extension extExtPop = new Extension().setUrl("populationId").setValue(new StringType(observationName));
.setUrl("populationId")
.setValue(new StringType(observationName));
obsExtension.addExtension(extExtPop); obsExtension.addExtension(extExtPop);
obs.addExtension(obsExtension); obs.addExtension(obsExtension);
return obs; return obs;
@ -256,8 +253,7 @@ public class MeasureEvaluation {
return inPopulation; return inPopulation;
} }
private void addPopulationCriteriaReport(MeasureReport report, private void addPopulationCriteriaReport(MeasureReport report, MeasureReport.MeasureReportGroupComponent reportGroup,
MeasureReport.MeasureReportGroupComponent reportGroup,
Measure.MeasureGroupPopulationComponent populationCriteria, int populationCount, Measure.MeasureGroupPopulationComponent populationCriteria, int populationCount,
Iterable<Patient> patientPopulation) { Iterable<Patient> patientPopulation) {
if (populationCriteria != null) { if (populationCriteria != null) {
@ -268,8 +264,7 @@ public class MeasureEvaluation {
SUBJECTLIST.setId(UUID.randomUUID().toString()); SUBJECTLIST.setId(UUID.randomUUID().toString());
populationReport.setSubjectResults(new Reference().setReference("#" + SUBJECTLIST.getId())); populationReport.setSubjectResults(new Reference().setReference("#" + SUBJECTLIST.getId()));
for (Patient patient : patientPopulation) { for (Patient patient : patientPopulation) {
ListResource.ListEntryComponent entry = new ListResource.ListEntryComponent() ListResource.ListEntryComponent entry = new ListResource.ListEntryComponent().setItem(new Reference()
.setItem(new Reference()
.setReference(patient.getIdElement().getIdPart().startsWith("Patient/") .setReference(patient.getIdElement().getIdPart().startsWith("Patient/")
? patient.getIdElement().getIdPart() ? patient.getIdElement().getIdPart()
: String.format("Patient/%s", patient.getIdElement().getIdPart())) : String.format("Patient/%s", patient.getIdElement().getIdPart()))
@ -288,8 +283,8 @@ public class MeasureEvaluation {
MeasureReportBuilder reportBuilder = new MeasureReportBuilder(); MeasureReportBuilder reportBuilder = new MeasureReportBuilder();
reportBuilder.buildStatus("complete"); reportBuilder.buildStatus("complete");
reportBuilder.buildType(type); reportBuilder.buildType(type);
reportBuilder.buildMeasureReference( reportBuilder
measure.getIdElement().getResourceType() + "/" + measure.getIdElement().getIdPart()); .buildMeasureReference(measure.getIdElement().getResourceType() + "/" + measure.getIdElement().getIdPart());
if (type == MeasureReport.MeasureReportType.INDIVIDUAL && !patients.isEmpty()) { if (type == MeasureReport.MeasureReportType.INDIVIDUAL && !patients.isEmpty()) {
IdType patientId = patients.get(0).getIdElement(); IdType patientId = patients.get(0).getIdElement();
reportBuilder.buildPatientReference(patientId.getResourceType() + "/" + patientId.getIdPart()); reportBuilder.buildPatientReference(patientId.getResourceType() + "/" + patientId.getIdPart());
@ -427,43 +422,37 @@ public class MeasureEvaluation {
// For each patient in the initial population // For each patient in the initial population
for (Patient patient : patients) { for (Patient patient : patients) {
// Are they in the initial population? // Are they in the initial population?
boolean inInitialPopulation = evaluatePopulationCriteria(context, patient, boolean inInitialPopulation = evaluatePopulationCriteria(context, patient, initialPopulationCriteria,
initialPopulationCriteria, initialPopulation, initialPopulationPatients, null, null, initialPopulation, initialPopulationPatients, null, null, null);
null); populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources, codeToResourceMap);
populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources,
codeToResourceMap);
if (inInitialPopulation) { if (inInitialPopulation) {
// Are they in the denominator? // Are they in the denominator?
boolean inDenominator = evaluatePopulationCriteria(context, patient, denominatorCriteria, boolean inDenominator = evaluatePopulationCriteria(context, patient, denominatorCriteria, denominator,
denominator, denominatorPatients, denominatorExclusionCriteria, denominatorPatients, denominatorExclusionCriteria, denominatorExclusion,
denominatorExclusion, denominatorExclusionPatients); denominatorExclusionPatients);
populateResourceMap(context, MeasurePopulationType.DENOMINATOR, resources, populateResourceMap(context, MeasurePopulationType.DENOMINATOR, resources, codeToResourceMap);
codeToResourceMap);
if (inDenominator) { if (inDenominator) {
// Are they in the numerator? // Are they in the numerator?
boolean inNumerator = evaluatePopulationCriteria(context, patient, numeratorCriteria, boolean inNumerator = evaluatePopulationCriteria(context, patient, numeratorCriteria, numerator,
numerator, numeratorPatients, numeratorExclusionCriteria, numeratorExclusion, numeratorPatients, numeratorExclusionCriteria, numeratorExclusion,
numeratorExclusionPatients); numeratorExclusionPatients);
populateResourceMap(context, MeasurePopulationType.NUMERATOR, resources, populateResourceMap(context, MeasurePopulationType.NUMERATOR, resources, codeToResourceMap);
codeToResourceMap);
if (!inNumerator && inDenominator && (denominatorExceptionCriteria != null)) { if (!inNumerator && inDenominator && (denominatorExceptionCriteria != null)) {
// Are they in the denominator exception? // Are they in the denominator exception?
boolean inException = false; boolean inException = false;
for (Resource resource : evaluateCriteria(context, patient, for (Resource resource : evaluateCriteria(context, patient, denominatorExceptionCriteria)) {
denominatorExceptionCriteria)) {
inException = true; inException = true;
denominatorException.put(resource.getIdElement().getIdPart(), resource); denominatorException.put(resource.getIdElement().getIdPart(), resource);
denominator.remove(resource.getIdElement().getIdPart()); denominator.remove(resource.getIdElement().getIdPart());
populateResourceMap(context, MeasurePopulationType.DENOMINATOREXCEPTION, populateResourceMap(context, MeasurePopulationType.DENOMINATOREXCEPTION, resources,
resources, codeToResourceMap); codeToResourceMap);
} }
if (inException) { if (inException) {
if (denominatorExceptionPatients != null) { if (denominatorExceptionPatients != null) {
denominatorExceptionPatients.put(patient.getIdElement().getIdPart(), denominatorExceptionPatients.put(patient.getIdElement().getIdPart(), patient);
patient);
} }
if (denominatorPatients != null) { if (denominatorPatients != null) {
denominatorPatients.remove(patient.getIdElement().getIdPart()); denominatorPatients.remove(patient.getIdElement().getIdPart());
@ -488,22 +477,20 @@ public class MeasureEvaluation {
for (Patient patient : patients) { for (Patient patient : patients) {
// Are they in the initial population? // Are they in the initial population?
boolean inInitialPopulation = evaluatePopulationCriteria(context, patient, boolean inInitialPopulation = evaluatePopulationCriteria(context, patient, initialPopulationCriteria,
initialPopulationCriteria, initialPopulation, initialPopulationPatients, null, null, initialPopulation, initialPopulationPatients, null, null, null);
null); populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources, codeToResourceMap);
populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources,
codeToResourceMap);
if (inInitialPopulation) { if (inInitialPopulation) {
// Are they in the measure population? // Are they in the measure population?
boolean inMeasurePopulation = evaluatePopulationCriteria(context, patient, boolean inMeasurePopulation = evaluatePopulationCriteria(context, patient, measurePopulationCriteria,
measurePopulationCriteria, measurePopulation, measurePopulationPatients, measurePopulation, measurePopulationPatients, measurePopulationExclusionCriteria,
measurePopulationExclusionCriteria, measurePopulationExclusion, measurePopulationExclusion, measurePopulationExclusionPatients);
measurePopulationExclusionPatients);
if (inMeasurePopulation) { if (inMeasurePopulation) {
for (Resource resource : measurePopulation.values()) { for (Resource resource : measurePopulation.values()) {
Resource observation = evaluateObservationCriteria(context, patient, resource, measureObservationCriteria, report); Resource observation = evaluateObservationCriteria(context, patient, resource,
measureObservationCriteria, report);
measureObservation.put(resource.getIdElement().getIdPart(), observation); measureObservation.put(resource.getIdElement().getIdPart(), observation);
report.addContained(observation); report.addContained(observation);
report.getEvaluatedResource().add(new Reference("#" + observation.getId())); report.getEvaluatedResource().add(new Reference("#" + observation.getId()));
@ -519,11 +506,9 @@ public class MeasureEvaluation {
// For each patient in the patient list // For each patient in the patient list
for (Patient patient : patients) { for (Patient patient : patients) {
evaluatePopulationCriteria(context, patient, evaluatePopulationCriteria(context, patient, initialPopulationCriteria, initialPopulation,
initialPopulationCriteria, initialPopulation, initialPopulationPatients, null, null, initialPopulationPatients, null, null, null);
null); populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources, codeToResourceMap);
populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources,
codeToResourceMap);
populateSDEAccumulators(measure, context, patient, sdeAccumulators, sde); populateSDEAccumulators(measure, context, patient, sdeAccumulators, sde);
} }
@ -535,8 +520,7 @@ public class MeasureEvaluation {
addPopulationCriteriaReport(report, reportGroup, initialPopulationCriteria, addPopulationCriteriaReport(report, reportGroup, initialPopulationCriteria,
initialPopulation != null ? initialPopulation.size() : 0, initialPopulation != null ? initialPopulation.size() : 0,
initialPopulationPatients != null ? initialPopulationPatients.values() : null); initialPopulationPatients != null ? initialPopulationPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, numeratorCriteria, addPopulationCriteriaReport(report, reportGroup, numeratorCriteria, numerator != null ? numerator.size() : 0,
numerator != null ? numerator.size() : 0,
numeratorPatients != null ? numeratorPatients.values() : null); numeratorPatients != null ? numeratorPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, numeratorExclusionCriteria, addPopulationCriteriaReport(report, reportGroup, numeratorExclusionCriteria,
numeratorExclusion != null ? numeratorExclusion.size() : 0, numeratorExclusion != null ? numeratorExclusion.size() : 0,
@ -590,10 +574,13 @@ public class MeasureEvaluation {
return report; return report;
} }
private void populateSDEAccumulators(Measure measure, Context context, Patient patient,HashMap<String, HashMap<String, Integer>> sdeAccumulators, private void populateSDEAccumulators(Measure measure, Context context, Patient patient,
HashMap<String, HashMap<String, Integer>> sdeAccumulators,
List<Measure.MeasureSupplementalDataComponent> sde) { List<Measure.MeasureSupplementalDataComponent> sde) {
context.setContextValue("Patient", patient.getIdElement().getIdPart()); context.setContextValue("Patient", patient.getIdElement().getIdPart());
List<Object> sdeList = sde.stream().map(sdeItem -> context.resolveExpressionRef(sdeItem.getCriteria().getExpression()).evaluate(context)).collect(Collectors.toList()); List<Object> sdeList = sde.stream()
.map(sdeItem -> context.resolveExpressionRef(sdeItem.getCriteria().getExpression()).evaluate(context))
.collect(Collectors.toList());
if (!sdeList.isEmpty()) { if (!sdeList.isEmpty()) {
for (int i = 0; i < sdeList.size(); i++) { for (int i = 0; i < sdeList.size(); i++) {
Object sdeListItem = sdeList.get(i); Object sdeListItem = sdeList.get(i);
@ -643,8 +630,9 @@ public class MeasureEvaluation {
} }
} }
private MeasureReport processAccumulators(MeasureReport report, HashMap<String, HashMap<String, Integer>> sdeAccumulators, private MeasureReport processAccumulators(MeasureReport report,
List<Measure.MeasureSupplementalDataComponent> sde, boolean isSingle, List<Patient> patients){ HashMap<String, HashMap<String, Integer>> sdeAccumulators, List<Measure.MeasureSupplementalDataComponent> sde,
boolean isSingle, List<Patient> patients) {
List<Reference> newRefList = new ArrayList<>(); List<Reference> newRefList = new ArrayList<>();
sdeAccumulators.forEach((sdeKey, sdeAccumulator) -> { sdeAccumulators.forEach((sdeKey, sdeAccumulator) -> {
sdeAccumulator.forEach((sdeAccumulatorKey, sdeAccumulatorValue) -> { sdeAccumulator.forEach((sdeAccumulatorKey, sdeAccumulatorValue) -> {
@ -671,16 +659,13 @@ public class MeasureEvaluation {
} }
CodeableConcept obsCodeableConcept = new CodeableConcept(); CodeableConcept obsCodeableConcept = new CodeableConcept();
Extension obsExtension = new Extension().setUrl("http://hl7.org/fhir/StructureDefinition/cqf-measureInfo"); Extension obsExtension = new Extension().setUrl("http://hl7.org/fhir/StructureDefinition/cqf-measureInfo");
Extension extExtMeasure = new Extension() Extension extExtMeasure = new Extension().setUrl("measure")
.setUrl("measure")
.setValue(new CanonicalType("http://hl7.org/fhir/us/cqfmeasures/" + report.getMeasure())); .setValue(new CanonicalType("http://hl7.org/fhir/us/cqfmeasures/" + report.getMeasure()));
obsExtension.addExtension(extExtMeasure); obsExtension.addExtension(extExtMeasure);
Extension extExtPop = new Extension() Extension extExtPop = new Extension().setUrl("populationId").setValue(new StringType(sdeKey));
.setUrl("populationId")
.setValue(new StringType(sdeKey));
obsExtension.addExtension(extExtPop); obsExtension.addExtension(extExtPop);
obs.addExtension(obsExtension); obs.addExtension(obsExtension);
obs.setValue(new IntegerType(sdeAccumulatorValue)); obs.setValue(new Quantity(sdeAccumulatorValue));
if (!isSingle) { if (!isSingle) {
valueCoding.setCode(sdeAccumulatorKey); valueCoding.setCode(sdeAccumulatorKey);
obsCodeableConcept.setCoding(Collections.singletonList(valueCoding)); obsCodeableConcept.setCoding(Collections.singletonList(valueCoding));
@ -714,8 +699,8 @@ public class MeasureEvaluation {
for (Object o : context.getEvaluatedResources()) { for (Object o : context.getEvaluatedResources()) {
if (o instanceof Resource) { if (o instanceof Resource) {
Resource r = (Resource) o; Resource r = (Resource) o;
String id = (r.getIdElement().getResourceType() != null ? (r.getIdElement().getResourceType() + "/") String id = (r.getIdElement().getResourceType() != null ? (r.getIdElement().getResourceType() + "/") : "")
: "") + r.getIdElement().getIdPart(); + r.getIdElement().getIdPart();
if (!codeHashSet.contains(id)) { if (!codeHashSet.contains(id)) {
codeHashSet.add(id); codeHashSet.add(id);
} }

View File

@ -20,10 +20,11 @@ package ca.uhn.fhir.cql.r4.helper;
* #L% * #L%
*/ */
import ca.uhn.fhir.cql.common.evaluation.LibraryLoader;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider; import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.cql.common.provider.LibrarySourceProvider;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.cql.common.provider.LibraryContentProvider;
import org.cqframework.cql.cql2elm.CqlTranslatorOptions;
import org.cqframework.cql.cql2elm.LibraryManager; import org.cqframework.cql.cql2elm.LibraryManager;
import org.cqframework.cql.cql2elm.ModelManager; import org.cqframework.cql.cql2elm.ModelManager;
import org.cqframework.cql.cql2elm.model.Model; import org.cqframework.cql.cql2elm.model.Model;
@ -33,14 +34,16 @@ import org.hl7.fhir.r4.model.Attachment;
import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Measure; import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.PlanDefinition;
import org.hl7.fhir.r4.model.RelatedArtifact; import org.hl7.fhir.r4.model.RelatedArtifact;
import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.Resource;
import org.opencds.cqf.cql.evaluator.cql2elm.model.CacheAwareModelManager; import org.opencds.cqf.cql.evaluator.cql2elm.model.CacheAwareModelManager;
import org.opencds.cqf.cql.evaluator.engine.execution.PrivateCachingLibraryLoaderDecorator;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.opencds.cqf.cql.evaluator.engine.execution.CacheAwareLibraryLoaderDecorator;
import org.opencds.cqf.cql.evaluator.engine.execution.TranslatingLibraryLoader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -51,34 +54,43 @@ public class LibraryHelper {
private static final Logger ourLog = LoggerFactory.getLogger(LibraryHelper.class); private static final Logger ourLog = LoggerFactory.getLogger(LibraryHelper.class);
private final Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache; private final Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache;
private Map<VersionedIdentifier, Library> libraryCache;
private CqlTranslatorOptions translatorOptions;
public LibraryHelper(Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache) { public LibraryHelper(Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache,
Map<VersionedIdentifier, Library> libraryCache, CqlTranslatorOptions translatorOptions) {
this.modelCache = modelCache; this.modelCache = modelCache;
this.libraryCache = libraryCache;
this.translatorOptions = translatorOptions;
} }
public org.opencds.cqf.cql.engine.execution.LibraryLoader createLibraryLoader(LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> provider) { public org.opencds.cqf.cql.engine.execution.LibraryLoader createLibraryLoader(
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> provider) {
ModelManager modelManager = new CacheAwareModelManager(this.modelCache); ModelManager modelManager = new CacheAwareModelManager(this.modelCache);
LibraryManager libraryManager = new LibraryManager(modelManager); LibraryManager libraryManager = new LibraryManager(modelManager);
libraryManager.getLibrarySourceLoader().clearProviders(); libraryManager.getLibrarySourceLoader().clearProviders();
List<org.opencds.cqf.cql.evaluator.cql2elm.content.LibraryContentProvider> contentProviders = Collections
libraryManager.getLibrarySourceLoader().registerProvider( .singletonList(new LibraryContentProvider<org.hl7.fhir.r4.model.Library, Attachment>(provider,
new LibrarySourceProvider<org.hl7.fhir.r4.model.Library, Attachment>(provider,
x -> x.getContent(), x -> x.getContentType(), x -> x.getData())); x -> x.getContent(), x -> x.getContentType(), x -> x.getData()));
return new PrivateCachingLibraryLoaderDecorator(new LibraryLoader(libraryManager, modelManager)); return new CacheAwareLibraryLoaderDecorator(
new TranslatingLibraryLoader(modelManager, contentProviders, translatorOptions), libraryCache);
} }
public org.opencds.cqf.cql.engine.execution.LibraryLoader createLibraryLoader(org.cqframework.cql.cql2elm.LibrarySourceProvider provider) { public org.opencds.cqf.cql.engine.execution.LibraryLoader createLibraryLoader(
org.cqframework.cql.cql2elm.LibrarySourceProvider provider) {
ModelManager modelManager = new CacheAwareModelManager(this.modelCache); ModelManager modelManager = new CacheAwareModelManager(this.modelCache);
LibraryManager libraryManager = new LibraryManager(modelManager); LibraryManager libraryManager = new LibraryManager(modelManager);
libraryManager.getLibrarySourceLoader().clearProviders(); libraryManager.getLibrarySourceLoader().clearProviders();
libraryManager.getLibrarySourceLoader().registerProvider(provider); libraryManager.getLibrarySourceLoader().registerProvider(provider);
return new PrivateCachingLibraryLoaderDecorator(new LibraryLoader(libraryManager, modelManager)); return new CacheAwareLibraryLoaderDecorator(new TranslatingLibraryLoader(modelManager, null, translatorOptions),
libraryCache);
} }
public org.hl7.fhir.r4.model.Library resolveLibraryReference(LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider, String reference) { public org.hl7.fhir.r4.model.Library resolveLibraryReference(
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider, String reference) {
// Raw references to Library/libraryId or libraryId // Raw references to Library/libraryId or libraryId
if (reference.startsWith("Library/") || !reference.contains("/")) { if (reference.startsWith("Library/") || !reference.contains("/")) {
return libraryResourceProvider.resolveLibraryById(reference.replace("Library/", "")); return libraryResourceProvider.resolveLibraryById(reference.replace("Library/", ""));
@ -96,19 +108,10 @@ public class LibraryHelper {
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider) { LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider) {
List<org.cqframework.cql.elm.execution.Library> libraries = new ArrayList<org.cqframework.cql.elm.execution.Library>(); List<org.cqframework.cql.elm.execution.Library> libraries = new ArrayList<org.cqframework.cql.elm.execution.Library>();
List<String> messages = new ArrayList<>();
// load libraries // load libraries
// TODO: if there's a bad measure argument, this blows up for an obscure error // TODO: if there's a bad measure argument, this blows up for an obscure error
org.hl7.fhir.r4.model.Library primaryLibrary = null; org.hl7.fhir.r4.model.Library primaryLibrary = null;
for (CanonicalType ref : measure.getLibrary()) {
List<CanonicalType> measureLibraries = measure.getLibrary();
if (measureLibraries.isEmpty()) {
String message = "No libraries found on " + measure.getId() + ". Did you perhaps load a DSTU3 Measure onto an R4 server?";
messages.add(message);
ourLog.warn(message);
}
for (CanonicalType ref : measureLibraries) {
// if library is contained in measure, load it into server // if library is contained in measure, load it into server
String id = ref.getValue(); // CanonicalHelper.getId(ref); String id = ref.getValue(); // CanonicalHelper.getId(ref);
if (id.startsWith("#")) { if (id.startsWith("#")) {
@ -127,34 +130,30 @@ public class LibraryHelper {
primaryLibrary = library; primaryLibrary = library;
} }
if (library != null && isLogicLibrary(library)) {
if (library != null) { libraries.add(libraryLoader
if (isLogicLibrary(library)) { .load(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion())));
libraries.add(
libraryLoader.load(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion()))
);
} else {
String message = "Skipping library " + library.getId() + " is not a logic library. Probably missing type.coding.system=\"http://terminology.hl7.org/CodeSystem/library-type\"";
messages.add(message);
ourLog.warn(message);
}
} }
} }
if (libraries.isEmpty()) { if (libraries.isEmpty()) {
throw new IllegalArgumentException(String throw new IllegalArgumentException(
.format("Could not load library source for libraries referenced in %s:\n%s", measure.getId(), StringUtils.join("\n", messages))); String.format("Could not load library source for libraries referenced in %s.", measure.getId()));
} }
for (RelatedArtifact artifact : primaryLibrary.getRelatedArtifact()) { for (RelatedArtifact artifact : primaryLibrary.getRelatedArtifact()) {
if (artifact.hasType() && artifact.getType().equals(RelatedArtifact.RelatedArtifactType.DEPENDSON) && artifact.hasResource()) { if (artifact.hasType() && artifact.getType().equals(RelatedArtifact.RelatedArtifactType.DEPENDSON)
&& artifact.hasResource()) {
org.hl7.fhir.r4.model.Library library = null; org.hl7.fhir.r4.model.Library library = null;
library = resolveLibraryReference(libraryResourceProvider, artifact.getResource()); library = resolveLibraryReference(libraryResourceProvider, artifact.getResource());
if (library != null && isLogicLibrary(library)) { if (library != null) {
libraries.add( if (isLogicLibrary(library)) {
libraryLoader.load(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion())) libraries.add(libraryLoader
); .load(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion())));
} else {
ourLog.warn("Library {} not included as part of evaluation context. Only Libraries with the 'logic-library' type are included.", library.getId());
}
} }
} }
} }
@ -168,11 +167,12 @@ public class LibraryHelper {
} }
if (!library.hasType()) { if (!library.hasType()) {
// If no type is specified, assume it is a logic library based on whether there is a CQL content element. // If no type is specified, assume it is a logic library based on whether there
// is a CQL content element.
if (library.hasContent()) { if (library.hasContent()) {
for (Attachment a : library.getContent()) { for (Attachment a : library.getContent()) {
if (a.hasContentType() && (a.getContentType().equals("text/cql") if (a.hasContentType()
|| a.getContentType().equals("application/elm+xml") && (a.getContentType().equals("text/cql") || a.getContentType().equals("application/elm+xml")
|| a.getContentType().equals("application/elm+json"))) { || a.getContentType().equals("application/elm+json"))) {
return true; return true;
} }
@ -186,8 +186,8 @@ public class LibraryHelper {
} }
for (Coding c : library.getType().getCoding()) { for (Coding c : library.getType().getCoding()) {
if (c.hasSystem() && c.getSystem().equals("http://terminology.hl7.org/CodeSystem/library-type") if (c.hasSystem() && c.getSystem().equals("http://terminology.hl7.org/CodeSystem/library-type") && c.hasCode()
&& c.hasCode() && c.getCode().equals("logic-library")) { && c.getCode().equals("logic-library")) {
return true; return true;
} }
} }
@ -195,10 +195,8 @@ public class LibraryHelper {
return false; return false;
} }
public Library resolveLibraryById(String libraryId, public Library resolveLibraryById(String libraryId, org.opencds.cqf.cql.engine.execution.LibraryLoader libraryLoader,
org.opencds.cqf.cql.engine.execution.LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider) { LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider) {
// Library library = null;
org.hl7.fhir.r4.model.Library fhirLibrary = libraryResourceProvider.resolveLibraryById(libraryId); org.hl7.fhir.r4.model.Library fhirLibrary = libraryResourceProvider.resolveLibraryById(libraryId);
return libraryLoader return libraryLoader
@ -214,8 +212,23 @@ public class LibraryHelper {
Library library = resolveLibraryById(id, libraryLoader, libraryResourceProvider); Library library = resolveLibraryById(id, libraryLoader, libraryResourceProvider);
if (library == null) { if (library == null) {
throw new IllegalArgumentException(String.format("Could not resolve primary library for Measure/%s.", throw new IllegalArgumentException(
measure.getIdElement().getIdPart())); String.format("Could not resolve primary library for Measure/%s.", measure.getIdElement().getIdPart()));
}
return library;
}
public Library resolvePrimaryLibrary(PlanDefinition planDefinition,
org.opencds.cqf.cql.engine.execution.LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider) {
String id = CanonicalHelper.getId(planDefinition.getLibrary().get(0));
Library library = resolveLibraryById(id, libraryLoader, libraryResourceProvider);
if (library == null) {
throw new IllegalArgumentException(String.format("Could not resolve primary library for PlanDefinition/%s",
planDefinition.getIdElement().getIdPart()));
} }
return library; return library;

View File

@ -0,0 +1,72 @@
package ca.uhn.fhir.cql.r4.listener;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.cqframework.cql.elm.execution.Library;
import org.cqframework.cql.elm.execution.VersionedIdentifier;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.cache.IResourceChangeEvent;
import ca.uhn.fhir.jpa.cache.IResourceChangeListener;
public class ElmCacheResourceChangeListener implements IResourceChangeListener {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ElmCacheResourceChangeListener.class);
private IFhirResourceDao<org.hl7.fhir.r4.model.Library> libraryDao;
private Map<VersionedIdentifier, Library> globalLibraryCache;
public ElmCacheResourceChangeListener(IFhirResourceDao<org.hl7.fhir.r4.model.Library> libraryDao, Map<VersionedIdentifier, Library> globalLibraryCache) {
this.libraryDao = libraryDao;
this.globalLibraryCache = globalLibraryCache;
}
@Override
public void handleInit(Collection<IIdType> theResourceIds) {
// Intentionally empty. Only cache ELM on eval request
}
@Override
public void handleChange(IResourceChangeEvent theResourceChangeEvent) {
if (theResourceChangeEvent == null) {
return;
}
this.invalidateCacheByIds(theResourceChangeEvent.getDeletedResourceIds());
this.invalidateCacheByIds(theResourceChangeEvent.getUpdatedResourceIds());
}
private void invalidateCacheByIds(List<IIdType> theIds) {
if (theIds == null) {
return;
}
for (IIdType id : theIds) {
this.invalidateCacheById(id);
}
}
private void invalidateCacheById(IIdType theId) {
if (!theId.getResourceType().equals("Library")) {
return;
}
try {
org.hl7.fhir.r4.model.Library library = this.libraryDao.read(theId);
this.globalLibraryCache.remove(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion()));
}
// This happens when a Library is deleted entirely so it's impossible to look up name and version.
catch (Exception e) {
// TODO: This needs to be smarter... the issue is that ELM is cached with library name and version as the key since
// that's the access path the CQL engine uses, but change notifications occur with the resource Id, which is not
// necessarily tied to the resource name. In any event, if a unknown resource is deleted, clear all libraries as a workaround.
// One option is to maintain a cache with multiple indices.
ourLog.debug("Failed to locate resource {} to look up name and version. Clearing all libraries from cache.", theId.getValueAsString());
this.globalLibraryCache.clear();
}
}
}

View File

@ -1,15 +1,23 @@
package ca.uhn.fhir.cql.config; package ca.uhn.fhir.cql.config;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.subscription.channel.config.SubscriptionChannelConfig; import ca.uhn.fhir.jpa.subscription.channel.config.SubscriptionChannelConfig;
import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig; import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
@Configuration @Configuration
@Import({SubscriptionSubmitterConfig.class, SubscriptionChannelConfig.class}) @Import({SubscriptionSubmitterConfig.class, SubscriptionChannelConfig.class})
public class TestCqlConfig { public class TestCqlConfig {
@Bean
public DaoConfig daoConfig() {
DaoConfig daoConfig = new DaoConfig();
daoConfig.setAllowExternalReferences(true);
daoConfig.setEnforceReferentialIntegrityOnWrite(false);
daoConfig.setEnforceReferenceTargetTypes(false);
return daoConfig;
}
} }

View File

@ -75,7 +75,7 @@ public class CqlMeasureEvaluationDstu3Test extends BaseCqlDstu3Test {
assertNotNull("expected MeasureReport can not be null", expected); assertNotNull("expected MeasureReport can not be null", expected);
assertNotNull("actual MeasureReport can not be null", actual); assertNotNull("actual MeasureReport can not be null", actual);
String errorLocator = String.format("Measure: %s, Subject: %s", expected.getMeasure(), String errorLocator = String.format("Measure: %s, Subject: %s", expected.getMeasure().getReference(),
expected.getPatient().getReference()); expected.getPatient().getReference());
assertEquals(expected.hasGroup(), actual.hasGroup(), errorLocator); assertEquals(expected.hasGroup(), actual.hasGroup(), errorLocator);
@ -83,10 +83,10 @@ public class CqlMeasureEvaluationDstu3Test extends BaseCqlDstu3Test {
for (MeasureReportGroupComponent mrgcExpected : expected.getGroup()) { for (MeasureReportGroupComponent mrgcExpected : expected.getGroup()) {
Optional<MeasureReportGroupComponent> mrgcActualOptional = actual.getGroup().stream() Optional<MeasureReportGroupComponent> mrgcActualOptional = actual.getGroup().stream()
.filter(x -> x.getId().equals(mrgcExpected.getId())).findFirst(); .filter(x -> x.getIdentifier() != null && x.getIdentifier().getValue().equals(mrgcExpected.getIdentifier().getValue())).findFirst();
errorLocator = String.format("Measure: %s, Subject: %s, Group: %s", expected.getMeasure(), errorLocator = String.format("Measure: %s, Subject: %s, Group: %s", expected.getMeasure().getReference(),
expected.getPatient().getReference(), mrgcExpected.getId()); expected.getPatient().getReference(), mrgcExpected.getIdentifier().getValue());
assertTrue(errorLocator, mrgcActualOptional.isPresent()); assertTrue(errorLocator, mrgcActualOptional.isPresent());
MeasureReportGroupComponent mrgcActual = mrgcActualOptional.get(); MeasureReportGroupComponent mrgcActual = mrgcActualOptional.get();
@ -94,7 +94,7 @@ public class CqlMeasureEvaluationDstu3Test extends BaseCqlDstu3Test {
if (mrgcExpected.getMeasureScore() == null) { if (mrgcExpected.getMeasureScore() == null) {
assertNull(mrgcActual.getMeasureScore(), errorLocator); assertNull(mrgcActual.getMeasureScore(), errorLocator);
} else { } else {
assertNotNull(mrgcActual.getMeasureScore()); assertNotNull(errorLocator, mrgcActual.getMeasureScore());
BigDecimal decimalExpected = mrgcExpected.getMeasureScore(); BigDecimal decimalExpected = mrgcExpected.getMeasureScore();
BigDecimal decimalActual = mrgcActual.getMeasureScore(); BigDecimal decimalActual = mrgcActual.getMeasureScore();
@ -135,10 +135,13 @@ public class CqlMeasureEvaluationDstu3Test extends BaseCqlDstu3Test {
return new DateTimeType(date).getValueAsString(); return new DateTimeType(date).getValueAsString();
} }
// As of 2/11/2021, all the DSTU3 bundles in the Connectathon IG are out of date @Test
// and can't be posted public void test_EXM124_FHIR3_72000() throws IOException {
// @Test this.testMeasureBundle("dstu3/connectathon/EXM124-FHIR3-7.2.000-bundle.json");
// public void test_EXM117_83000() throws IOException { }
// this.testMeasureBundle("dstu3/connectathon/EXM117_FHIR3-8.3.000-bundle.json");
// } @Test
public void test_EXM104_FHIR3_81000() throws IOException {
this.testMeasureBundle("dstu3/connectathon/EXM104-FHIR3-8.1.000-bundle.json");
}
} }

View File

@ -83,7 +83,7 @@ public class CqlMeasureEvaluationR4Test extends BaseCqlR4Test {
for (MeasureReportGroupComponent mrgcExpected : expected.getGroup()) { for (MeasureReportGroupComponent mrgcExpected : expected.getGroup()) {
Optional<MeasureReportGroupComponent> mrgcActualOptional = actual.getGroup().stream() Optional<MeasureReportGroupComponent> mrgcActualOptional = actual.getGroup().stream()
.filter(x -> x.getId().equals(mrgcExpected.getId())).findFirst(); .filter(x -> x.getId() != null && x.getId().equals(mrgcExpected.getId())).findFirst();
errorLocator = String.format("Measure: %s, Subject: %s, Group: %s", expected.getMeasure(), errorLocator = String.format("Measure: %s, Subject: %s, Group: %s", expected.getMeasure(),
expected.getSubject().getReference(), mrgcExpected.getId()); expected.getSubject().getReference(), mrgcExpected.getId());

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -829,8 +829,8 @@
<elastic_apm_version>1.13.0</elastic_apm_version> <elastic_apm_version>1.13.0</elastic_apm_version>
<!-- CQL Support --> <!-- CQL Support -->
<cql-engine.version>1.5.1</cql-engine.version> <cql-engine.version>1.5.1</cql-engine.version>
<cql-evaluator.version>1.1.0</cql-evaluator.version> <cql-evaluator.version>1.2.0</cql-evaluator.version>
<cqframework.version>1.5.1</cqframework.version> <cqframework.version>1.5.2</cqframework.version>
<!-- Site properties --> <!-- Site properties -->
<fontawesomeVersion>5.4.1</fontawesomeVersion> <fontawesomeVersion>5.4.1</fontawesomeVersion>