Merge CQL Initial Implementation Into master (#2330)

* CQL initial implementation
This commit is contained in:
Kevin Dougan SmileCDR 2021-01-29 16:35:38 -05:00 committed by GitHub
parent 5961fde2dd
commit 2308d8f44b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 47083 additions and 17 deletions

9
.gitignore vendored
View File

@ -153,3 +153,12 @@ local.properties
# TeXlipse plugin
.texlipse
# JVM Dumps
core.*
javacore.*
jitdump.*
Snap.*
# VS Code
.vscode

View File

@ -78,7 +78,11 @@
<artifactId>commons-codec</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-android</artifactId>

View File

@ -30,8 +30,11 @@ import org.hl7.fhir.instance.model.api.IBase;
import ca.uhn.fhir.model.api.annotation.Child;
import ca.uhn.fhir.model.api.annotation.Description;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class BaseRuntimeChildDatatypeDefinition extends BaseRuntimeDeclaredChildDefinition {
Logger ourLog = LoggerFactory.getLogger(BaseRuntimeChildDatatypeDefinition.class);
private Class<? extends IBase> myDatatype;

130
hapi-fhir-jpaserver-cql/.gitignore vendored Normal file
View File

@ -0,0 +1,130 @@
ca.*/
target/
/bin
nohup.out
LogMessages.html
# Created by https://www.gitignore.io
### Java ###
*.class
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.ear
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### Maven ###
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
### Vim ###
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
*~
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
*.iml
## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:
# User-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle:
# .idea/gradle.xml
# .idea/libraries
# Mongo Explorer plugin:
# .idea/mongoSettings.xml
## File-based project format:
*.ipr
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
### Eclipse ###
*.pydevproject
.metadata
.gradle
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.loadpath
# Eclipse Core
.project
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# CDT-specific
.cproject
# JDT-specific (Eclipse Java Development Tools)
# PDT-specific
.buildpath
# sbteclipse plugin
.target
# TeXlipse plugin
.texlipse
/target/

View File

@ -0,0 +1,201 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>5.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<artifactId>hapi-fhir-jpaserver-cql</artifactId>
<packaging>jar</packaging>
<name>HAPI FHIR JPA Server - Clinical Quality Language</name>
<dependencies>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>org.hl7.fhir.dstu3</artifactId>
<version>${fhir_core_version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.opencds.cqf.cql</groupId>
<artifactId>evaluator.engine</artifactId>
<version>${cql-evaluator.version}</version>
</dependency>
<dependency>
<groupId>org.opencds.cqf</groupId>
<artifactId>tooling</artifactId>
<version>${cqf-tooling.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>javax.activation</groupId>
<artifactId>javax.activation-api</artifactId>
</exclusion>
<exclusion>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</exclusion>
<exclusion>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</exclusion>
<exclusion>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.istack</groupId>
<artifactId>istack-commons-tools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.istack</groupId>
<artifactId>istack-commons-runtime</artifactId>
</exclusion>
<exclusion>
<groupId>info.bliki.wiki</groupId>
<artifactId>bliki-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.opencds.cqf.cql</groupId>
<artifactId>engine</artifactId>
<version>${cql-engine.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.opencds.cqf.cql</groupId>
<artifactId>engine.fhir</artifactId>
<version>${cql-engine.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>info.cqframework</groupId>
<artifactId>cql-to-elm</artifactId>
<version>${cqframework.version}</version>
<exclusions>
<exclusion>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</exclusion>
<exclusion>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>${jaxb_api_version}</version>
</dependency>
<dependency>
<groupId>com.jamesmurty.utils</groupId>
<artifactId>java-xmlbuilder</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-base</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu3</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r4</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r5</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-base</artifactId>
<version>${project.version}</version>
<exclusions>
<exclusion>
<groupId>com.sun.activation</groupId>
<artifactId>javax.activation</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-all</artifactId>
<version>0.50.26</version>
</dependency>
<!-- test -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-test-utilities</artifactId>
<version>5.3.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-test-utilities</artifactId>
<version>5.3.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.basepom.maven</groupId>
<artifactId>duplicate-finder-maven-plugin</artifactId>
<configuration>
<failBuildInCaseOfDifferentContentConflict>false</failBuildInCaseOfDifferentContentConflict>
<failBuildInCaseOfEqualContentConflict>false</failBuildInCaseOfEqualContentConflict>
<failBuildInCaseOfConflict>false</failBuildInCaseOfConflict>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,41 @@
package ca.uhn.fhir.cql.common.builder;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
/*
These builders are based off of work performed by Philips Healthcare.
I simplified their work with this generic base class and added/expanded builders.
Tip of the hat to Philips Healthcare developer nly98977
*/
public class BaseBuilder<T> {
protected T complexProperty;
public BaseBuilder(T complexProperty) {
this.complexProperty = complexProperty;
}
public T build() {
return complexProperty;
}
}

View File

@ -0,0 +1,122 @@
package ca.uhn.fhir.cql.common.evaluation;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.apache.commons.lang3.Validate;
import org.cqframework.cql.cql2elm.CqlTranslator;
import org.cqframework.cql.cql2elm.CqlTranslatorException;
import org.cqframework.cql.cql2elm.CqlTranslatorOptions;
import org.cqframework.cql.cql2elm.LibraryManager;
import org.cqframework.cql.cql2elm.ModelManager;
import org.cqframework.cql.elm.execution.Library;
import org.cqframework.cql.elm.execution.VersionedIdentifier;
import javax.xml.bind.JAXBException;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import static ca.uhn.fhir.cql.common.helper.TranslatorHelper.errorsToString;
import static ca.uhn.fhir.cql.common.helper.TranslatorHelper.getTranslator;
import static ca.uhn.fhir.cql.common.helper.TranslatorHelper.readLibrary;
public class LibraryLoader implements org.opencds.cqf.cql.engine.execution.LibraryLoader {
private LibraryManager libraryManager;
private ModelManager modelManager;
private Map<String, Library> libraries = new HashMap<>();
// private static final Logger logger =
// LoggerFactory.getLogger(LibraryLoader.class);
public Collection<Library> getLibraries() {
return this.libraries.values();
}
public LibraryManager getLibraryManager() {
return this.libraryManager;
}
public ModelManager getModelManager() {
return this.modelManager;
}
public LibraryLoader(LibraryManager libraryManager, ModelManager modelManager) {
this.libraryManager = libraryManager;
this.modelManager = modelManager;
}
private Library resolveLibrary(VersionedIdentifier libraryIdentifier) {
Validate.notNull(libraryIdentifier, "Library identifier is null.");
Validate.notNull(libraryIdentifier.getId(), "Library identifier id is null.");
String mangledId = this.mangleIdentifer(libraryIdentifier);
Library library = libraries.get(mangledId);
if (library == null) {
library = loadLibrary(libraryIdentifier);
libraries.put(mangledId, library);
}
return library;
}
private String mangleIdentifer(VersionedIdentifier libraryIdentifier) {
String id = libraryIdentifier.getId();
String version = libraryIdentifier.getVersion();
return version == null ? id : id + "-" + version;
}
private Library loadLibrary(VersionedIdentifier libraryIdentifier) {
org.hl7.elm.r1.VersionedIdentifier identifier = new org.hl7.elm.r1.VersionedIdentifier()
.withId(libraryIdentifier.getId()).withSystem(libraryIdentifier.getSystem())
.withVersion(libraryIdentifier.getVersion());
ArrayList<CqlTranslatorException> errors = new ArrayList<>();
org.hl7.elm.r1.Library translatedLibrary = libraryManager.resolveLibrary(identifier, CqlTranslatorOptions.defaultOptions(), errors).getLibrary();
if (CqlTranslatorException.HasErrors(errors)) {
throw new IllegalArgumentException(errorsToString(errors));
}
try {
CqlTranslator translator = getTranslator("", libraryManager, modelManager);
if (translator.getErrors().size() > 0) {
throw new IllegalArgumentException(errorsToString(translator.getErrors()));
}
return readLibrary(new ByteArrayInputStream(
translator.convertToXml(translatedLibrary).getBytes(StandardCharsets.UTF_8)));
} catch (JAXBException e) {
throw new IllegalArgumentException(String.format("Errors occurred translating library %s%s.",
identifier.getId(), identifier.getVersion() != null ? ("-" + identifier.getVersion()) : ""));
}
}
@Override
public Library load(VersionedIdentifier versionedIdentifier) {
return resolveLibrary(versionedIdentifier);
}
}

View File

@ -0,0 +1,103 @@
package ca.uhn.fhir.cql.common.evaluation;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.util.HashMap;
import java.util.Map;
public enum MeasurePopulationType {
INITIALPOPULATION("initial-population", "Initial Population",
"The initial population refers to all patients or events to be evaluated by a quality measure involving patients who share a common set of specified characterstics. All patients or events counted (for example, as numerator, as denominator) are drawn from the initial population"),
NUMERATOR("numerator", "Numerator",
"\tThe upper portion of a fraction used to calculate a rate, proportion, or ratio. Also called the measure focus, it is the target process, condition, event, or outcome. Numerator criteria are the processes or outcomes expected for each patient, or event defined in the denominator. A numerator statement describes the clinical action that satisfies the conditions of the measure"),
NUMERATOREXCLUSION("numerator-exclusion", "Numerator Exclusion",
"Numerator exclusion criteria define patients or events to be removed from the numerator. Numerator exclusions are used in proportion and ratio measures to help narrow the numerator (for inverted measures)"),
DENOMINATOR("denominator", "Denominator",
"The lower portion of a fraction used to calculate a rate, proportion, or ratio. The denominator can be the same as the initial population, or a subset of the initial population to further constrain the population for the purpose of the measure"),
DENOMINATOREXCLUSION("denominator-exclusion", "Denominator Exclusion",
"Denominator exclusion criteria define patients or events that should be removed from the denominator before determining if numerator criteria are met. Denominator exclusions are used in proportion and ratio measures to help narrow the denominator. For example, patients with bilateral lower extremity amputations would be listed as a denominator exclusion for a measure requiring foot exams"),
DENOMINATOREXCEPTION("denominator-exception", "Denominator Exception",
"Denominator exceptions are conditions that should remove a patient or event from the denominator of a measure only if the numerator criteria are not met. Denominator exception allows for adjustment of the calculated score for those providers with higher risk populations. Denominator exception criteria are only used in proportion measures"),
MEASUREPOPULATION("measure-population", "Measure Population",
"Measure population criteria define the patients or events for which the individual observation for the measure should be taken. Measure populations are used for continuous variable measures rather than numerator and denominator criteria"),
MEASUREPOPULATIONEXCLUSION("measure-population-exclusion", "Measure Population Exclusion",
"Measure population criteria define the patients or events that should be removed from the measure population before determining the outcome of one or more continuous variables defined for the measure observation. Measure population exclusion criteria are used within continuous variable measures to help narrow the measure population"),
MEASUREOBSERVATION("measure-observation", "Measure Observation",
"Defines the individual observation to be performed for each patient or event in the measure population. Measure observations for each case in the population are aggregated to determine the overall measure score for the population");
private String code;
private String display;
private String definition;
MeasurePopulationType(String code, String display, String definition) {
this.code = code;
this.display = display;
this.definition = definition;
}
private static final Map<String, MeasurePopulationType> lookup = new HashMap<>();
static {
for (MeasurePopulationType mpt : MeasurePopulationType.values()) {
lookup.put(mpt.toCode(), mpt);
}
}
// This method can be used for reverse lookup purpose
public static MeasurePopulationType fromCode(String code) {
if (code != null && !code.isEmpty()) {
if (lookup.containsKey(code)) {
return lookup.get(code);
}
// } else if (Configuration.isAcceptInvalidEnums()) {
// return null;
// } else {
// // throw new FHIRException("Unknown MeasureScoring code \'" + code + "\'");
// }
}
return null;
}
public String getSystem() {
return "http://hl7.org/fhir/measure-population";
}
public String toCode() {
return this.code;
}
public String getDisplay() {
return this.display;
}
public String getDefinition() {
return this.definition;
}
}

View File

@ -0,0 +1,84 @@
package ca.uhn.fhir.cql.common.evaluation;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.util.HashMap;
import java.util.Map;
public enum MeasureScoring {
PROPORTION("proportion", "Proportion", "The measure score is defined using a proportion"),
RATIO("ratio", "Ratio", "The measure score is defined using a ratio"),
CONTINUOUSVARIABLE("continuous-variable", "Continuous Variable", "The score is defined by a calculation of some quantity"),
COHORT("cohort", "Cohort", "The measure is a cohort definition");
private String code;
private String display;
private String definition;
MeasureScoring(String code, String display, String definition) {
this.code = code;
this.display = display;
this.definition = definition;
}
private static final Map<String, MeasureScoring> lookup = new HashMap<>();
static {
for (MeasureScoring ms : MeasureScoring.values()) {
lookup.put(ms.toCode(), ms);
}
}
public static MeasureScoring fromCode(String code) {
if (code != null && !code.isEmpty()) {
if (lookup.containsKey(code)) {
return lookup.get(code);
}
// } else if (Configuration.isAcceptInvalidEnums()) {
// return null;
// } else {
// // throw new FHIRException("Unknown MeasureScoring code \'" + code + "\'");
// }
}
return null;
}
public String toCode() {
return this.code;
}
public String getSystem() {
return "http://hl7.org/fhir/measure-scoring";
}
public String getDefinition() {
return this.definition;
}
public String getDisplay() {
return this.display;
}
}

View File

@ -0,0 +1,45 @@
package ca.uhn.fhir.cql.common.helper;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.DateTimeType;
import java.util.Date;
/**
* Helper class to resolve period dates used by {@link ca.uhn.fhir.cql.dstu3.evaluation.MeasureEvaluationSeed}
* and {@link ca.uhn.fhir.cql.r4.evaluation.MeasureEvaluationSeed}.
*/
public class DateHelper {
/**
*
* @param date A date String in the format YYYY-MM-DD.
* @return A {@link java.util.Date} object representing the String data that was passed in.
*/
public static Date resolveRequestDate(String date) {
if (StringUtils.isBlank(date)) {
throw new IllegalArgumentException("date parameter cannot be blank!");
}
return new DateTimeType(date).getValue();
}
}

View File

@ -0,0 +1,102 @@
package ca.uhn.fhir.cql.common.helper;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.cqframework.cql.cql2elm.CqlTranslator;
import org.cqframework.cql.cql2elm.CqlTranslatorException;
import org.cqframework.cql.cql2elm.LibraryManager;
import org.cqframework.cql.cql2elm.ModelManager;
import org.cqframework.cql.elm.execution.Library;
import org.cqframework.cql.elm.tracking.TrackBack;
import org.opencds.cqf.cql.engine.execution.CqlLibraryReader;
import javax.xml.bind.JAXBException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
public class TranslatorHelper {
public static Library readLibrary(InputStream xmlStream) {
try {
return CqlLibraryReader.read(xmlStream);
} catch (IOException | JAXBException e) {
throw new IllegalArgumentException("Error encountered while reading ELM xml: " + e.getMessage());
}
}
public static String errorsToString(Iterable<CqlTranslatorException> exceptions) {
ArrayList<String> errors = new ArrayList<>();
for (CqlTranslatorException error : exceptions) {
TrackBack tb = error.getLocator();
String lines = tb == null ? "[n/a]"
: String.format("%s [%d:%d, %d:%d] ",
(tb.getLibrary() != null ? tb.getLibrary().getId()
+ (tb.getLibrary().getVersion() != null ? ("-" + tb.getLibrary().getVersion()) : "")
: ""),
tb.getStartLine(), tb.getStartChar(), tb.getEndLine(), tb.getEndChar());
errors.add(lines + error.getMessage());
}
return String.join("\n", errors);
}
public static CqlTranslator getTranslator(String cql, LibraryManager libraryManager, ModelManager modelManager) {
return getTranslator(new ByteArrayInputStream(cql.getBytes(StandardCharsets.UTF_8)), libraryManager,
modelManager);
}
public static CqlTranslator getTranslator(InputStream cqlStream, LibraryManager libraryManager,
ModelManager modelManager) {
ArrayList<CqlTranslator.Options> options = new ArrayList<>();
options.add(CqlTranslator.Options.EnableAnnotations);
options.add(CqlTranslator.Options.EnableLocators);
options.add(CqlTranslator.Options.DisableListDemotion);
options.add(CqlTranslator.Options.DisableListPromotion);
options.add(CqlTranslator.Options.DisableMethodInvocation);
CqlTranslator translator;
try {
translator = CqlTranslator.fromStream(cqlStream, modelManager, libraryManager,
options.toArray(new CqlTranslator.Options[options.size()]));
} catch (IOException e) {
throw new IllegalArgumentException(
String.format("Errors occurred translating library: %s", e.getMessage()));
}
return translator;
}
public static Library translateLibrary(String cql, LibraryManager libraryManager, ModelManager modelManager) {
return translateLibrary(new ByteArrayInputStream(cql.getBytes(StandardCharsets.UTF_8)), libraryManager,
modelManager);
}
public static Library translateLibrary(InputStream cqlStream, LibraryManager libraryManager,
ModelManager modelManager) {
CqlTranslator translator = getTranslator(cqlStream, libraryManager, modelManager);
return readLibrary(new ByteArrayInputStream(translator.toXml().getBytes(StandardCharsets.UTF_8)));
}
public static Library translateLibrary(CqlTranslator translator) {
return readLibrary(new ByteArrayInputStream(translator.toXml().getBytes(StandardCharsets.UTF_8)));
}
}

View File

@ -0,0 +1,63 @@
package ca.uhn.fhir.cql.common.helper;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.apache.commons.lang3.tuple.Triple;
import org.cqframework.cql.elm.execution.Library.Usings;
import org.cqframework.cql.elm.execution.UsingDef;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class UsingHelper {
private static Map<String, String> urlsByModelName = new HashMap<String, String>() {
private static final long serialVersionUID = 1L;
{
put("FHIR", "http://hl7.org/fhir");
put("QDM", "urn:healthit-gov:qdm:v5_4");
}
};
// Returns a list of (Model, Version, Url) for the usings in library. The
// "System" using is excluded.
public static List<Triple<String, String, String>> getUsingUrlAndVersion(Usings usings) {
if (usings == null || usings.getDef() == null) {
return Collections.emptyList();
}
List<Triple<String, String, String>> usingDefs = new ArrayList<>();
for (UsingDef def : usings.getDef()) {
if (def.getLocalIdentifier().equals("System"))
continue;
usingDefs.add(Triple.of(def.getLocalIdentifier(), def.getVersion(),
urlsByModelName.get(def.getLocalIdentifier())));
}
return usingDefs;
}
}

View File

@ -0,0 +1,47 @@
package ca.uhn.fhir.cql.common.provider;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
@Service
public class CqlProviderFactory {
@Autowired
private FhirContext myFhirContext;
@Autowired
private ApplicationContext myApplicationContext;
public Object getMeasureOperationsProvider() {
switch (myFhirContext.getVersion().getVersion()) {
case DSTU3:
return myApplicationContext.getBean(ca.uhn.fhir.cql.dstu3.provider.MeasureOperationsProvider.class);
case R4:
return myApplicationContext.getBean(ca.uhn.fhir.cql.r4.provider.MeasureOperationsProvider.class);
default:
throw new ConfigurationException("CQL is not supported for FHIR version " + myFhirContext.getVersion().getVersion());
}
}
}

View File

@ -0,0 +1,55 @@
package ca.uhn.fhir.cql.common.provider;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.provider.ResourceProviderFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
@Service
public class CqlProviderLoader {
private static final Logger myLogger = LoggerFactory.getLogger(CqlProviderLoader.class);
@Autowired
private FhirContext myFhirContext;
@Autowired
private ResourceProviderFactory myResourceProviderFactory;
@Autowired
private CqlProviderFactory myCqlProviderFactory;
@PostConstruct
public void loadProvider() {
switch (myFhirContext.getVersion().getVersion()) {
case DSTU3:
case R4:
myLogger.info("Registering CQL Provider");
myResourceProviderFactory.addSupplier(() -> myCqlProviderFactory.getMeasureOperationsProvider());
break;
default:
throw new ConfigurationException("CQL not supported for FHIR version " + myFhirContext.getVersion().getVersion());
}
}
}

View File

@ -0,0 +1,37 @@
package ca.uhn.fhir.cql.common.provider;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.opencds.cqf.cql.engine.data.DataProvider;
import org.opencds.cqf.cql.engine.terminology.TerminologyProvider;
// TODO: This interface is a partial duplicate of the provider factory interface
// in the cql service layer. We need another round of refactoring to consolidate that.
public interface EvaluationProviderFactory {
public DataProvider createDataProvider(String model, String version);
public DataProvider createDataProvider(String model, String version, String url, String user, String pass);
public DataProvider createDataProvider(String model, String version, TerminologyProvider terminologyProvider);
public TerminologyProvider createTerminologyProvider(String model, String version, String url, String user,
String pass);
}

View File

@ -0,0 +1,106 @@
package ca.uhn.fhir.cql.common.provider;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import java.util.function.Function;
public interface LibraryResolutionProvider<LibraryType> {
public LibraryType resolveLibraryById(String libraryId);
public LibraryType resolveLibraryByName(String libraryName, String libraryVersion);
public LibraryType resolveLibraryByCanonicalUrl(String libraryUrl);
// Hmmm... Probably need to think through this use case a bit more.
// Should we throw an exception? Should this be a different interface?
public void update(LibraryType library);
// This function assumes that you're selecting from a set of libraries with the same name.
// It returns the closest matching version, or the max version if no version is specified.
static <LibraryType> LibraryType selectFromList(Iterable<LibraryType> libraries, String libraryVersion, Function<LibraryType, String> getVersion) {
LibraryType library = null;
LibraryType maxVersion = null;
for (LibraryType l : libraries) {
String currentVersion = getVersion.apply(l);
if ((libraryVersion != null && currentVersion.equals(libraryVersion)) ||
(libraryVersion == null && currentVersion == null))
{
library = l;
}
if (maxVersion == null || compareVersions(
getVersion.apply(maxVersion),
getVersion.apply(l) ) < 0){
maxVersion = l;
}
}
// If we were not given a version, return the highest found
if (libraryVersion == null && maxVersion != null) {
return maxVersion;
}
return library;
}
public static int compareVersions(String version1, String version2)
{
// Treat null as MAX VERSION
if (version1 == null && version2 == null) {
return 0;
}
if (version1 != null && version2 == null) {
return -1;
}
if (version1 == null && version2 != null) {
return 1;
}
String[] string1Vals = version1.split("\\.");
String[] string2Vals = version2.split("\\.");
int length = Math.max(string1Vals.length, string2Vals.length);
for (int i = 0; i < length; i++)
{
Integer v1 = (i < string1Vals.length)?Integer.parseInt(string1Vals[i]):0;
Integer v2 = (i < string2Vals.length)?Integer.parseInt(string2Vals[i]):0;
//Making sure Version1 bigger than version2
if (v1 > v2)
{
return 1;
}
//Making sure Version1 smaller than version2
else if(v1 < v2)
{
return -1;
}
}
//Both are equal
return 0;
}
}

View File

@ -0,0 +1,69 @@
package ca.uhn.fhir.cql.common.provider;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.cqframework.cql.cql2elm.FhirLibrarySourceProvider;
import org.hl7.elm.r1.VersionedIdentifier;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.function.Function;
public class LibrarySourceProvider<LibraryType, AttachmentType>
implements org.cqframework.cql.cql2elm.LibrarySourceProvider {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LibrarySourceProvider.class);
private FhirLibrarySourceProvider innerProvider;
private LibraryResolutionProvider<LibraryType> provider;
private Function<LibraryType, Iterable<AttachmentType>> getAttachments;
private Function<AttachmentType, String> getContentType;
private Function<AttachmentType, byte[]> getContent;
public LibrarySourceProvider(LibraryResolutionProvider<LibraryType> provider,
Function<LibraryType, Iterable<AttachmentType>> getAttachments,
Function<AttachmentType, String> getContentType, Function<AttachmentType, byte[]> getContent) {
this.innerProvider = new FhirLibrarySourceProvider();
this.provider = provider;
this.getAttachments = getAttachments;
this.getContentType = getContentType;
this.getContent = getContent;
}
@Override
public InputStream getLibrarySource(VersionedIdentifier versionedIdentifier) {
try {
LibraryType lib = this.provider.resolveLibraryByName(versionedIdentifier.getId(),
versionedIdentifier.getVersion());
for (AttachmentType attachment : this.getAttachments.apply(lib)) {
if ("text/cql".equals(this.getContentType.apply(attachment))) {
return new ByteArrayInputStream(this.getContent.apply(attachment));
}
}
} catch (Exception e) {
ourLog.warn("Failed to parse Library source for VersionedIdentifier '" + versionedIdentifier + "'!"
+ System.lineSeparator() + e.getMessage(), e);
}
return this.innerProvider.getLibrarySource(versionedIdentifier);
}
}

View File

@ -0,0 +1,114 @@
package ca.uhn.fhir.cql.common.retrieve;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.opencds.cqf.cql.engine.fhir.retrieve.SearchParamFhirRetrieveProvider;
import org.opencds.cqf.cql.engine.fhir.searchparam.SearchParameterMap;
import org.opencds.cqf.cql.engine.fhir.searchparam.SearchParameterResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Component
public class JpaFhirRetrieveProvider extends SearchParamFhirRetrieveProvider {
private static final Logger logger = LoggerFactory.getLogger(JpaFhirRetrieveProvider.class);
DaoRegistry registry;
@Autowired
public JpaFhirRetrieveProvider(DaoRegistry registry, SearchParameterResolver searchParameterResolver) {
super(searchParameterResolver);
this.registry = registry;
}
@Override
protected Iterable<Object> executeQueries(String dataType, List<SearchParameterMap> queries) {
if (queries == null || queries.isEmpty()) {
return Collections.emptyList();
}
List<Object> objects = new ArrayList<>();
for (SearchParameterMap map : queries) {
objects.addAll(executeQuery(dataType, map));
}
return objects;
}
protected Collection<Object> executeQuery(String dataType, SearchParameterMap map) {
// TODO: Once HAPI breaks this out from the server dependencies
// we can include it on its own.
ca.uhn.fhir.jpa.searchparam.SearchParameterMap hapiMap = new ca.uhn.fhir.jpa.searchparam.SearchParameterMap();
try {
Method[] methods = hapiMap.getClass().getDeclaredMethods();
List<Method> methodList = Arrays.asList(methods);
List<Method> puts = methodList.stream().filter(x -> x.getName().equals("put")).collect(Collectors.toList());
Method method = puts.get(0);
method.setAccessible(true);
for (Map.Entry<String, List<List<IQueryParameterType>>> entry : map.entrySet()) {
method.invoke(hapiMap, entry.getKey(), entry.getValue());
}
} catch (Exception e) {
logger.warn("Error converting search parameter map", e);
}
IFhirResourceDao<?> dao = this.registry.getResourceDao(dataType);
IBundleProvider bundleProvider = dao.search(hapiMap);
if (bundleProvider.size() == null) {
return resolveResourceList(bundleProvider.getResources(0, 10000));
}
if (bundleProvider.size() == 0) {
return new ArrayList<>();
}
List<IBaseResource> resourceList = bundleProvider.getResources(0, bundleProvider.size());
return resolveResourceList(resourceList);
}
public synchronized Collection<Object> resolveResourceList(List<IBaseResource> resourceList) {
List<Object> ret = new ArrayList<>();
for (IBaseResource res : resourceList) {
Class<?> clazz = res.getClass();
ret.add(clazz.cast(res));
}
// ret.addAll(resourceList);
return ret;
}
}

View File

@ -0,0 +1,49 @@
package ca.uhn.fhir.cql.config;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.cql.common.provider.CqlProviderFactory;
import ca.uhn.fhir.cql.common.provider.CqlProviderLoader;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.cqframework.cql.cql2elm.model.Model;
import org.hl7.elm.r1.VersionedIdentifier;
import org.springframework.context.annotation.Bean;
public abstract class BaseCqlConfig {
@Bean
CqlProviderFactory cqlProviderFactory() {
return new CqlProviderFactory();
}
@Bean
CqlProviderLoader cqlProviderLoader() {
return new CqlProviderLoader();
}
@Bean(name="globalModelCache")
Map<VersionedIdentifier, Model> globalModelCache() {
return new ConcurrentHashMap<VersionedIdentifier, Model>();
}
}

View File

@ -0,0 +1,85 @@
package ca.uhn.fhir.cql.config;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.cql.common.provider.EvaluationProviderFactory;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.cql.dstu3.evaluation.ProviderFactory;
import ca.uhn.fhir.cql.dstu3.helper.LibraryHelper;
import ca.uhn.fhir.cql.dstu3.provider.JpaTerminologyProvider;
import ca.uhn.fhir.cql.dstu3.provider.LibraryResolutionProviderImpl;
import ca.uhn.fhir.cql.dstu3.provider.MeasureOperationsProvider;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.rp.dstu3.ValueSetResourceProvider;
import ca.uhn.fhir.jpa.term.api.ITermReadSvcDstu3;
import org.opencds.cqf.cql.engine.model.ModelResolver;
import java.util.Map;
import org.cqframework.cql.cql2elm.model.Model;
import org.hl7.elm.r1.VersionedIdentifier;
import org.opencds.cqf.cql.engine.fhir.model.Dstu3FhirModelResolver;
import org.opencds.cqf.cql.engine.terminology.TerminologyProvider;
import org.opencds.cqf.cql.evaluator.engine.model.CachingModelResolverDecorator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@Configuration
public class CqlDstu3Config extends BaseCqlConfig {
@Lazy
@Bean
TerminologyProvider terminologyProvider(ITermReadSvcDstu3 theITermReadSvc,
ValueSetResourceProvider theValueSetResourceProvider, IValidationSupport theValidationSupport) {
return new JpaTerminologyProvider(theITermReadSvc, theValueSetResourceProvider, theValidationSupport);
}
@Lazy
@Bean
EvaluationProviderFactory evaluationProviderFactory(FhirContext theFhirContext, DaoRegistry theDaoRegistry, TerminologyProvider theLocalSystemTerminologyProvider, ModelResolver modelResolver) {
return new ProviderFactory(theFhirContext, theDaoRegistry, theLocalSystemTerminologyProvider, modelResolver);
}
@Lazy
@Bean
LibraryResolutionProvider libraryResolutionProvider() {
return new LibraryResolutionProviderImpl();
}
@Lazy
@Bean
public MeasureOperationsProvider measureOperationsProvider() {
return new MeasureOperationsProvider();
}
@Bean
public ModelResolver fhirModelResolver () {
return new CachingModelResolverDecorator(new Dstu3FhirModelResolver());
}
@Bean
public LibraryHelper libraryHelper(Map<VersionedIdentifier, Model> globalModelCache) {
return new LibraryHelper(globalModelCache);
}
}

View File

@ -0,0 +1,92 @@
package ca.uhn.fhir.cql.config;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.cql.common.provider.CqlProviderFactory;
import ca.uhn.fhir.cql.common.provider.EvaluationProviderFactory;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.cql.r4.provider.LibraryResolutionProviderImpl;
import ca.uhn.fhir.cql.r4.evaluation.ProviderFactory;
import ca.uhn.fhir.cql.r4.helper.LibraryHelper;
import ca.uhn.fhir.cql.r4.provider.JpaTerminologyProvider;
import ca.uhn.fhir.cql.r4.provider.MeasureOperationsProvider;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.rp.r4.ValueSetResourceProvider;
import ca.uhn.fhir.jpa.term.api.ITermReadSvcR4;
import org.opencds.cqf.cql.engine.model.ModelResolver;
import java.util.Map;
import org.cqframework.cql.cql2elm.model.Model;
import org.hl7.elm.r1.VersionedIdentifier;
import org.opencds.cqf.cql.engine.fhir.model.R4FhirModelResolver;
import org.opencds.cqf.cql.engine.terminology.TerminologyProvider;
import org.opencds.cqf.cql.evaluator.engine.model.CachingModelResolverDecorator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@Configuration
public class CqlR4Config extends BaseCqlConfig {
@Lazy
@Bean
CqlProviderFactory cqlProviderFactory() {
return new CqlProviderFactory();
}
@Lazy
@Bean
TerminologyProvider terminologyProvider(ITermReadSvcR4 theITermReadSvc, ValueSetResourceProvider theValueSetResourceProvider, IValidationSupport theValidationSupport) {
return new JpaTerminologyProvider(theITermReadSvc,theValueSetResourceProvider, theValidationSupport);
}
@Lazy
@Bean
EvaluationProviderFactory evaluationProviderFactory(FhirContext theFhirContext, DaoRegistry theDaoRegistry, TerminologyProvider theLocalSystemTerminologyProvider, ModelResolver modelResolver) {
return new ProviderFactory(theFhirContext, theDaoRegistry, theLocalSystemTerminologyProvider, modelResolver);
}
@Lazy
@Bean
LibraryResolutionProvider libraryResolutionProvider() {
return new LibraryResolutionProviderImpl();
}
@Lazy
@Bean
public MeasureOperationsProvider measureOperationsProvider() {
return new MeasureOperationsProvider();
}
@Bean
public ModelResolver fhirModelResolver () {
return new CachingModelResolverDecorator(new R4FhirModelResolver());
}
@Bean
public LibraryHelper libraryHelper(Map<VersionedIdentifier, Model> globalModelCache) {
return new LibraryHelper(globalModelCache);
}
}

View File

@ -0,0 +1,69 @@
package ca.uhn.fhir.cql.dstu3.builder;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.cql.common.builder.BaseBuilder;
import org.hl7.fhir.dstu3.model.MeasureReport;
import org.hl7.fhir.dstu3.model.Period;
import org.hl7.fhir.dstu3.model.Reference;
import org.hl7.fhir.exceptions.FHIRException;
import org.opencds.cqf.cql.engine.runtime.Interval;
import java.util.Date;
public class MeasureReportBuilder extends BaseBuilder<MeasureReport> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(MeasureReportBuilder.class);
public MeasureReportBuilder() {
super(new MeasureReport());
}
public MeasureReportBuilder buildStatus(String status) {
try {
this.complexProperty.setStatus(MeasureReport.MeasureReportStatus.fromCode(status));
} catch (FHIRException e) {
ourLog.warn("Exception caught while attempting to set Status to '" + status + "', assuming status COMPLETE!"
+ System.lineSeparator() + e.getMessage());
this.complexProperty.setStatus(MeasureReport.MeasureReportStatus.COMPLETE);
}
return this;
}
public MeasureReportBuilder buildType(MeasureReport.MeasureReportType type) {
this.complexProperty.setType(type);
return this;
}
public MeasureReportBuilder buildMeasureReference(String measureRef) {
this.complexProperty.setMeasure(new Reference(measureRef));
return this;
}
public MeasureReportBuilder buildPatientReference(String patientRef) {
this.complexProperty.setPatient(new Reference(patientRef));
return this;
}
public MeasureReportBuilder buildPeriod(Interval period) {
this.complexProperty.setPeriod(new Period().setStart((Date) period.getStart()).setEnd((Date) period.getEnd()));
return this;
}
}

View File

@ -0,0 +1,730 @@
package ca.uhn.fhir.cql.dstu3.evaluation;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.cql.common.evaluation.MeasurePopulationType;
import ca.uhn.fhir.cql.common.evaluation.MeasureScoring;
import ca.uhn.fhir.cql.dstu3.builder.MeasureReportBuilder;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import org.cqframework.cql.elm.execution.ExpressionDef;
import org.cqframework.cql.elm.execution.FunctionDef;
import org.hl7.fhir.dstu3.model.CodeableConcept;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.Extension;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.IntegerType;
import org.hl7.fhir.dstu3.model.ListResource;
import org.hl7.fhir.dstu3.model.Measure;
import org.hl7.fhir.dstu3.model.MeasureReport;
import org.hl7.fhir.dstu3.model.Observation;
import org.hl7.fhir.dstu3.model.Patient;
import org.hl7.fhir.dstu3.model.Reference;
import org.hl7.fhir.dstu3.model.Resource;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.dstu3.model.UriType;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.opencds.cqf.cql.engine.execution.Context;
import org.opencds.cqf.cql.engine.execution.Variable;
import org.opencds.cqf.cql.engine.runtime.Code;
import org.opencds.cqf.cql.engine.runtime.Interval;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class MeasureEvaluation {
private static final Logger logger = LoggerFactory.getLogger(MeasureEvaluation.class);
private Interval measurementPeriod;
private DaoRegistry registry;
public MeasureEvaluation(DaoRegistry registry, Interval measurementPeriod) {
this.registry = registry;
this.measurementPeriod = measurementPeriod;
}
public MeasureReport evaluatePatientMeasure(Measure measure, Context context, String patientId) {
logger.info("Generating individual report");
if (patientId == null) {
return evaluatePopulationMeasure(measure, context);
}
Patient patient = registry.getResourceDao(Patient.class).read(new IdType(patientId));
// Iterable<Object> patientRetrieve = provider.retrieve("Patient", "id",
// patientId, "Patient", null, null, null, null, null, null, null, null);
// Patient patient = null;
// if (patientRetrieve.iterator().hasNext()) {
// patient = (Patient) patientRetrieve.iterator().next();
// }
boolean isSingle = true;
return evaluate(measure, context,
patient == null ? Collections.emptyList() : Collections.singletonList(patient),
MeasureReport.MeasureReportType.INDIVIDUAL, isSingle);
}
public MeasureReport evaluatePatientListMeasure(Measure measure, Context context, String practitionerRef) {
logger.info("Generating patient-list report");
List<Patient> patients = practitionerRef == null ? getAllPatients() : getPractitionerPatients(practitionerRef);
boolean isSingle = false;
return evaluate(measure, context, patients, MeasureReport.MeasureReportType.PATIENTLIST, isSingle);
}
private List<Patient> getPractitionerPatients(String practitionerRef) {
SearchParameterMap map = new SearchParameterMap();
map.add("general-practitioner", new ReferenceParam(
practitionerRef.startsWith("Practitioner/") ? practitionerRef : "Practitioner/" + practitionerRef));
List<Patient> patients = new ArrayList<>();
IBundleProvider patientProvider = registry.getResourceDao("Patient").search(map);
List<IBaseResource> patientList = patientProvider.getResources(0, patientProvider.size());
patientList.forEach(x -> patients.add((Patient) x));
return patients;
}
private List<Patient> getAllPatients() {
List<Patient> patients = new ArrayList<>();
IBundleProvider patientProvider = registry.getResourceDao("Patient").search(new SearchParameterMap());
List<IBaseResource> patientList = patientProvider.getResources(0, patientProvider.size());
patientList.forEach(x -> patients.add((Patient) x));
return patients;
}
public MeasureReport evaluatePopulationMeasure(Measure measure, Context context) {
logger.info("Generating summary report");
boolean isSingle = false;
return evaluate(measure, context, getAllPatients(), MeasureReport.MeasureReportType.SUMMARY, isSingle);
}
@SuppressWarnings("unchecked")
private void clearExpressionCache(Context context) {
// Hack to clear expression cache
// See cqf-ruler github issue #153
try {
Field privateField = Context.class.getDeclaredField("expressions");
privateField.setAccessible(true);
LinkedHashMap<String, Object> expressions = (LinkedHashMap<String, Object>) privateField.get(context);
expressions.clear();
} catch (Exception e) {
logger.warn("Error resetting expression cache", e);
}
}
private Resource evaluateObservationCriteria(Context context, Patient patient, Resource resource, Measure.MeasureGroupPopulationComponent pop, MeasureReport report) {
if (pop == null || !pop.hasCriteria()) {
return null;
}
context.setContextValue("Patient", patient.getIdElement().getIdPart());
clearExpressionCache(context);
String observationName = pop.getCriteria();
ExpressionDef ed = context.resolveExpressionRef(observationName);
if (!(ed instanceof FunctionDef)) {
throw new IllegalArgumentException(String.format("Measure observation %s does not reference a function definition", observationName));
}
Object result = null;
context.pushWindow();
try {
context.push(new Variable().withName(((FunctionDef)ed).getOperand().get(0).getName()).withValue(resource));
result = ed.getExpression().evaluate(context);
}
finally {
context.popWindow();
}
if (result instanceof Resource) {
return (Resource)result;
}
Observation obs = new Observation();
obs.setStatus(Observation.ObservationStatus.FINAL);
obs.setId(UUID.randomUUID().toString());
CodeableConcept cc = new CodeableConcept();
cc.setText(observationName);
obs.setCode(cc);
Extension obsExtension = new Extension().setUrl("http://hl7.org/fhir/StructureDefinition/cqf-measureInfo");
Extension extExtMeasure = new Extension()
.setUrl("measure")
.setValue(new UriType("http://hl7.org/fhir/us/cqfmeasures/" + report.getMeasure()));
obsExtension.addExtension(extExtMeasure);
Extension extExtPop = new Extension()
.setUrl("populationId")
.setValue(new StringType(observationName));
obsExtension.addExtension(extExtPop);
obs.addExtension(obsExtension);
return obs;
}
@SuppressWarnings("unchecked")
private Iterable<Resource> evaluateCriteria(Context context, Patient patient,
Measure.MeasureGroupPopulationComponent pop) {
if (pop == null || !pop.hasCriteria()) {
return Collections.emptyList();
}
context.setContextValue("Patient", patient.getIdElement().getIdPart());
clearExpressionCache(context);
Object result = context.resolveExpressionRef(pop.getCriteria()).evaluate(context);
if (result == null) {
return Collections.emptyList();
}
if (result instanceof Boolean) {
if (((Boolean) result)) {
return Collections.singletonList(patient);
} else {
return Collections.emptyList();
}
}
return (Iterable<Resource>) result;
}
private boolean evaluatePopulationCriteria(Context context, Patient patient,
Measure.MeasureGroupPopulationComponent criteria, HashMap<String, Resource> population,
HashMap<String, Patient> populationPatients, Measure.MeasureGroupPopulationComponent exclusionCriteria,
HashMap<String, Resource> exclusionPopulation, HashMap<String, Patient> exclusionPatients) {
boolean inPopulation = false;
if (criteria != null) {
for (Resource resource : evaluateCriteria(context, patient, criteria)) {
inPopulation = true;
population.put(resource.getIdElement().getIdPart(), resource);
}
}
if (inPopulation) {
// Are they in the exclusion?
if (exclusionCriteria != null) {
for (Resource resource : evaluateCriteria(context, patient, exclusionCriteria)) {
inPopulation = false;
exclusionPopulation.put(resource.getIdElement().getIdPart(), resource);
population.remove(resource.getIdElement().getIdPart());
}
}
}
if (inPopulation && populationPatients != null) {
populationPatients.put(patient.getIdElement().getIdPart(), patient);
}
if (!inPopulation && exclusionPatients != null) {
exclusionPatients.put(patient.getIdElement().getIdPart(), patient);
}
return inPopulation;
}
private void addPopulationCriteriaReport(MeasureReport report,
MeasureReport.MeasureReportGroupComponent reportGroup,
Measure.MeasureGroupPopulationComponent populationCriteria, int populationCount,
Iterable<Patient> patientPopulation) {
if (populationCriteria != null) {
MeasureReport.MeasureReportGroupPopulationComponent populationReport = new MeasureReport.MeasureReportGroupPopulationComponent();
populationReport.setIdentifier(populationCriteria.getIdentifier());
populationReport.setCode(populationCriteria.getCode());
if (report.getType() == MeasureReport.MeasureReportType.PATIENTLIST && patientPopulation != null) {
ListResource subjectList = new ListResource();
subjectList.setId(UUID.randomUUID().toString());
populationReport.setPatients(new Reference().setReference("#" + subjectList.getId()));
for (Patient patient : patientPopulation) {
ListResource.ListEntryComponent entry = new ListResource.ListEntryComponent()
.setItem(new Reference()
.setReference(patient.getIdElement().getIdPart().startsWith("Patient/")
? patient.getIdElement().getIdPart()
: String.format("Patient/%s", patient.getIdElement().getIdPart()))
.setDisplay(patient.getNameFirstRep().getNameAsSingleString()));
subjectList.addEntry(entry);
}
report.addContained(subjectList);
}
populationReport.setCount(populationCount);
reportGroup.addPopulation(populationReport);
}
}
private MeasureReport evaluate(Measure measure, Context context, List<Patient> patients,
MeasureReport.MeasureReportType type, boolean isSingle) {
MeasureReportBuilder reportBuilder = new MeasureReportBuilder();
reportBuilder.buildStatus("complete");
reportBuilder.buildType(type);
reportBuilder.buildMeasureReference(measure.getIdElement().getValue());
if (type == MeasureReport.MeasureReportType.INDIVIDUAL && !patients.isEmpty()) {
reportBuilder.buildPatientReference(patients.get(0).getIdElement().getValue());
}
reportBuilder.buildPeriod(measurementPeriod);
MeasureReport report = reportBuilder.build();
HashMap<String, Resource> resources = new HashMap<>();
HashMap<String, HashSet<String>> codeToResourceMap = new HashMap<>();
Set<String> evaluatedResourcesList = new HashSet<>();
MeasureScoring measureScoring = MeasureScoring.fromCode(measure.getScoring().getCodingFirstRep().getCode());
if (measureScoring == null) {
throw new RuntimeException("Measure scoring is required in order to calculate.");
}
List<Measure.MeasureSupplementalDataComponent> sde = new ArrayList<>();
HashMap<String, HashMap<String, Integer>> sdeAccumulators = null;
for (Measure.MeasureGroupComponent group : measure.getGroup()) {
MeasureReport.MeasureReportGroupComponent reportGroup = new MeasureReport.MeasureReportGroupComponent();
reportGroup.setIdentifier(group.getIdentifier());
report.getGroup().add(reportGroup);
// Declare variables to avoid a hash lookup on every patient
// TODO: Isn't quite right, there may be multiple initial populations for a
// ratio measure...
Measure.MeasureGroupPopulationComponent initialPopulationCriteria = null;
Measure.MeasureGroupPopulationComponent numeratorCriteria = null;
Measure.MeasureGroupPopulationComponent numeratorExclusionCriteria = null;
Measure.MeasureGroupPopulationComponent denominatorCriteria = null;
Measure.MeasureGroupPopulationComponent denominatorExclusionCriteria = null;
Measure.MeasureGroupPopulationComponent denominatorExceptionCriteria = null;
Measure.MeasureGroupPopulationComponent measurePopulationCriteria = null;
Measure.MeasureGroupPopulationComponent measurePopulationExclusionCriteria = null;
// TODO: Isn't quite right, there may be multiple measure observations...
Measure.MeasureGroupPopulationComponent measureObservationCriteria = null;
HashMap<String, Resource> initialPopulation = null;
HashMap<String, Resource> numerator = null;
HashMap<String, Resource> numeratorExclusion = null;
HashMap<String, Resource> denominator = null;
HashMap<String, Resource> denominatorExclusion = null;
HashMap<String, Resource> denominatorException = null;
HashMap<String, Resource> measurePopulation = null;
HashMap<String, Resource> measurePopulationExclusion = null;
HashMap<String, Resource> measureObservation = null;
HashMap<String, Patient> initialPopulationPatients = null;
HashMap<String, Patient> numeratorPatients = null;
HashMap<String, Patient> numeratorExclusionPatients = null;
HashMap<String, Patient> denominatorPatients = null;
HashMap<String, Patient> denominatorExclusionPatients = null;
HashMap<String, Patient> denominatorExceptionPatients = null;
HashMap<String, Patient> measurePopulationPatients = null;
HashMap<String, Patient> measurePopulationExclusionPatients = null;
sdeAccumulators = new HashMap<>();
sde = measure.getSupplementalData();
for (Measure.MeasureGroupPopulationComponent pop : group.getPopulation()) {
MeasurePopulationType populationType = MeasurePopulationType
.fromCode(pop.getCode().getCodingFirstRep().getCode());
if (populationType != null) {
switch (populationType) {
case INITIALPOPULATION:
initialPopulationCriteria = pop;
initialPopulation = new HashMap<String, Resource>();
if (type == MeasureReport.MeasureReportType.PATIENTLIST) {
initialPopulationPatients = new HashMap<String, Patient>();
}
break;
case NUMERATOR:
numeratorCriteria = pop;
numerator = new HashMap<String, Resource>();
if (type == MeasureReport.MeasureReportType.PATIENTLIST) {
numeratorPatients = new HashMap<String, Patient>();
}
break;
case NUMERATOREXCLUSION:
numeratorExclusionCriteria = pop;
numeratorExclusion = new HashMap<String, Resource>();
if (type == MeasureReport.MeasureReportType.PATIENTLIST) {
numeratorExclusionPatients = new HashMap<String, Patient>();
}
break;
case DENOMINATOR:
denominatorCriteria = pop;
denominator = new HashMap<String, Resource>();
if (type == MeasureReport.MeasureReportType.PATIENTLIST) {
denominatorPatients = new HashMap<String, Patient>();
}
break;
case DENOMINATOREXCLUSION:
denominatorExclusionCriteria = pop;
denominatorExclusion = new HashMap<String, Resource>();
if (type == MeasureReport.MeasureReportType.PATIENTLIST) {
denominatorExclusionPatients = new HashMap<String, Patient>();
}
break;
case DENOMINATOREXCEPTION:
denominatorExceptionCriteria = pop;
denominatorException = new HashMap<String, Resource>();
if (type == MeasureReport.MeasureReportType.PATIENTLIST) {
denominatorExceptionPatients = new HashMap<String, Patient>();
}
break;
case MEASUREPOPULATION:
measurePopulationCriteria = pop;
measurePopulation = new HashMap<String, Resource>();
if (type == MeasureReport.MeasureReportType.PATIENTLIST) {
measurePopulationPatients = new HashMap<String, Patient>();
}
break;
case MEASUREPOPULATIONEXCLUSION:
measurePopulationExclusionCriteria = pop;
measurePopulationExclusion = new HashMap<String, Resource>();
if (type == MeasureReport.MeasureReportType.PATIENTLIST) {
measurePopulationExclusionPatients = new HashMap<String, Patient>();
}
break;
case MEASUREOBSERVATION:
measureObservationCriteria = pop;
measureObservation = new HashMap<>();
break;
}
}
}
switch (measureScoring) {
case PROPORTION:
case RATIO: {
// For each patient in the initial population
for (Patient patient : patients) {
// Are they in the initial population?
boolean inInitialPopulation = evaluatePopulationCriteria(context, patient,
initialPopulationCriteria, initialPopulation, initialPopulationPatients, null, null,
null);
populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources,
codeToResourceMap);
if (inInitialPopulation) {
// Are they in the denominator?
boolean inDenominator = evaluatePopulationCriteria(context, patient, denominatorCriteria,
denominator, denominatorPatients, denominatorExclusionCriteria,
denominatorExclusion, denominatorExclusionPatients);
populateResourceMap(context, MeasurePopulationType.DENOMINATOR, resources,
codeToResourceMap);
if (inDenominator) {
// Are they in the numerator?
boolean inNumerator = evaluatePopulationCriteria(context, patient, numeratorCriteria,
numerator, numeratorPatients, numeratorExclusionCriteria, numeratorExclusion,
numeratorExclusionPatients);
populateResourceMap(context, MeasurePopulationType.NUMERATOR, resources,
codeToResourceMap);
if (!inNumerator && inDenominator && (denominatorExceptionCriteria != null)) {
// Are they in the denominator exception?
boolean inException = false;
for (Resource resource : evaluateCriteria(context, patient,
denominatorExceptionCriteria)) {
inException = true;
denominatorException.put(resource.getIdElement().getIdPart(), resource);
denominator.remove(resource.getIdElement().getIdPart());
populateResourceMap(context, MeasurePopulationType.DENOMINATOREXCEPTION,
resources, codeToResourceMap);
}
if (inException) {
if (denominatorExceptionPatients != null) {
denominatorExceptionPatients.put(patient.getIdElement().getIdPart(),
patient);
}
if (denominatorPatients != null) {
denominatorPatients.remove(patient.getIdElement().getIdPart());
}
}
}
}
}
populateSDEAccumulators(measure, context, patient, sdeAccumulators, sde);
}
// Calculate actual measure score, Count(numerator) / Count(denominator)
if (denominator != null && numerator != null && denominator.size() > 0) {
reportGroup.setMeasureScore(numerator.size() / (double) denominator.size());
}
break;
}
case CONTINUOUSVARIABLE: {
// For each patient in the patient list
for (Patient patient : patients) {
// Are they in the initial population?
boolean inInitialPopulation = evaluatePopulationCriteria(context, patient,
initialPopulationCriteria, initialPopulation, initialPopulationPatients, null, null,
null);
populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources,
codeToResourceMap);
if (inInitialPopulation) {
// Are they in the measure population?
boolean inMeasurePopulation = evaluatePopulationCriteria(context, patient,
measurePopulationCriteria, measurePopulation, measurePopulationPatients,
measurePopulationExclusionCriteria, measurePopulationExclusion,
measurePopulationExclusionPatients);
if (inMeasurePopulation) {
for (Resource resource : measurePopulation.values()) {
Resource observation = evaluateObservationCriteria(context, patient, resource, measureObservationCriteria, report);
measureObservation.put(resource.getIdElement().getIdPart(), observation);
report.addContained(observation);
// TODO: Add to the evaluatedResources bundle
//report.getEvaluatedResources().add(new Reference("#" + observation.getId()));
}
}
}
populateSDEAccumulators(measure, context, patient, sdeAccumulators,sde);
}
break;
}
case COHORT: {
// For each patient in the patient list
for (Patient patient : patients) {
evaluatePopulationCriteria(context, patient,
initialPopulationCriteria, initialPopulation, initialPopulationPatients, null, null,
null);
populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources,
codeToResourceMap);
populateSDEAccumulators(measure, context, patient, sdeAccumulators, sde);
}
break;
}
}
// Add population reports for each group
addPopulationCriteriaReport(report, reportGroup, initialPopulationCriteria,
initialPopulation != null ? initialPopulation.size() : 0,
initialPopulationPatients != null ? initialPopulationPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, numeratorCriteria,
numerator != null ? numerator.size() : 0,
numeratorPatients != null ? numeratorPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, numeratorExclusionCriteria,
numeratorExclusion != null ? numeratorExclusion.size() : 0,
numeratorExclusionPatients != null ? numeratorExclusionPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, denominatorCriteria,
denominator != null ? denominator.size() : 0,
denominatorPatients != null ? denominatorPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, denominatorExclusionCriteria,
denominatorExclusion != null ? denominatorExclusion.size() : 0,
denominatorExclusionPatients != null ? denominatorExclusionPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, denominatorExceptionCriteria,
denominatorException != null ? denominatorException.size() : 0,
denominatorExceptionPatients != null ? denominatorExceptionPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, measurePopulationCriteria,
measurePopulation != null ? measurePopulation.size() : 0,
measurePopulationPatients != null ? measurePopulationPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, measurePopulationExclusionCriteria,
measurePopulationExclusion != null ? measurePopulationExclusion.size() : 0,
measurePopulationExclusionPatients != null ? measurePopulationExclusionPatients.values() : null);
// TODO: Measure Observations...
}
for (String key : codeToResourceMap.keySet()) {
org.hl7.fhir.dstu3.model.ListResource list = new org.hl7.fhir.dstu3.model.ListResource();
for (String element : codeToResourceMap.get(key)) {
org.hl7.fhir.dstu3.model.ListResource.ListEntryComponent comp = new org.hl7.fhir.dstu3.model.ListResource.ListEntryComponent();
comp.setItem(new Reference('#' + element));
list.addEntry(comp);
}
if (!list.isEmpty()) {
list.setId("List/" + UUID.randomUUID().toString());
list.setTitle(key);
resources.put(list.getId(), list);
list.getEntry().forEach(listResource -> evaluatedResourcesList.add(listResource.getItem().getReference()));
}
}
if (!resources.isEmpty()) {
List<Reference> evaluatedResourceIds = new ArrayList<>();
evaluatedResourcesList.forEach((resource) -> {
evaluatedResourceIds.add(new Reference(resource));
});
}
if (sdeAccumulators.size() > 0) {
report = processAccumulators(report, sdeAccumulators, sde, isSingle, patients);
}
return report;
}
private void populateSDEAccumulators(Measure measure, Context context, Patient patient,HashMap<String, HashMap<String, Integer>> sdeAccumulators,
List<Measure.MeasureSupplementalDataComponent> sde){
context.setContextValue("Patient", patient.getIdElement().getIdPart());
List<Object> sdeList = sde.stream().map(sdeItem -> context.resolveExpressionRef(sdeItem.getCriteria()).evaluate(context)).collect(Collectors.toList());
if(!sdeList.isEmpty()) {
for (int i = 0; i < sdeList.size(); i++) {
Object sdeListItem = sdeList.get(i);
if(null != sdeListItem) {
String sdeAccumulatorKey = sde.get(i).getId();
if(null == sdeAccumulatorKey || sdeAccumulatorKey.length() < 1){
sdeAccumulatorKey = sde.get(i).getCriteria();
}
HashMap<String, Integer> sdeItemMap = sdeAccumulators.get(sdeAccumulatorKey);
String code = "";
switch (sdeListItem.getClass().getSimpleName()) {
case "Code":
code = ((Code) sdeListItem).getCode();
break;
case "ArrayList":
if (((ArrayList<?>) sdeListItem).size() > 0) {
if (((ArrayList<?>) sdeListItem).get(0).getClass().getSimpleName().equals("Coding")) {
code = ((Coding) ((ArrayList<?>) sdeListItem).get(0)).getCode();
} else {
continue;
}
}else{
continue;
}
break;
}
if(null == code){
continue;
}
if (null != sdeItemMap && null != sdeItemMap.get(code)) {
Integer sdeItemValue = sdeItemMap.get(code);
sdeItemValue++;
sdeItemMap.put(code, sdeItemValue);
sdeAccumulators.get(sdeAccumulatorKey).put(code, sdeItemValue);
} else {
if (null == sdeAccumulators.get(sdeAccumulatorKey)) {
HashMap<String, Integer> newSDEItem = new HashMap<>();
newSDEItem.put(code, 1);
sdeAccumulators.put(sdeAccumulatorKey, newSDEItem);
} else {
sdeAccumulators.get(sdeAccumulatorKey).put(code, 1);
}
}
}
}
}
}
private MeasureReport processAccumulators(MeasureReport report, HashMap<String, HashMap<String, Integer>> sdeAccumulators,
List<Measure.MeasureSupplementalDataComponent> sde, boolean isSingle, List<Patient> patients){
List<Reference> newRefList = new ArrayList<>();
sdeAccumulators.forEach((sdeKey, sdeAccumulator) -> {
sdeAccumulator.forEach((sdeAccumulatorKey, sdeAccumulatorValue)->{
Observation obs = new Observation();
obs.setStatus(Observation.ObservationStatus.FINAL);
obs.setId(UUID.randomUUID().toString());
Coding valueCoding = new Coding();
if(sdeKey.equalsIgnoreCase("sde-sex")){
valueCoding.setCode(sdeAccumulatorKey);
}else {
String coreCategory = sdeKey.substring(sdeKey.lastIndexOf('-') >= 0 ? sdeKey.lastIndexOf('-') : 0);
patients.forEach((pt)-> {
pt.getExtension().forEach((ptExt) -> {
if (ptExt.getUrl().contains(coreCategory)) {
String code = ((Coding) ptExt.getExtension().get(0).getValue()).getCode();
if(code.equalsIgnoreCase(sdeAccumulatorKey)) {
valueCoding.setSystem(((Coding) ptExt.getExtension().get(0).getValue()).getSystem());
valueCoding.setCode(code);
valueCoding.setDisplay(((Coding) ptExt.getExtension().get(0).getValue()).getDisplay());
}
}
});
});
}
CodeableConcept obsCodeableConcept = new CodeableConcept();
Extension obsExtension = new Extension().setUrl("http://hl7.org/fhir/StructureDefinition/cqf-measureInfo");
Extension extExtMeasure = new Extension()
.setUrl("measure")
.setValue(new StringType("http://hl7.org/fhir/us/cqfmeasures/" + report.getMeasure()));
obsExtension.addExtension(extExtMeasure);
Extension extExtPop = new Extension()
.setUrl("populationId")
.setValue(new StringType(sdeKey));
obsExtension.addExtension(extExtPop);
obs.addExtension(obsExtension);
obs.setValue(new IntegerType(sdeAccumulatorValue));
if(!isSingle) {
valueCoding.setCode(sdeAccumulatorKey);
obsCodeableConcept.setCoding(Collections.singletonList(valueCoding));
obs.setCode(obsCodeableConcept);
}else{
obs.setCode(new CodeableConcept().setText(sdeKey));
obsCodeableConcept.setCoding(Collections.singletonList(valueCoding));
obs.setValue(obsCodeableConcept);
}
newRefList.add(new Reference("#" + obs.getId()));
report.addContained(obs);
});
});
// TODO: Evaluated resources
// newRefList.addAll(report.getEvaluatedResource());
// report.setEvaluatedResource(newRefList);
return report;
}
private void populateResourceMap(Context context, MeasurePopulationType type, HashMap<String, Resource> resources,
HashMap<String, HashSet<String>> codeToResourceMap) {
if (context.getEvaluatedResources().isEmpty()) {
return;
}
if (!codeToResourceMap.containsKey(type.toCode())) {
codeToResourceMap.put(type.toCode(), new HashSet<>());
}
HashSet<String> codeHashSet = codeToResourceMap.get((type.toCode()));
for (Object o : context.getEvaluatedResources()) {
if (o instanceof Resource) {
Resource r = (Resource) o;
String id = (r.getIdElement().getResourceType() != null ? (r.getIdElement().getResourceType() + "/")
: "") + r.getIdElement().getIdPart();
if (!codeHashSet.contains(id)) {
codeHashSet.add(id);
}
if (!resources.containsKey(id)) {
resources.put(id, r);
}
}
}
context.clearEvaluatedResources();
}
}

View File

@ -0,0 +1,134 @@
package ca.uhn.fhir.cql.dstu3.evaluation;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.cql.common.helper.DateHelper;
import ca.uhn.fhir.cql.common.helper.UsingHelper;
import ca.uhn.fhir.cql.common.provider.EvaluationProviderFactory;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.cql.dstu3.helper.LibraryHelper;
import org.apache.commons.lang3.tuple.Triple;
import org.cqframework.cql.elm.execution.Library;
import org.hl7.fhir.dstu3.model.Measure;
import org.opencds.cqf.cql.engine.data.DataProvider;
import org.opencds.cqf.cql.engine.debug.DebugMap;
import org.opencds.cqf.cql.engine.execution.Context;
import org.opencds.cqf.cql.engine.execution.LibraryLoader;
import org.opencds.cqf.cql.engine.runtime.DateTime;
import org.opencds.cqf.cql.engine.runtime.Interval;
import org.opencds.cqf.cql.engine.terminology.TerminologyProvider;
import java.util.Date;
import java.util.List;
public class MeasureEvaluationSeed {
private Measure measure;
private Context context;
private Interval measurementPeriod;
private LibraryLoader libraryLoader;
private LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> libraryResourceProvider;
private EvaluationProviderFactory providerFactory;
private DataProvider dataProvider;
private LibraryHelper libraryHelper;
public MeasureEvaluationSeed(EvaluationProviderFactory providerFactory, LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> libraryResourceProvider, LibraryHelper libraryHelper) {
this.providerFactory = providerFactory;
this.libraryLoader = libraryLoader;
this.libraryResourceProvider = libraryResourceProvider;
this.libraryHelper = libraryHelper;
}
public Measure getMeasure() {
return this.measure;
}
public Context getContext() {
return this.context;
}
public Interval getMeasurementPeriod() {
return this.measurementPeriod;
}
public DataProvider getDataProvider() {
return this.dataProvider;
}
public void setup(Measure measure, String periodStart, String periodEnd, String productLine, String source,
String user, String pass) {
this.measure = measure;
this.libraryHelper.loadLibraries(measure, this.libraryLoader, this.libraryResourceProvider);
// resolve primary library
Library library = this.libraryHelper.resolvePrimaryLibrary(measure, libraryLoader, this.libraryResourceProvider);
// resolve execution context
context = new Context(library);
context.registerLibraryLoader(libraryLoader);
List<Triple<String, String, String>> usingDefs = UsingHelper.getUsingUrlAndVersion(library.getUsings());
if (usingDefs.size() > 1) {
throw new IllegalArgumentException(
"Evaluation of Measure using multiple Models is not supported at this time.");
}
// If there are no Usings, there is probably not any place the Terminology
// actually used so I think the assumption that at least one provider exists is
// ok.
TerminologyProvider terminologyProvider = null;
if (usingDefs.size() > 0) {
// Creates a terminology provider based on the first using statement. This
// assumes the terminology
// server matches the FHIR version of the CQL.
terminologyProvider = this.providerFactory.createTerminologyProvider(usingDefs.get(0).getLeft(),
usingDefs.get(0).getMiddle(), source, user, pass);
context.registerTerminologyProvider(terminologyProvider);
}
for (Triple<String, String, String> def : usingDefs) {
this.dataProvider = this.providerFactory.createDataProvider(def.getLeft(), def.getMiddle(),
terminologyProvider);
context.registerDataProvider(def.getRight(), dataProvider);
}
// resolve the measurement period
measurementPeriod = new Interval(DateHelper.resolveRequestDate(periodStart), true,
DateHelper.resolveRequestDate(periodEnd), true);
context.setParameter(null, "Measurement Period",
new Interval(DateTime.fromJavaDate((Date) measurementPeriod.getStart()), true,
DateTime.fromJavaDate((Date) measurementPeriod.getEnd()), true));
if (productLine != null) {
context.setParameter(null, "Product Line", productLine);
}
context.setExpressionCaching(true);
// This needs to be made configurable
DebugMap debugMap = new DebugMap();
debugMap.setIsLoggingEnabled(true);
context.setDebugMap(debugMap);
}
}

View File

@ -0,0 +1,84 @@
package ca.uhn.fhir.cql.dstu3.evaluation;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.cql.common.provider.EvaluationProviderFactory;
import ca.uhn.fhir.cql.common.retrieve.JpaFhirRetrieveProvider;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import org.opencds.cqf.cql.engine.data.CompositeDataProvider;
import org.opencds.cqf.cql.engine.data.DataProvider;
import org.opencds.cqf.cql.engine.fhir.searchparam.SearchParameterResolver;
import org.opencds.cqf.cql.engine.model.ModelResolver;
import org.opencds.cqf.cql.engine.terminology.TerminologyProvider;
import org.opencds.cqf.cql.evaluator.engine.terminology.PrivateCachingTerminologyProviderDecorator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// This class is a relatively dumb factory for data providers. It supports only
// creating JPA providers for FHIR and only basic auth for terminology
@Component
public class ProviderFactory implements EvaluationProviderFactory {
DaoRegistry registry;
TerminologyProvider defaultTerminologyProvider;
FhirContext fhirContext;
ModelResolver fhirModelResolver;
@Autowired
public ProviderFactory(FhirContext fhirContext, DaoRegistry registry,
TerminologyProvider defaultTerminologyProvider, ModelResolver fhirModelResolver) {
this.defaultTerminologyProvider = defaultTerminologyProvider;
this.registry = registry;
this.fhirContext = fhirContext;
this.fhirModelResolver = fhirModelResolver;
}
public DataProvider createDataProvider(String model, String version) {
return this.createDataProvider(model, version, null, null, null);
}
public DataProvider createDataProvider(String model, String version, String url, String user, String pass) {
TerminologyProvider terminologyProvider = this.createTerminologyProvider(model, version, url, user, pass);
return this.createDataProvider(model, version, terminologyProvider);
}
public DataProvider createDataProvider(String model, String version, TerminologyProvider terminologyProvider) {
if (model.equals("FHIR") && version.startsWith("3")) {
JpaFhirRetrieveProvider retrieveProvider = new JpaFhirRetrieveProvider(this.registry,
new SearchParameterResolver(this.fhirContext));
retrieveProvider.setTerminologyProvider(terminologyProvider);
retrieveProvider.setExpandValueSets(true);
return new CompositeDataProvider(this.fhirModelResolver, retrieveProvider);
}
throw new IllegalArgumentException(
String.format("Can't construct a data provider for model %s version %s", model, version));
}
public TerminologyProvider createTerminologyProvider(String model, String version, String url, String user,
String pass) {
TerminologyProvider terminologyProvider = null;
terminologyProvider = this.defaultTerminologyProvider;
return new PrivateCachingTerminologyProviderDecorator(terminologyProvider);
}
}

View File

@ -0,0 +1,209 @@
package ca.uhn.fhir.cql.dstu3.helper;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.cql.common.evaluation.LibraryLoader;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.cql.common.provider.LibrarySourceProvider;
import org.cqframework.cql.cql2elm.LibraryManager;
import org.cqframework.cql.cql2elm.ModelManager;
import org.cqframework.cql.cql2elm.model.Model;
import org.cqframework.cql.elm.execution.Library;
import org.cqframework.cql.elm.execution.VersionedIdentifier;
import org.hl7.fhir.dstu3.model.Attachment;
import org.hl7.fhir.dstu3.model.Coding;
import org.hl7.fhir.dstu3.model.Measure;
import org.hl7.fhir.dstu3.model.PlanDefinition;
import org.hl7.fhir.dstu3.model.Reference;
import org.hl7.fhir.dstu3.model.RelatedArtifact;
import org.hl7.fhir.dstu3.model.Resource;
import org.opencds.cqf.cql.evaluator.cql2elm.model.CacheAwareModelManager;
import org.opencds.cqf.cql.evaluator.engine.execution.PrivateCachingLibraryLoaderDecorator;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class LibraryHelper {
private Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache;
public LibraryHelper(Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache) {
this.modelCache = modelCache;
}
public org.opencds.cqf.cql.engine.execution.LibraryLoader createLibraryLoader(
LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> provider) {
ModelManager modelManager = new CacheAwareModelManager(this.modelCache);
LibraryManager libraryManager = new LibraryManager(modelManager);
libraryManager.getLibrarySourceLoader().clearProviders();
libraryManager.getLibrarySourceLoader().registerProvider(
new LibrarySourceProvider<org.hl7.fhir.dstu3.model.Library, Attachment>(
provider, x -> x.getContent(), x -> x.getContentType(), x -> x.getData()));
return new PrivateCachingLibraryLoaderDecorator(new LibraryLoader(libraryManager, modelManager));
}
public List<Library> loadLibraries(Measure measure,
org.opencds.cqf.cql.engine.execution.LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> libraryResourceProvider) {
List<org.cqframework.cql.elm.execution.Library> libraries = new ArrayList<Library>();
// load libraries
//TODO: if there's a bad measure argument, this blows up for an obscure error
for (Reference ref : measure.getLibrary()) {
// if library is contained in measure, load it into server
if (ref.getReferenceElement().getIdPart().startsWith("#")) {
for (Resource resource : measure.getContained()) {
if (resource instanceof org.hl7.fhir.dstu3.model.Library && resource.getIdElement().getIdPart()
.equals(ref.getReferenceElement().getIdPart().substring(1))) {
libraryResourceProvider.update((org.hl7.fhir.dstu3.model.Library) resource);
}
}
}
// We just loaded it into the server so we can access it by Id
String id = ref.getReferenceElement().getIdPart();
if (id.startsWith("#")) {
id = id.substring(1);
}
org.hl7.fhir.dstu3.model.Library library = libraryResourceProvider.resolveLibraryById(id);
if (library != null && isLogicLibrary(library)) {
libraries.add(libraryLoader
.load(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion())));
}
}
if (libraries.isEmpty()) {
throw new IllegalArgumentException(String
.format("Could not load library source for libraries referenced in Measure %s.", measure.getId()));
}
VersionedIdentifier primaryLibraryId = libraries.get(0).getIdentifier();
org.hl7.fhir.dstu3.model.Library primaryLibrary = libraryResourceProvider.resolveLibraryByName(primaryLibraryId.getId(), primaryLibraryId.getVersion());
for (RelatedArtifact artifact : primaryLibrary.getRelatedArtifact()) {
if (artifact.hasType() && artifact.getType().equals(RelatedArtifact.RelatedArtifactType.DEPENDSON) && artifact.hasResource() && artifact.getResource().hasReference()) {
if (artifact.getResource().getReferenceElement().getResourceType().equals("Library")) {
org.hl7.fhir.dstu3.model.Library library = libraryResourceProvider.resolveLibraryById(artifact.getResource().getReferenceElement().getIdPart());
if (library != null && isLogicLibrary(library)) {
libraries.add(
libraryLoader.load(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion()))
);
}
}
}
}
return libraries;
}
private boolean isLogicLibrary(org.hl7.fhir.dstu3.model.Library library) {
if (library == null) {
return false;
}
if (!library.hasType()) {
// If no type is specified, assume it is a logic library based on whether there is a CQL content element.
if (library.hasContent()) {
for (Attachment a : library.getContent()) {
if (a.hasContentType() && (a.getContentType().equals("text/cql")
|| a.getContentType().equals("application/elm+xml")
|| a.getContentType().equals("application/elm+json"))) {
return true;
}
}
}
return false;
}
if (!library.getType().hasCoding()) {
return false;
}
for (Coding c : library.getType().getCoding()) {
if (c.hasSystem() && c.getSystem().equals("http://hl7.org/fhir/library-type")
&& c.hasCode() && c.getCode().equals("logic-library")) {
return true;
}
}
return false;
}
public Library resolveLibraryById(String libraryId,
org.opencds.cqf.cql.engine.execution.LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> libraryResourceProvider) {
// Library library = null;
org.hl7.fhir.dstu3.model.Library fhirLibrary = libraryResourceProvider.resolveLibraryById(libraryId);
return libraryLoader
.load(new VersionedIdentifier().withId(fhirLibrary.getName()).withVersion(fhirLibrary.getVersion()));
// for (Library l : libraryLoader.getLibraries()) {
// VersionedIdentifier vid = l.getIdentifier();
// if (vid.getId().equals(fhirLibrary.getName()) &&
// LibraryResourceHelper.compareVersions(fhirLibrary.getVersion(),
// vid.getVersion()) == 0) {
// library = l;
// break;
// }
// }
// if (library == null) {
// }
// return library;
}
public Library resolvePrimaryLibrary(Measure measure,
org.opencds.cqf.cql.engine.execution.LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> libraryResourceProvider) {
// default is the first library reference
String id = measure.getLibraryFirstRep().getReferenceElement().getIdPart();
Library library = resolveLibraryById(id, libraryLoader, libraryResourceProvider);
if (library == null) {
throw new IllegalArgumentException(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.dstu3.model.Library> libraryResourceProvider) {
String id = planDefinition.getLibraryFirstRep().getReferenceElement().getIdPart();
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;
}
}

View File

@ -0,0 +1,152 @@
package ca.uhn.fhir.cql.dstu3.provider;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport.LookupCodeResult;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
import ca.uhn.fhir.jpa.rp.dstu3.ValueSetResourceProvider;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.term.api.ITermReadSvcDstu3;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.opencds.cqf.cql.engine.runtime.Code;
import org.opencds.cqf.cql.engine.terminology.CodeSystemInfo;
import org.opencds.cqf.cql.engine.terminology.TerminologyProvider;
import org.opencds.cqf.cql.engine.terminology.ValueSetInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Christopher Schuler on 7/17/2017.
*/
@Component
public class JpaTerminologyProvider implements TerminologyProvider {
private ITermReadSvcDstu3 terminologySvc;
private ValueSetResourceProvider valueSetResourceProvider;
private final IValidationSupport validationSupport;
@Autowired
public JpaTerminologyProvider(ITermReadSvcDstu3 terminologySvc,
ValueSetResourceProvider valueSetResourceProvider, IValidationSupport validationSupport) {
this.terminologySvc = terminologySvc;
this.valueSetResourceProvider = valueSetResourceProvider;
this.validationSupport = validationSupport;
}
@Override
public boolean in(Code code, ValueSetInfo valueSet) throws ResourceNotFoundException {
for (Code c : expand(valueSet)) {
if (c == null)
continue;
if (c.getCode().equals(code.getCode()) && c.getSystem().equals(code.getSystem())) {
return true;
}
}
return false;
}
@Override
public Iterable<Code> expand(ValueSetInfo valueSet) throws ResourceNotFoundException {
List<Code> codes = new ArrayList<>();
boolean needsExpand = false;
ValueSet vs = null;
if (valueSet.getId().startsWith("http://") || valueSet.getId().startsWith("https://")) {
if (valueSet.getVersion() != null
|| (valueSet.getCodeSystems() != null && valueSet.getCodeSystems().size() > 0)) {
if (!(valueSet.getCodeSystems().size() == 1 && valueSet.getCodeSystems().get(0).getVersion() == null)) {
throw new UnsupportedOperationException(String.format(
"Could not expand value set %s; version and code system bindings are not supported at this time.",
valueSet.getId()));
}
}
IBundleProvider bundleProvider = valueSetResourceProvider.getDao()
.search(new SearchParameterMap().add(ValueSet.SP_URL, new UriParam(valueSet.getId())));
List<IBaseResource> valueSets = bundleProvider.getResources(0, bundleProvider.size());
if (valueSets.isEmpty()) {
throw new IllegalArgumentException(String.format("Could not resolve value set %s.", valueSet.getId()));
} else if (valueSets.size() == 1) {
vs = (ValueSet) valueSets.get(0);
} else {
throw new IllegalArgumentException("Found more than 1 ValueSet with url: " + valueSet.getId());
}
} else {
vs = valueSetResourceProvider.getDao().read(new IdType(valueSet.getId()));
}
if (vs != null) {
if (vs.hasCompose()) {
if (vs.getCompose().hasInclude()) {
for (ValueSet.ConceptSetComponent include : vs.getCompose().getInclude()) {
if (include.hasValueSet() || include.hasFilter()) {
needsExpand = true;
break;
}
for (ValueSet.ConceptReferenceComponent concept : include.getConcept()) {
if (concept.hasCode()) {
codes.add(new Code().withCode(concept.getCode()).withSystem(include.getSystem()));
}
}
}
if (!needsExpand) {
return codes;
}
}
}
if (vs.hasExpansion() && vs.getExpansion().hasContains()) {
for (ValueSetExpansionContainsComponent vsecc : vs.getExpansion().getContains()) {
codes.add(new Code().withCode(vsecc.getCode()).withSystem(vsecc.getSystem()));
}
return codes;
}
}
org.hl7.fhir.r4.model.ValueSet expansion = terminologySvc
.expandValueSet(new ValueSetExpansionOptions().setCount(Integer.MAX_VALUE), valueSet.getId(), null);
expansion.getExpansion().getContains()
.forEach(concept -> codes.add(new Code().withCode(concept.getCode()).withSystem(concept.getSystem())));
return codes;
}
@Override
public Code lookup(Code code, CodeSystemInfo codeSystem) throws ResourceNotFoundException {
LookupCodeResult cs = terminologySvc.lookupCode(new ValidationSupportContext(validationSupport), codeSystem.getId(), code.getCode());
code.setDisplay(cs.getCodeDisplay());
code.setSystem(codeSystem.getId());
return code;
}
}

View File

@ -0,0 +1,119 @@
package ca.uhn.fhir.cql.dstu3.provider;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.param.UriParam;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.Library;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Component
public class LibraryResolutionProviderImpl implements LibraryResolutionProvider<Library> {
@Autowired
private IFhirResourceDao<Library> myLibraryDao;
// TODO: Figure out if we should throw an exception or something here.
@Override
public void update(Library library) {
myLibraryDao.update(library);
}
@Override
public Library resolveLibraryById(String libraryId) {
try {
return myLibraryDao.read(new IdType(libraryId));
} catch (Exception e) {
throw new IllegalArgumentException(String.format("Could not resolve library id %s", libraryId));
}
}
@Override
public Library resolveLibraryByCanonicalUrl(String url) {
Objects.requireNonNull(url, "url must not be null");
String[] parts = url.split("\\|");
String resourceUrl = parts[0];
String version = null;
if (parts.length > 1) {
version = parts[1];
}
SearchParameterMap map = new SearchParameterMap();
map.add("url", new UriParam(resourceUrl));
if (version != null) {
map.add("version", new TokenParam(version));
}
ca.uhn.fhir.rest.api.server.IBundleProvider bundleProvider = myLibraryDao.search(map);
if (bundleProvider.size() == 0) {
return null;
}
List<IBaseResource> resourceList = bundleProvider.getResources(0, bundleProvider.size());
return LibraryResolutionProvider.selectFromList(resolveLibraries(resourceList), version, x -> x.getVersion());
}
@Override
public Library resolveLibraryByName(String libraryName, String libraryVersion) {
Iterable<org.hl7.fhir.dstu3.model.Library> libraries = getLibrariesByName(libraryName);
org.hl7.fhir.dstu3.model.Library library = LibraryResolutionProvider.selectFromList(libraries, libraryVersion,
x -> x.getVersion());
if (library == null) {
throw new IllegalArgumentException(String.format("Could not resolve library name %s", libraryName));
}
return library;
}
private Iterable<org.hl7.fhir.dstu3.model.Library> getLibrariesByName(String name) {
// Search for libraries by name
SearchParameterMap map = new SearchParameterMap();
map.add("name", new StringParam(name, true));
ca.uhn.fhir.rest.api.server.IBundleProvider bundleProvider = myLibraryDao.search(map);
if (bundleProvider.size() == 0) {
return new ArrayList<>();
}
List<IBaseResource> resourceList = bundleProvider.getResources(0, bundleProvider.size());
return resolveLibraries(resourceList);
}
private Iterable<org.hl7.fhir.dstu3.model.Library> resolveLibraries(List<IBaseResource> resourceList) {
List<org.hl7.fhir.dstu3.model.Library> ret = new ArrayList<>();
for (IBaseResource res : resourceList) {
Class<?> clazz = res.getClass();
ret.add((org.hl7.fhir.dstu3.model.Library) clazz.cast(res));
}
return ret;
}
}

View File

@ -0,0 +1,126 @@
package ca.uhn.fhir.cql.dstu3.provider;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.cql.common.provider.EvaluationProviderFactory;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.cql.dstu3.evaluation.MeasureEvaluation;
import ca.uhn.fhir.cql.dstu3.evaluation.MeasureEvaluationSeed;
import ca.uhn.fhir.cql.dstu3.helper.LibraryHelper;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.dstu3.model.Extension;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.Library;
import org.hl7.fhir.dstu3.model.Measure;
import org.hl7.fhir.dstu3.model.MeasureReport;
import org.hl7.fhir.dstu3.model.StringType;
import org.hl7.fhir.exceptions.FHIRException;
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.stereotype.Component;
/**
* This class implements the dstu3 $evaluate-measure operation defined in the FHIR Clinical Reasoning module.
* Changes should comply with the specification in as far as is possible, and questions about Measure or CQL evaluation can be directed to the original authors.
* @author Jonathan Percival
* @author Bryn Rhodes
* @see <a href="https://hl7.org/fhir/STU3/measure-operations.html#evaluate-measure">https://hl7.org/fhir/STU3/measure-operations.html#evaluate-measure</a>
*/
@Component
public class MeasureOperationsProvider {
private static final Logger logger = LoggerFactory.getLogger(MeasureOperationsProvider.class);
@Autowired
private LibraryResolutionProvider<Library> libraryResolutionProvider;
@Autowired
private DaoRegistry registry;
@Autowired
private IFhirResourceDao<Measure> myMeasureDao;
@Autowired
private EvaluationProviderFactory factory;
@Autowired
private LibraryHelper libraryHelper;
/*
*
* NOTE that the source, user, and pass parameters are not standard parameters
* for the FHIR $evaluate-measure operation
*
*/
@Operation(name = "$evaluate-measure", idempotent = true, type = Measure.class)
public MeasureReport evaluateMeasure(@IdParam IdType theId,
@OperationParam(name = "periodStart") String periodStart,
@OperationParam(name = "periodEnd") String periodEnd,
@OperationParam(name = "measure") String measureRef,
@OperationParam(name = "reportType") String reportType,
@OperationParam(name = "patient") String patientRef,
@OperationParam(name = "productLine") String productLine,
@OperationParam(name = "practitioner") String practitionerRef,
@OperationParam(name = "lastReceivedOn") String lastReceivedOn,
@OperationParam(name = "source") String source,
@OperationParam(name = "user") String user,
@OperationParam(name = "pass") String pass) throws InternalErrorException, FHIRException {
LibraryLoader libraryLoader = this.libraryHelper.createLibraryLoader(this.libraryResolutionProvider);
MeasureEvaluationSeed seed = new MeasureEvaluationSeed(this.factory, libraryLoader,
this.libraryResolutionProvider, this.libraryHelper);
Measure measure = myMeasureDao.read(theId);
if (measure == null) {
throw new RuntimeException("Could not find Measure/" + theId.getIdPart());
}
seed.setup(measure, periodStart, periodEnd, productLine, source, user, pass);
// resolve report type
MeasureEvaluation evaluator = new MeasureEvaluation(this.registry,
seed.getMeasurementPeriod());
if (reportType != null) {
switch (reportType) {
case "patient":
return evaluator.evaluatePatientMeasure(seed.getMeasure(), seed.getContext(), patientRef);
case "patient-list":
return evaluator.evaluatePatientListMeasure(seed.getMeasure(), seed.getContext(), practitionerRef);
case "population":
return evaluator.evaluatePopulationMeasure(seed.getMeasure(), seed.getContext());
default:
throw new IllegalArgumentException("Invalid report type: " + reportType);
}
}
// default report type is patient
MeasureReport report = evaluator.evaluatePatientMeasure(seed.getMeasure(), seed.getContext(), patientRef);
if (productLine != null) {
Extension ext = new Extension();
ext.setUrl("http://hl7.org/fhir/us/cqframework/cqfmeasures/StructureDefinition/cqfm-productLine");
ext.setValue(new StringType(productLine));
report.addExtension(ext);
}
return report;
}
}

View File

@ -0,0 +1,84 @@
package ca.uhn.fhir.cql.r4.builder;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.cql.common.builder.BaseBuilder;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Period;
import org.hl7.fhir.r4.model.Reference;
import org.opencds.cqf.cql.engine.runtime.DateTime;
import org.opencds.cqf.cql.engine.runtime.Interval;
import java.util.Date;
public class MeasureReportBuilder extends BaseBuilder<MeasureReport> {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ca.uhn.fhir.cql.dstu3.builder.MeasureReportBuilder.class);
public MeasureReportBuilder() {
super(new MeasureReport());
}
public MeasureReportBuilder buildStatus(String status) {
try {
this.complexProperty.setStatus(MeasureReport.MeasureReportStatus.fromCode(status));
} catch (FHIRException e) {
ourLog.warn("Exception caught while attempting to set Status to '" + status + "', assuming status COMPLETE!"
+ System.lineSeparator() + e.getMessage());
this.complexProperty.setStatus(MeasureReport.MeasureReportStatus.COMPLETE);
}
return this;
}
public MeasureReportBuilder buildType(MeasureReport.MeasureReportType type) {
this.complexProperty.setType(type);
return this;
}
public MeasureReportBuilder buildType(String type) {
this.complexProperty.setType(MeasureReport.MeasureReportType.fromCode(type));
return this;
}
public MeasureReportBuilder buildMeasureReference(String measureRef) {
this.complexProperty.setMeasure(measureRef);
return this;
}
public MeasureReportBuilder buildPatientReference(String patientRef) {
this.complexProperty.setSubject(new Reference(patientRef));
return this;
}
public MeasureReportBuilder buildPeriod(Interval period) {
Object start = period.getStart();
if (start instanceof DateTime) {
this.complexProperty
.setPeriod(new Period().setStart(Date.from(((DateTime) start).getDateTime().toInstant()))
.setEnd(Date.from(((DateTime) period.getEnd()).getDateTime().toInstant())));
} else if (start instanceof Date) {
this.complexProperty
.setPeriod(new Period().setStart((Date) period.getStart()).setEnd((Date) period.getEnd()));
}
return this;
}
}

View File

@ -0,0 +1,731 @@
package ca.uhn.fhir.cql.r4.evaluation;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.cql.common.evaluation.MeasurePopulationType;
import ca.uhn.fhir.cql.common.evaluation.MeasureScoring;
import ca.uhn.fhir.cql.r4.builder.MeasureReportBuilder;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.ReferenceParam;
import org.cqframework.cql.elm.execution.ExpressionDef;
import org.cqframework.cql.elm.execution.FunctionDef;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Extension;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.ListResource;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType;
import org.opencds.cqf.cql.engine.data.DataProvider;
import org.opencds.cqf.cql.engine.execution.Context;
import org.opencds.cqf.cql.engine.execution.Variable;
import org.opencds.cqf.cql.engine.runtime.Code;
import org.opencds.cqf.cql.engine.runtime.Interval;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class MeasureEvaluation {
private static final Logger logger = LoggerFactory.getLogger(MeasureEvaluation.class);
private DataProvider provider;
private Interval measurementPeriod;
private DaoRegistry registry;
public MeasureEvaluation(DataProvider provider, DaoRegistry registry, Interval measurementPeriod) {
this.provider = provider;
this.registry = registry;
this.measurementPeriod = measurementPeriod;
}
public MeasureReport evaluatePatientMeasure(Measure measure, Context context, String patientId) {
logger.info("Generating individual report");
if (patientId == null) {
return evaluatePopulationMeasure(measure, context);
}
Iterable<Object> patientRetrieve = provider.retrieve("Patient", "id", patientId, "Patient", null, null, null,
null, null, null, null, null);
Patient patient = null;
if (patientRetrieve.iterator().hasNext()) {
patient = (Patient) patientRetrieve.iterator().next();
}
boolean isSingle = true;
return evaluate(measure, context,
patient == null ? Collections.emptyList() : Collections.singletonList(patient),
MeasureReport.MeasureReportType.INDIVIDUAL, isSingle);
}
public MeasureReport evaluateSubjectListMeasure(Measure measure, Context context, String practitionerRef) {
logger.info("Generating patient-list report");
List<Patient> patients = practitionerRef == null ? getAllPatients() : getPractitionerPatients(practitionerRef);
boolean isSingle = false;
return evaluate(measure, context, patients, MeasureReport.MeasureReportType.SUBJECTLIST, isSingle);
}
private List<Patient> getPractitionerPatients(String practitionerRef) {
SearchParameterMap map = new SearchParameterMap();
map.add("general-practitioner", new ReferenceParam(
practitionerRef.startsWith("Practitioner/") ? practitionerRef : "Practitioner/" + practitionerRef));
List<Patient> patients = new ArrayList<>();
IBundleProvider patientProvider = registry.getResourceDao("Patient").search(map);
List<IBaseResource> patientList = patientProvider.getResources(0, patientProvider.size());
patientList.forEach(x -> patients.add((Patient) x));
return patients;
}
private List<Patient> getAllPatients() {
List<Patient> patients = new ArrayList<>();
IBundleProvider patientProvider = registry.getResourceDao("Patient").search(new SearchParameterMap());
List<IBaseResource> patientList = patientProvider.getResources(0, patientProvider.size());
patientList.forEach(x -> patients.add((Patient) x));
return patients;
}
public MeasureReport evaluatePopulationMeasure(Measure measure, Context context) {
logger.info("Generating summary report");
boolean isSingle = false;
return evaluate(measure, context, getAllPatients(), MeasureReport.MeasureReportType.SUMMARY, isSingle);
}
@SuppressWarnings("unchecked")
private void clearExpressionCache(Context context) {
// Hack to clear expression cache
// See cqf-ruler github issue #153
try {
Field privateField = Context.class.getDeclaredField("expressions");
privateField.setAccessible(true);
LinkedHashMap<String, Object> expressions = (LinkedHashMap<String, Object>) privateField.get(context);
expressions.clear();
} catch (Exception e) {
logger.warn("Error resetting expression cache", e);
}
}
private Resource evaluateObservationCriteria(Context context, Patient patient, Resource resource, Measure.MeasureGroupPopulationComponent pop, MeasureReport report) {
if (pop == null || !pop.hasCriteria()) {
return null;
}
context.setContextValue("Patient", patient.getIdElement().getIdPart());
clearExpressionCache(context);
String observationName = pop.getCriteria().getExpression();
ExpressionDef ed = context.resolveExpressionRef(observationName);
if (!(ed instanceof FunctionDef)) {
throw new IllegalArgumentException(String.format("Measure observation %s does not reference a function definition", observationName));
}
Object result = null;
context.pushWindow();
try {
context.push(new Variable().withName(((FunctionDef)ed).getOperand().get(0).getName()).withValue(resource));
result = ed.getExpression().evaluate(context);
}
finally {
context.popWindow();
}
if (result instanceof Resource) {
return (Resource)result;
}
Observation obs = new Observation();
obs.setStatus(Observation.ObservationStatus.FINAL);
obs.setId(UUID.randomUUID().toString());
CodeableConcept cc = new CodeableConcept();
cc.setText(observationName);
obs.setCode(cc);
Extension obsExtension = new Extension().setUrl("http://hl7.org/fhir/StructureDefinition/cqf-measureInfo");
Extension extExtMeasure = new Extension()
.setUrl("measure")
.setValue(new CanonicalType("http://hl7.org/fhir/us/cqfmeasures/" + report.getMeasure()));
obsExtension.addExtension(extExtMeasure);
Extension extExtPop = new Extension()
.setUrl("populationId")
.setValue(new StringType(observationName));
obsExtension.addExtension(extExtPop);
obs.addExtension(obsExtension);
return obs;
}
@SuppressWarnings("unchecked")
private Iterable<Resource> evaluateCriteria(Context context, Patient patient,
Measure.MeasureGroupPopulationComponent pop) {
if (pop == null || !pop.hasCriteria()) {
return Collections.emptyList();
}
context.setContextValue("Patient", patient.getIdElement().getIdPart());
clearExpressionCache(context);
Object result = context.resolveExpressionRef(pop.getCriteria().getExpression()).evaluate(context);
if (result == null) {
return Collections.emptyList();
}
if (result instanceof Boolean) {
if (((Boolean) result)) {
return Collections.singletonList(patient);
} else {
return Collections.emptyList();
}
}
return (Iterable<Resource>) result;
}
private boolean evaluatePopulationCriteria(Context context, Patient patient,
Measure.MeasureGroupPopulationComponent criteria, HashMap<String, Resource> population,
HashMap<String, Patient> populationPatients, Measure.MeasureGroupPopulationComponent exclusionCriteria,
HashMap<String, Resource> exclusionPopulation, HashMap<String, Patient> exclusionPatients) {
boolean inPopulation = false;
if (criteria != null) {
for (Resource resource : evaluateCriteria(context, patient, criteria)) {
inPopulation = true;
population.put(resource.getIdElement().getIdPart(), resource);
}
}
if (inPopulation) {
// Are they in the exclusion?
if (exclusionCriteria != null) {
for (Resource resource : evaluateCriteria(context, patient, exclusionCriteria)) {
inPopulation = false;
exclusionPopulation.put(resource.getIdElement().getIdPart(), resource);
population.remove(resource.getIdElement().getIdPart());
}
}
}
if (inPopulation && populationPatients != null) {
populationPatients.put(patient.getIdElement().getIdPart(), patient);
}
if (!inPopulation && exclusionPatients != null) {
exclusionPatients.put(patient.getIdElement().getIdPart(), patient);
}
return inPopulation;
}
private void addPopulationCriteriaReport(MeasureReport report,
MeasureReport.MeasureReportGroupComponent reportGroup,
Measure.MeasureGroupPopulationComponent populationCriteria, int populationCount,
Iterable<Patient> patientPopulation) {
if (populationCriteria != null) {
MeasureReport.MeasureReportGroupPopulationComponent populationReport = new MeasureReport.MeasureReportGroupPopulationComponent();
populationReport.setCode(populationCriteria.getCode());
if (report.getType() == MeasureReport.MeasureReportType.SUBJECTLIST && patientPopulation != null) {
ListResource SUBJECTLIST = new ListResource();
SUBJECTLIST.setId(UUID.randomUUID().toString());
populationReport.setSubjectResults(new Reference().setReference("#" + SUBJECTLIST.getId()));
for (Patient patient : patientPopulation) {
ListResource.ListEntryComponent entry = new ListResource.ListEntryComponent()
.setItem(new Reference()
.setReference(patient.getIdElement().getIdPart().startsWith("Patient/")
? patient.getIdElement().getIdPart()
: String.format("Patient/%s", patient.getIdElement().getIdPart()))
.setDisplay(patient.getNameFirstRep().getNameAsSingleString()));
SUBJECTLIST.addEntry(entry);
}
report.addContained(SUBJECTLIST);
}
populationReport.setCount(populationCount);
reportGroup.addPopulation(populationReport);
}
}
private MeasureReport evaluate(Measure measure, Context context, List<Patient> patients,
MeasureReport.MeasureReportType type, boolean isSingle) {
MeasureReportBuilder reportBuilder = new MeasureReportBuilder();
reportBuilder.buildStatus("complete");
reportBuilder.buildType(type);
reportBuilder.buildMeasureReference(
measure.getIdElement().getResourceType() + "/" + measure.getIdElement().getIdPart());
if (type == MeasureReport.MeasureReportType.INDIVIDUAL && !patients.isEmpty()) {
IdType patientId = patients.get(0).getIdElement();
reportBuilder.buildPatientReference(patientId.getResourceType() + "/" + patientId.getIdPart());
}
if (measurementPeriod != null) {
reportBuilder.buildPeriod(measurementPeriod);
}
MeasureReport report = reportBuilder.build();
HashMap<String, Resource> resources = new HashMap<>();
HashMap<String, HashSet<String>> codeToResourceMap = new HashMap<>();
Set<String> evaluatedResourcesList = new HashSet<>();
MeasureScoring measureScoring = MeasureScoring.fromCode(measure.getScoring().getCodingFirstRep().getCode());
if (measureScoring == null) {
throw new RuntimeException("Measure scoring is required in order to calculate.");
}
List<Measure.MeasureSupplementalDataComponent> sde = new ArrayList<>();
HashMap<String, HashMap<String, Integer>> sdeAccumulators = null;
for (Measure.MeasureGroupComponent group : measure.getGroup()) {
MeasureReport.MeasureReportGroupComponent reportGroup = new MeasureReport.MeasureReportGroupComponent();
reportGroup.setId(group.getId());
report.getGroup().add(reportGroup);
// Declare variables to avoid a hash lookup on every patient
// TODO: Isn't quite right, there may be multiple initial populations for a
// ratio measure...
Measure.MeasureGroupPopulationComponent initialPopulationCriteria = null;
Measure.MeasureGroupPopulationComponent numeratorCriteria = null;
Measure.MeasureGroupPopulationComponent numeratorExclusionCriteria = null;
Measure.MeasureGroupPopulationComponent denominatorCriteria = null;
Measure.MeasureGroupPopulationComponent denominatorExclusionCriteria = null;
Measure.MeasureGroupPopulationComponent denominatorExceptionCriteria = null;
Measure.MeasureGroupPopulationComponent measurePopulationCriteria = null;
Measure.MeasureGroupPopulationComponent measurePopulationExclusionCriteria = null;
// TODO: Isn't quite right, there may be multiple measure observations...
Measure.MeasureGroupPopulationComponent measureObservationCriteria = null;
HashMap<String, Resource> initialPopulation = null;
HashMap<String, Resource> numerator = null;
HashMap<String, Resource> numeratorExclusion = null;
HashMap<String, Resource> denominator = null;
HashMap<String, Resource> denominatorExclusion = null;
HashMap<String, Resource> denominatorException = null;
HashMap<String, Resource> measurePopulation = null;
HashMap<String, Resource> measurePopulationExclusion = null;
HashMap<String, Resource> measureObservation = null;
HashMap<String, Patient> initialPopulationPatients = null;
HashMap<String, Patient> numeratorPatients = null;
HashMap<String, Patient> numeratorExclusionPatients = null;
HashMap<String, Patient> denominatorPatients = null;
HashMap<String, Patient> denominatorExclusionPatients = null;
HashMap<String, Patient> denominatorExceptionPatients = null;
HashMap<String, Patient> measurePopulationPatients = null;
HashMap<String, Patient> measurePopulationExclusionPatients = null;
sdeAccumulators = new HashMap<>();
sde = measure.getSupplementalData();
for (Measure.MeasureGroupPopulationComponent pop : group.getPopulation()) {
MeasurePopulationType populationType = MeasurePopulationType
.fromCode(pop.getCode().getCodingFirstRep().getCode());
if (populationType != null) {
switch (populationType) {
case INITIALPOPULATION:
initialPopulationCriteria = pop;
initialPopulation = new HashMap<>();
if (type == MeasureReport.MeasureReportType.SUBJECTLIST) {
initialPopulationPatients = new HashMap<>();
}
break;
case NUMERATOR:
numeratorCriteria = pop;
numerator = new HashMap<>();
if (type == MeasureReport.MeasureReportType.SUBJECTLIST) {
numeratorPatients = new HashMap<>();
}
break;
case NUMERATOREXCLUSION:
numeratorExclusionCriteria = pop;
numeratorExclusion = new HashMap<>();
if (type == MeasureReport.MeasureReportType.SUBJECTLIST) {
numeratorExclusionPatients = new HashMap<>();
}
break;
case DENOMINATOR:
denominatorCriteria = pop;
denominator = new HashMap<>();
if (type == MeasureReport.MeasureReportType.SUBJECTLIST) {
denominatorPatients = new HashMap<>();
}
break;
case DENOMINATOREXCLUSION:
denominatorExclusionCriteria = pop;
denominatorExclusion = new HashMap<>();
if (type == MeasureReport.MeasureReportType.SUBJECTLIST) {
denominatorExclusionPatients = new HashMap<>();
}
break;
case DENOMINATOREXCEPTION:
denominatorExceptionCriteria = pop;
denominatorException = new HashMap<>();
if (type == MeasureReport.MeasureReportType.SUBJECTLIST) {
denominatorExceptionPatients = new HashMap<>();
}
break;
case MEASUREPOPULATION:
measurePopulationCriteria = pop;
measurePopulation = new HashMap<>();
if (type == MeasureReport.MeasureReportType.SUBJECTLIST) {
measurePopulationPatients = new HashMap<>();
}
break;
case MEASUREPOPULATIONEXCLUSION:
measurePopulationExclusionCriteria = pop;
measurePopulationExclusion = new HashMap<>();
if (type == MeasureReport.MeasureReportType.SUBJECTLIST) {
measurePopulationExclusionPatients = new HashMap<>();
}
break;
case MEASUREOBSERVATION:
measureObservationCriteria = pop;
measureObservation = new HashMap<>();
break;
}
}
}
switch (measureScoring) {
case PROPORTION:
case RATIO: {
// For each patient in the initial population
for (Patient patient : patients) {
// Are they in the initial population?
boolean inInitialPopulation = evaluatePopulationCriteria(context, patient,
initialPopulationCriteria, initialPopulation, initialPopulationPatients, null, null,
null);
populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources,
codeToResourceMap);
if (inInitialPopulation) {
// Are they in the denominator?
boolean inDenominator = evaluatePopulationCriteria(context, patient, denominatorCriteria,
denominator, denominatorPatients, denominatorExclusionCriteria,
denominatorExclusion, denominatorExclusionPatients);
populateResourceMap(context, MeasurePopulationType.DENOMINATOR, resources,
codeToResourceMap);
if (inDenominator) {
// Are they in the numerator?
boolean inNumerator = evaluatePopulationCriteria(context, patient, numeratorCriteria,
numerator, numeratorPatients, numeratorExclusionCriteria, numeratorExclusion,
numeratorExclusionPatients);
populateResourceMap(context, MeasurePopulationType.NUMERATOR, resources,
codeToResourceMap);
if (!inNumerator && inDenominator && (denominatorExceptionCriteria != null)) {
// Are they in the denominator exception?
boolean inException = false;
for (Resource resource : evaluateCriteria(context, patient,
denominatorExceptionCriteria)) {
inException = true;
denominatorException.put(resource.getIdElement().getIdPart(), resource);
denominator.remove(resource.getIdElement().getIdPart());
populateResourceMap(context, MeasurePopulationType.DENOMINATOREXCEPTION,
resources, codeToResourceMap);
}
if (inException) {
if (denominatorExceptionPatients != null) {
denominatorExceptionPatients.put(patient.getIdElement().getIdPart(),
patient);
}
if (denominatorPatients != null) {
denominatorPatients.remove(patient.getIdElement().getIdPart());
}
}
}
}
}
populateSDEAccumulators(measure, context, patient, sdeAccumulators, sde);
}
// Calculate actual measure score, Count(numerator) / Count(denominator)
if (denominator != null && numerator != null && denominator.size() > 0) {
reportGroup.setMeasureScore(new Quantity(numerator.size() / (double) denominator.size()));
}
break;
}
case CONTINUOUSVARIABLE: {
// For each patient in the patient list
for (Patient patient : patients) {
// Are they in the initial population?
boolean inInitialPopulation = evaluatePopulationCriteria(context, patient,
initialPopulationCriteria, initialPopulation, initialPopulationPatients, null, null,
null);
populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources,
codeToResourceMap);
if (inInitialPopulation) {
// Are they in the measure population?
boolean inMeasurePopulation = evaluatePopulationCriteria(context, patient,
measurePopulationCriteria, measurePopulation, measurePopulationPatients,
measurePopulationExclusionCriteria, measurePopulationExclusion,
measurePopulationExclusionPatients);
if (inMeasurePopulation) {
for (Resource resource : measurePopulation.values()) {
Resource observation = evaluateObservationCriteria(context, patient, resource, measureObservationCriteria, report);
measureObservation.put(resource.getIdElement().getIdPart(), observation);
report.addContained(observation);
report.getEvaluatedResource().add(new Reference("#" + observation.getId()));
}
}
}
populateSDEAccumulators(measure, context, patient, sdeAccumulators,sde);
}
break;
}
case COHORT: {
// For each patient in the patient list
for (Patient patient : patients) {
evaluatePopulationCriteria(context, patient,
initialPopulationCriteria, initialPopulation, initialPopulationPatients, null, null,
null);
populateResourceMap(context, MeasurePopulationType.INITIALPOPULATION, resources,
codeToResourceMap);
populateSDEAccumulators(measure, context, patient, sdeAccumulators, sde);
}
break;
}
}
// Add population reports for each group
addPopulationCriteriaReport(report, reportGroup, initialPopulationCriteria,
initialPopulation != null ? initialPopulation.size() : 0,
initialPopulationPatients != null ? initialPopulationPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, numeratorCriteria,
numerator != null ? numerator.size() : 0,
numeratorPatients != null ? numeratorPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, numeratorExclusionCriteria,
numeratorExclusion != null ? numeratorExclusion.size() : 0,
numeratorExclusionPatients != null ? numeratorExclusionPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, denominatorCriteria,
denominator != null ? denominator.size() : 0,
denominatorPatients != null ? denominatorPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, denominatorExclusionCriteria,
denominatorExclusion != null ? denominatorExclusion.size() : 0,
denominatorExclusionPatients != null ? denominatorExclusionPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, denominatorExceptionCriteria,
denominatorException != null ? denominatorException.size() : 0,
denominatorExceptionPatients != null ? denominatorExceptionPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, measurePopulationCriteria,
measurePopulation != null ? measurePopulation.size() : 0,
measurePopulationPatients != null ? measurePopulationPatients.values() : null);
addPopulationCriteriaReport(report, reportGroup, measurePopulationExclusionCriteria,
measurePopulationExclusion != null ? measurePopulationExclusion.size() : 0,
measurePopulationExclusionPatients != null ? measurePopulationExclusionPatients.values() : null);
// TODO: Measure Observations...
}
for (String key : codeToResourceMap.keySet()) {
org.hl7.fhir.r4.model.ListResource list = new org.hl7.fhir.r4.model.ListResource();
for (String element : codeToResourceMap.get(key)) {
org.hl7.fhir.r4.model.ListResource.ListEntryComponent comp = new org.hl7.fhir.r4.model.ListResource.ListEntryComponent();
comp.setItem(new Reference('#' + element));
list.addEntry(comp);
}
if (!list.isEmpty()) {
list.setId("List/" + UUID.randomUUID().toString());
list.setTitle(key);
resources.put(list.getId(), list);
list.getEntry().forEach(listResource -> evaluatedResourcesList.add(listResource.getItem().getReference()));
}
}
if (!evaluatedResourcesList.isEmpty()) {
List<Reference> evaluatedResourceIds = new ArrayList<>();
evaluatedResourcesList.forEach((resource) -> {
evaluatedResourceIds.add(new Reference(resource));
});
report.setEvaluatedResource(evaluatedResourceIds);
}
if (sdeAccumulators.size() > 0) {
report = processAccumulators(report, sdeAccumulators, sde, isSingle, patients);
}
return report;
}
private void populateSDEAccumulators(Measure measure, Context context, Patient patient,HashMap<String, HashMap<String, Integer>> sdeAccumulators,
List<Measure.MeasureSupplementalDataComponent> sde){
context.setContextValue("Patient", patient.getIdElement().getIdPart());
List<Object> sdeList = sde.stream().map(sdeItem -> context.resolveExpressionRef(sdeItem.getCriteria().getExpression()).evaluate(context)).collect(Collectors.toList());
if(!sdeList.isEmpty()) {
for (int i = 0; i < sdeList.size(); i++) {
Object sdeListItem = sdeList.get(i);
if(null != sdeListItem) {
String sdeAccumulatorKey = sde.get(i).getCode().getText();
if(null == sdeAccumulatorKey || sdeAccumulatorKey.length() < 1){
sdeAccumulatorKey = sde.get(i).getCriteria().getExpression();
}
HashMap<String, Integer> sdeItemMap = sdeAccumulators.get(sdeAccumulatorKey);
String code = "";
switch (sdeListItem.getClass().getSimpleName()) {
case "Code":
code = ((Code) sdeListItem).getCode();
break;
case "ArrayList":
if (((ArrayList<?>) sdeListItem).size() > 0) {
if (((ArrayList<?>) sdeListItem).get(0).getClass().getSimpleName().equals("Coding")) {
code = ((Coding) ((ArrayList<?>) sdeListItem).get(0)).getCode();
} else {
continue;
}
}else{
continue;
}
break;
}
if(null == code){
continue;
}
if (null != sdeItemMap && null != sdeItemMap.get(code)) {
Integer sdeItemValue = sdeItemMap.get(code);
sdeItemValue++;
sdeItemMap.put(code, sdeItemValue);
sdeAccumulators.get(sdeAccumulatorKey).put(code, sdeItemValue);
} else {
if (null == sdeAccumulators.get(sdeAccumulatorKey)) {
HashMap<String, Integer> newSDEItem = new HashMap<>();
newSDEItem.put(code, 1);
sdeAccumulators.put(sdeAccumulatorKey, newSDEItem);
} else {
sdeAccumulators.get(sdeAccumulatorKey).put(code, 1);
}
}
}
}
}
}
private MeasureReport processAccumulators(MeasureReport report, HashMap<String, HashMap<String, Integer>> sdeAccumulators,
List<Measure.MeasureSupplementalDataComponent> sde, boolean isSingle, List<Patient> patients){
List<Reference> newRefList = new ArrayList<>();
sdeAccumulators.forEach((sdeKey, sdeAccumulator) -> {
sdeAccumulator.forEach((sdeAccumulatorKey, sdeAccumulatorValue)->{
Observation obs = new Observation();
obs.setStatus(Observation.ObservationStatus.FINAL);
obs.setId(UUID.randomUUID().toString());
Coding valueCoding = new Coding();
if(sdeKey.equalsIgnoreCase("sde-sex")){
valueCoding.setCode(sdeAccumulatorKey);
}else {
String coreCategory = sdeKey.substring(sdeKey.lastIndexOf('-') >= 0 ? sdeKey.lastIndexOf('-') : 0);
patients.forEach((pt)-> {
pt.getExtension().forEach((ptExt) -> {
if (ptExt.getUrl().contains(coreCategory)) {
String code = ((Coding) ptExt.getExtension().get(0).getValue()).getCode();
if(code.equalsIgnoreCase(sdeAccumulatorKey)) {
valueCoding.setSystem(((Coding) ptExt.getExtension().get(0).getValue()).getSystem());
valueCoding.setCode(code);
valueCoding.setDisplay(((Coding) ptExt.getExtension().get(0).getValue()).getDisplay());
}
}
});
});
}
CodeableConcept obsCodeableConcept = new CodeableConcept();
Extension obsExtension = new Extension().setUrl("http://hl7.org/fhir/StructureDefinition/cqf-measureInfo");
Extension extExtMeasure = new Extension()
.setUrl("measure")
.setValue(new CanonicalType("http://hl7.org/fhir/us/cqfmeasures/" + report.getMeasure()));
obsExtension.addExtension(extExtMeasure);
Extension extExtPop = new Extension()
.setUrl("populationId")
.setValue(new StringType(sdeKey));
obsExtension.addExtension(extExtPop);
obs.addExtension(obsExtension);
obs.setValue(new IntegerType(sdeAccumulatorValue));
if(!isSingle) {
valueCoding.setCode(sdeAccumulatorKey);
obsCodeableConcept.setCoding(Collections.singletonList(valueCoding));
obs.setCode(obsCodeableConcept);
}else{
obs.setCode(new CodeableConcept().setText(sdeKey));
obsCodeableConcept.setCoding(Collections.singletonList(valueCoding));
obs.setValue(obsCodeableConcept);
}
newRefList.add(new Reference("#" + obs.getId()));
report.addContained(obs);
});
});
newRefList.addAll(report.getEvaluatedResource());
report.setEvaluatedResource(newRefList);
return report;
}
private void populateResourceMap(Context context, MeasurePopulationType type, HashMap<String, Resource> resources,
HashMap<String, HashSet<String>> codeToResourceMap) {
if (context.getEvaluatedResources().isEmpty()) {
return;
}
if (!codeToResourceMap.containsKey(type.toCode())) {
codeToResourceMap.put(type.toCode(), new HashSet<>());
}
HashSet<String> codeHashSet = codeToResourceMap.get((type.toCode()));
for (Object o : context.getEvaluatedResources()) {
if (o instanceof Resource) {
Resource r = (Resource) o;
String id = (r.getIdElement().getResourceType() != null ? (r.getIdElement().getResourceType() + "/")
: "") + r.getIdElement().getIdPart();
if (!codeHashSet.contains(id)) {
codeHashSet.add(id);
}
if (!resources.containsKey(id)) {
resources.put(id, r);
}
}
}
context.clearEvaluatedResources();
}
}

View File

@ -0,0 +1,137 @@
package ca.uhn.fhir.cql.r4.evaluation;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.cql.common.helper.DateHelper;
import ca.uhn.fhir.cql.common.helper.UsingHelper;
import ca.uhn.fhir.cql.common.provider.EvaluationProviderFactory;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.cql.r4.helper.LibraryHelper;
import org.apache.commons.lang3.tuple.Triple;
import org.cqframework.cql.elm.execution.Library;
import org.hl7.fhir.r4.model.Measure;
import org.opencds.cqf.cql.engine.data.DataProvider;
import org.opencds.cqf.cql.engine.debug.DebugMap;
import org.opencds.cqf.cql.engine.execution.Context;
import org.opencds.cqf.cql.engine.execution.LibraryLoader;
import org.opencds.cqf.cql.engine.runtime.DateTime;
import org.opencds.cqf.cql.engine.runtime.Interval;
import org.opencds.cqf.cql.engine.terminology.TerminologyProvider;
import java.util.Date;
import java.util.List;
public class MeasureEvaluationSeed {
private Measure measure;
private Context context;
private Interval measurementPeriod;
private LibraryLoader libraryLoader;
private LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider;
private EvaluationProviderFactory providerFactory;
private DataProvider dataProvider;
private LibraryHelper libraryHelper;
public MeasureEvaluationSeed(EvaluationProviderFactory providerFactory, LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider, LibraryHelper libraryHelper) {
this.providerFactory = providerFactory;
this.libraryLoader = libraryLoader;
this.libraryResourceProvider = libraryResourceProvider;
this.libraryHelper = libraryHelper;
}
public Measure getMeasure() {
return this.measure;
}
public Context getContext() {
return this.context;
}
public Interval getMeasurementPeriod() {
return this.measurementPeriod;
}
public DataProvider getDataProvider() {
return this.dataProvider;
}
public void setup(Measure measure, String periodStart, String periodEnd, String productLine, String source,
String user, String pass) {
this.measure = measure;
this.libraryHelper.loadLibraries(measure, this.libraryLoader, this.libraryResourceProvider);
// resolve primary library
Library library = this.libraryHelper.resolvePrimaryLibrary(measure, libraryLoader, this.libraryResourceProvider);
// resolve execution context
context = new Context(library);
context.registerLibraryLoader(libraryLoader);
List<Triple<String, String, String>> usingDefs = UsingHelper.getUsingUrlAndVersion(library.getUsings());
if (usingDefs.size() > 1) {
throw new IllegalArgumentException(
"Evaluation of Measure using multiple Models is not supported at this time.");
}
// If there are no Usings, there is probably not any place the Terminology
// actually used so I think the assumption that at least one provider exists is
// ok.
TerminologyProvider terminologyProvider = null;
if (usingDefs.size() > 0) {
// Creates a terminology provider based on the first using statement. This
// assumes the terminology
// server matches the FHIR version of the CQL.
terminologyProvider = this.providerFactory.createTerminologyProvider(usingDefs.get(0).getLeft(),
usingDefs.get(0).getMiddle(), source, user, pass);
context.registerTerminologyProvider(terminologyProvider);
}
for (Triple<String, String, String> def : usingDefs) {
this.dataProvider = this.providerFactory.createDataProvider(def.getLeft(), def.getMiddle(),
terminologyProvider);
context.registerDataProvider(def.getRight(), dataProvider);
}
if (periodStart != null && periodEnd != null) {
// resolve the measurement period
measurementPeriod = new Interval(DateHelper.resolveRequestDate(periodStart), true,
DateHelper.resolveRequestDate(periodEnd), true);
context.setParameter(null, "Measurement Period",
new Interval(DateTime.fromJavaDate((Date) measurementPeriod.getStart()), true,
DateTime.fromJavaDate((Date) measurementPeriod.getEnd()), true));
}
if (productLine != null) {
context.setParameter(null, "Product Line", productLine);
}
context.setExpressionCaching(true);
// This needs to be made configurable
DebugMap debugMap = new DebugMap();
debugMap.setIsLoggingEnabled(true);
context.setDebugMap(debugMap);
}
}

View File

@ -0,0 +1,84 @@
package ca.uhn.fhir.cql.r4.evaluation;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.cql.common.provider.EvaluationProviderFactory;
import ca.uhn.fhir.cql.common.retrieve.JpaFhirRetrieveProvider;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import org.opencds.cqf.cql.engine.data.CompositeDataProvider;
import org.opencds.cqf.cql.engine.data.DataProvider;
import org.opencds.cqf.cql.engine.fhir.searchparam.SearchParameterResolver;
import org.opencds.cqf.cql.engine.model.ModelResolver;
import org.opencds.cqf.cql.engine.terminology.TerminologyProvider;
import org.opencds.cqf.cql.evaluator.engine.terminology.PrivateCachingTerminologyProviderDecorator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// This class is a relatively dumb factory for data providers. It supports only
// creating JPA providers for FHIR, and only basic auth for terminology
@Component
public class ProviderFactory implements EvaluationProviderFactory {
DaoRegistry registry;
TerminologyProvider defaultTerminologyProvider;
FhirContext fhirContext;
ModelResolver fhirModelResolver;
@Autowired
public ProviderFactory(FhirContext fhirContext, DaoRegistry registry,
TerminologyProvider defaultTerminologyProvider, ModelResolver fhirModelResolver) {
this.defaultTerminologyProvider = defaultTerminologyProvider;
this.registry = registry;
this.fhirContext = fhirContext;
this.fhirModelResolver = fhirModelResolver;
}
public DataProvider createDataProvider(String model, String version) {
return this.createDataProvider(model, version, null, null, null);
}
public DataProvider createDataProvider(String model, String version, String url, String user, String pass) {
TerminologyProvider terminologyProvider = this.createTerminologyProvider(model, version, url, user, pass);
return this.createDataProvider(model, version, terminologyProvider);
}
public DataProvider createDataProvider(String model, String version, TerminologyProvider terminologyProvider) {
if (model.equals("FHIR") && version.startsWith("4")) {
JpaFhirRetrieveProvider retrieveProvider = new JpaFhirRetrieveProvider(this.registry,
new SearchParameterResolver(this.fhirContext));
retrieveProvider.setTerminologyProvider(terminologyProvider);
retrieveProvider.setExpandValueSets(true);
return new CompositeDataProvider(this.fhirModelResolver, retrieveProvider);
}
throw new IllegalArgumentException(
String.format("Can't construct a data provider for model %s version %s", model, version));
}
public TerminologyProvider createTerminologyProvider(String model, String version, String url, String user,
String pass) {
TerminologyProvider terminologyProvider = null;
terminologyProvider = this.defaultTerminologyProvider;
return new PrivateCachingTerminologyProviderDecorator(terminologyProvider);
}
}

View File

@ -0,0 +1,49 @@
package ca.uhn.fhir.cql.r4.helper;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.hl7.fhir.r4.model.CanonicalType;
public class CanonicalHelper {
public static String getId(CanonicalType canonical) {
if (canonical.hasValue()) {
String id = canonical.getValue();
String temp = id.contains("/") ? id.substring(id.lastIndexOf("/") + 1) : id;
return temp.split("\\|")[0];
}
throw new RuntimeException("CanonicalType must have a value for id extraction");
}
public static String getResourceName(CanonicalType canonical) {
if (canonical.hasValue()) {
String id = canonical.getValue();
if (id.contains("/")) {
id = id.replace(id.substring(id.lastIndexOf("/")), "");
return id.contains("/") ? id.substring(id.lastIndexOf("/") + 1) : id;
}
return null;
}
throw new RuntimeException("CanonicalType must have a value for resource name extraction");
}
}

View File

@ -0,0 +1,237 @@
package ca.uhn.fhir.cql.r4.helper;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.cql.common.evaluation.LibraryLoader;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.cql.common.provider.LibrarySourceProvider;
import org.cqframework.cql.cql2elm.LibraryManager;
import org.cqframework.cql.cql2elm.ModelManager;
import org.cqframework.cql.cql2elm.model.Model;
import org.cqframework.cql.elm.execution.Library;
import org.cqframework.cql.elm.execution.VersionedIdentifier;
import org.hl7.fhir.r4.model.Attachment;
import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.Coding;
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.Resource;
import org.opencds.cqf.cql.evaluator.cql2elm.model.CacheAwareModelManager;
import org.opencds.cqf.cql.evaluator.engine.execution.PrivateCachingLibraryLoaderDecorator;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Created by Christopher on 1/11/2017.
*/
public class LibraryHelper {
private Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache;
public LibraryHelper(Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache) {
this.modelCache = modelCache;
}
public org.opencds.cqf.cql.engine.execution.LibraryLoader createLibraryLoader(LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> provider) {
ModelManager modelManager = new CacheAwareModelManager(this.modelCache);
LibraryManager libraryManager = new LibraryManager(modelManager);
libraryManager.getLibrarySourceLoader().clearProviders();
libraryManager.getLibrarySourceLoader().registerProvider(
new LibrarySourceProvider<org.hl7.fhir.r4.model.Library, Attachment>(provider,
x -> x.getContent(), x -> x.getContentType(), x -> x.getData()));
return new PrivateCachingLibraryLoaderDecorator(new LibraryLoader(libraryManager, modelManager));
}
public org.opencds.cqf.cql.engine.execution.LibraryLoader createLibraryLoader(org.cqframework.cql.cql2elm.LibrarySourceProvider provider) {
ModelManager modelManager = new CacheAwareModelManager(this.modelCache);
LibraryManager libraryManager = new LibraryManager(modelManager);
libraryManager.getLibrarySourceLoader().clearProviders();
libraryManager.getLibrarySourceLoader().registerProvider(provider);
return new PrivateCachingLibraryLoaderDecorator(new LibraryLoader(libraryManager, modelManager));
}
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
if (reference.startsWith("Library/") || !reference.contains("/")) {
return libraryResourceProvider.resolveLibraryById(reference.replace("Library/", ""));
}
// Full url (e.g. http://hl7.org/fhir/us/Library/FHIRHelpers)
else if (reference.contains(("/Library/"))) {
return libraryResourceProvider.resolveLibraryByCanonicalUrl(reference);
}
return null;
}
public List<org.cqframework.cql.elm.execution.Library> loadLibraries(Measure measure,
org.opencds.cqf.cql.engine.execution.LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider) {
List<org.cqframework.cql.elm.execution.Library> libraries = new ArrayList<org.cqframework.cql.elm.execution.Library>();
// load libraries
//TODO: if there's a bad measure argument, this blows up for an obscure error
org.hl7.fhir.r4.model.Library primaryLibrary = null;
for (CanonicalType ref : measure.getLibrary()) {
// if library is contained in measure, load it into server
String id = ref.getValue(); //CanonicalHelper.getId(ref);
if (id.startsWith("#")) {
id = id.substring(1);
for (Resource resource : measure.getContained()) {
if (resource instanceof org.hl7.fhir.r4.model.Library
&& resource.getIdElement().getIdPart().equals(id)) {
libraryResourceProvider.update((org.hl7.fhir.r4.model.Library) resource);
}
}
}
// We just loaded it into the server so we can access it by Id
org.hl7.fhir.r4.model.Library library = resolveLibraryReference(libraryResourceProvider, id);
if (primaryLibrary == null) {
primaryLibrary = library;
}
if (library != null && isLogicLibrary(library)) {
libraries.add(
libraryLoader.load(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion()))
);
}
}
if (libraries.isEmpty()) {
throw new IllegalArgumentException(String
.format("Could not load library source for libraries referenced in %s.", measure.getId()));
}
//VersionedIdentifier primaryLibraryId = libraries.get(0).getIdentifier();
//org.hl7.fhir.r4.model.Library primaryLibrary = libraryResourceProvider.resolveLibraryByName(primaryLibraryId.getId(), primaryLibraryId.getVersion());
for (RelatedArtifact artifact : primaryLibrary.getRelatedArtifact()) {
if (artifact.hasType() && artifact.getType().equals(RelatedArtifact.RelatedArtifactType.DEPENDSON) && artifact.hasResource()) {
org.hl7.fhir.r4.model.Library library = null;
library = resolveLibraryReference(libraryResourceProvider, artifact.getResource());
if (library != null && isLogicLibrary(library)) {
libraries.add(
libraryLoader.load(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion()))
);
}
}
}
return libraries;
}
private boolean isLogicLibrary(org.hl7.fhir.r4.model.Library library) {
if (library == null) {
return false;
}
if (!library.hasType()) {
// If no type is specified, assume it is a logic library based on whether there is a CQL content element.
if (library.hasContent()) {
for (Attachment a : library.getContent()) {
if (a.hasContentType() && (a.getContentType().equals("text/cql")
|| a.getContentType().equals("application/elm+xml")
|| a.getContentType().equals("application/elm+json"))) {
return true;
}
}
}
return false;
}
if (!library.getType().hasCoding()) {
return false;
}
for (Coding c : library.getType().getCoding()) {
if (c.hasSystem() && c.getSystem().equals("http://terminology.hl7.org/CodeSystem/library-type")
&& c.hasCode() && c.getCode().equals("logic-library")) {
return true;
}
}
return false;
}
public Library resolveLibraryById(String libraryId,
org.opencds.cqf.cql.engine.execution.LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider) {
// Library library = null;
org.hl7.fhir.r4.model.Library fhirLibrary = libraryResourceProvider.resolveLibraryById(libraryId);
return libraryLoader
.load(new VersionedIdentifier().withId(fhirLibrary.getName()).withVersion(fhirLibrary.getVersion()));
// for (Library l : libraryLoader.getLibraries()) {
// VersionedIdentifier vid = l.getIdentifier();
// if (vid.getId().equals(fhirLibrary.getName()) &&
// LibraryResourceHelper.compareVersions(fhirLibrary.getVersion(),
// vid.getVersion()) == 0) {
// library = l;
// break;
// }
// }
// if (library == null) {
// }
// return library;
}
public Library resolvePrimaryLibrary(Measure measure,
org.opencds.cqf.cql.engine.execution.LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider) {
// default is the first library reference
String id = CanonicalHelper.getId(measure.getLibrary().get(0));
Library library = resolveLibraryById(id, libraryLoader, libraryResourceProvider);
if (library == null) {
throw new IllegalArgumentException(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;
}
}

View File

@ -0,0 +1,149 @@
package ca.uhn.fhir.cql.r4.provider;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport.LookupCodeResult;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
import ca.uhn.fhir.jpa.rp.r4.ValueSetResourceProvider;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.term.api.ITermReadSvcR4;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.ValueSet;
import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent;
import org.opencds.cqf.cql.engine.runtime.Code;
import org.opencds.cqf.cql.engine.terminology.CodeSystemInfo;
import org.opencds.cqf.cql.engine.terminology.TerminologyProvider;
import org.opencds.cqf.cql.engine.terminology.ValueSetInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class JpaTerminologyProvider implements TerminologyProvider {
private ITermReadSvcR4 terminologySvc;
private ValueSetResourceProvider valueSetResourceProvider;
private final IValidationSupport validationSupport;
@Autowired
public JpaTerminologyProvider(ITermReadSvcR4 terminologySvc,
ValueSetResourceProvider valueSetResourceProvider, IValidationSupport validationSupport) {
this.terminologySvc = terminologySvc;
this.valueSetResourceProvider = valueSetResourceProvider;
this.validationSupport = validationSupport;
}
@Override
public boolean in(Code code, ValueSetInfo valueSet) throws ResourceNotFoundException {
for (Code c : expand(valueSet)) {
if (c == null)
continue;
if (c.getCode().equals(code.getCode()) && c.getSystem().equals(code.getSystem())) {
return true;
}
}
return false;
}
@Override
public Iterable<Code> expand(ValueSetInfo valueSet) throws ResourceNotFoundException {
List<Code> codes = new ArrayList<>();
boolean needsExpand = false;
ValueSet vs;
if (valueSet.getId().startsWith("http://") || valueSet.getId().startsWith("https://")) {
if (valueSet.getVersion() != null
|| (valueSet.getCodeSystems() != null && valueSet.getCodeSystems().size() > 0)) {
if (!(valueSet.getCodeSystems().size() == 1 && valueSet.getCodeSystems().get(0).getVersion() == null)) {
throw new UnsupportedOperationException(String.format(
"Could not expand value set %s; version and code system bindings are not supported at this time.",
valueSet.getId()));
}
}
IBundleProvider bundleProvider = valueSetResourceProvider.getDao()
.search(new SearchParameterMap().add(ValueSet.SP_URL, new UriParam(valueSet.getId())));
List<IBaseResource> valueSets = bundleProvider.getResources(0, bundleProvider.size());
if (valueSets.isEmpty()) {
throw new IllegalArgumentException(String.format("Could not resolve value set %s.", valueSet.getId()));
} else if (valueSets.size() == 1) {
vs = (ValueSet) valueSets.get(0);
} else {
throw new IllegalArgumentException("Found more than 1 ValueSet with url: " + valueSet.getId());
}
} else {
vs = valueSetResourceProvider.getDao().read(new IdType(valueSet.getId()));
}
if (vs != null) {
if (vs.hasCompose()) {
if (vs.getCompose().hasInclude()) {
for (ValueSet.ConceptSetComponent include : vs.getCompose().getInclude()) {
if (include.hasValueSet() || include.hasFilter()) {
needsExpand = true;
break;
}
for (ValueSet.ConceptReferenceComponent concept : include.getConcept()) {
if (concept.hasCode()) {
codes.add(new Code().withCode(concept.getCode()).withSystem(include.getSystem()));
}
}
}
if (!needsExpand) {
return codes;
}
}
}
if (vs.hasExpansion() && vs.getExpansion().hasContains()) {
for (ValueSetExpansionContainsComponent vsecc : vs.getExpansion().getContains()) {
codes.add(new Code().withCode(vsecc.getCode()).withSystem(vsecc.getSystem()));
}
return codes;
}
}
ValueSet expansion = terminologySvc
.expandValueSet(new ValueSetExpansionOptions().setCount(Integer.MAX_VALUE), valueSet.getId(), null);
expansion.getExpansion().getContains()
.forEach(concept -> codes.add(new Code().withCode(concept.getCode()).withSystem(concept.getSystem())));
return codes;
}
@Override
public Code lookup(Code code, CodeSystemInfo codeSystem) throws ResourceNotFoundException {
LookupCodeResult cs = terminologySvc.lookupCode(new ValidationSupportContext(validationSupport), codeSystem.getId(), code.getCode());
code.setDisplay(cs.getCodeDisplay());
code.setSystem(codeSystem.getId());
return code;
}
}

View File

@ -0,0 +1,125 @@
package ca.uhn.fhir.cql.r4.provider;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.param.UriParam;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Library;
import org.opencds.cqf.cql.engine.terminology.TerminologyProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@Component
public class LibraryResolutionProviderImpl implements LibraryResolutionProvider<Library> {
@Autowired
private IFhirResourceDao<Library> myLibraryDao;
@Autowired
DaoRegistry registry;
@Autowired
TerminologyProvider defaultTerminologyProvider;
// TODO: Figure out if we should throw an exception or something here.
@Override
public void update(Library library) {
myLibraryDao.update(library);
}
@Override
public Library resolveLibraryById(String libraryId) {
try {
return myLibraryDao.read(new IdType(libraryId));
} catch (Exception e) {
throw new IllegalArgumentException(String.format("Could not resolve library id %s", libraryId));
}
}
@Override
public Library resolveLibraryByName(String libraryName, String libraryVersion) {
Iterable<org.hl7.fhir.r4.model.Library> libraries = getLibrariesByName(libraryName);
org.hl7.fhir.r4.model.Library library = LibraryResolutionProvider.selectFromList(libraries, libraryVersion,
x -> x.getVersion());
if (library == null) {
throw new IllegalArgumentException(String.format("Could not resolve library name %s", libraryName));
}
return library;
}
@Override
public Library resolveLibraryByCanonicalUrl(String url) {
Objects.requireNonNull(url, "url must not be null");
String[] parts = url.split("\\|");
String resourceUrl = parts[0];
String version = null;
if (parts.length > 1) {
version = parts[1];
}
SearchParameterMap map = new SearchParameterMap();
map.add("url", new UriParam(resourceUrl));
if (version != null) {
map.add("version", new TokenParam(version));
}
ca.uhn.fhir.rest.api.server.IBundleProvider bundleProvider = myLibraryDao.search(map);
if (bundleProvider.size() == 0) {
return null;
}
List<IBaseResource> resourceList = bundleProvider.getResources(0, bundleProvider.size());
return LibraryResolutionProvider.selectFromList(resolveLibraries(resourceList), version, x -> x.getVersion());
}
private Iterable<org.hl7.fhir.r4.model.Library> getLibrariesByName(String name) {
// Search for libraries by name
SearchParameterMap map = new SearchParameterMap();
map.add("name", new StringParam(name, true));
ca.uhn.fhir.rest.api.server.IBundleProvider bundleProvider = myLibraryDao.search(map);
if (bundleProvider.size() == 0) {
return new ArrayList<>();
}
List<IBaseResource> resourceList = bundleProvider.getResources(0, bundleProvider.size());
return resolveLibraries(resourceList);
}
private Iterable<org.hl7.fhir.r4.model.Library> resolveLibraries(List<IBaseResource> resourceList) {
List<org.hl7.fhir.r4.model.Library> ret = new ArrayList<>();
for (IBaseResource res : resourceList) {
Class<?> clazz = res.getClass();
ret.add((org.hl7.fhir.r4.model.Library) clazz.cast(res));
}
return ret;
}
}

View File

@ -0,0 +1,123 @@
package ca.uhn.fhir.cql.r4.provider;
/*-
* #%L
* HAPI FHIR - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2021 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.cql.common.provider.EvaluationProviderFactory;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.cql.r4.evaluation.MeasureEvaluation;
import ca.uhn.fhir.cql.r4.evaluation.MeasureEvaluationSeed;
import ca.uhn.fhir.cql.r4.helper.LibraryHelper;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.model.Extension;
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.MeasureReport;
import org.hl7.fhir.r4.model.StringType;
import org.opencds.cqf.cql.engine.execution.LibraryLoader;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* This class implements the r4 $evaluate-measure operation defined in the FHIR Clinical Reasoning module.
* Changes should comply with the specification in as far as is possible, and questions about Measure or CQL evaluation can be directed to the original authors.
* @author Jonathan Percival
* @author Bryn Rhodes
* @see <a href="https://hl7.org/fhir/measure-operation-evaluate-measure.html">https://hl7.org/fhir/measure-operation-evaluate-measure.html</a>
*/
@Component
public class MeasureOperationsProvider {
@Autowired
private LibraryResolutionProvider<Library> libraryResolutionProvider;
@Autowired
private IFhirResourceDao<Measure> myMeasureDao;
@Autowired
private DaoRegistry registry;
@Autowired
private EvaluationProviderFactory factory;
@Autowired
private LibraryHelper libraryHelper;
/*
*
* NOTE that the source, user, and pass parameters are not standard parameters
* for the FHIR $evaluate-measure operation
*
*/
@Operation(name = "$evaluate-measure", idempotent = true, type = Measure.class)
public MeasureReport evaluateMeasure(@IdParam IdType theId,
@OperationParam(name = "periodStart") String periodStart,
@OperationParam(name = "periodEnd") String periodEnd,
@OperationParam(name = "measure") String measureRef,
@OperationParam(name = "reportType") String reportType,
@OperationParam(name = "patient") String patientRef,
@OperationParam(name = "productLine") String productLine,
@OperationParam(name = "practitioner") String practitionerRef,
@OperationParam(name = "lastReceivedOn") String lastReceivedOn,
@OperationParam(name = "source") String source,
@OperationParam(name = "user") String user,
@OperationParam(name = "pass") String pass) throws InternalErrorException, FHIRException {
LibraryLoader libraryLoader = this.libraryHelper.createLibraryLoader(this.libraryResolutionProvider);
MeasureEvaluationSeed seed = new MeasureEvaluationSeed(this.factory, libraryLoader,
this.libraryResolutionProvider, this.libraryHelper);
Measure measure = myMeasureDao.read(theId);
if (measure == null) {
throw new RuntimeException("Could not find Measure/" + theId.getIdPart());
}
seed.setup(measure, periodStart, periodEnd, productLine, source, user, pass);
// resolve report type
MeasureEvaluation evaluator = new MeasureEvaluation(seed.getDataProvider(), this.registry,
seed.getMeasurementPeriod());
if (reportType != null) {
switch (reportType) {
case "patient":
return evaluator.evaluatePatientMeasure(seed.getMeasure(), seed.getContext(), patientRef);
case "patient-list":
return evaluator.evaluateSubjectListMeasure(seed.getMeasure(), seed.getContext(), practitionerRef);
case "population":
return evaluator.evaluatePopulationMeasure(seed.getMeasure(), seed.getContext());
default:
throw new IllegalArgumentException("Invalid report type: " + reportType);
}
}
// default report type is patient
MeasureReport report = evaluator.evaluatePatientMeasure(seed.getMeasure(), seed.getContext(), patientRef);
if (productLine != null) {
Extension ext = new Extension();
ext.setUrl("http://hl7.org/fhir/us/cqframework/cqfmeasures/StructureDefinition/cqfm-productLine");
ext.setValue(new StringType(productLine));
report.addExtension(ext);
}
return report;
}
}

View File

@ -0,0 +1,82 @@
package ca.uhn.fhir.cql;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.cql.common.provider.CqlProviderTestBase;
import ca.uhn.fhir.cql.config.CqlDstu3Config;
import ca.uhn.fhir.cql.config.TestCqlConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig;
import ca.uhn.fhir.jpa.test.BaseJpaDstu3Test;
import org.apache.commons.io.FileUtils;
import org.hl7.fhir.dstu3.model.Bundle;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {TestCqlConfig.class, SubscriptionProcessorConfig.class, CqlDstu3Config.class})
public class BaseCqlDstu3Test extends BaseJpaDstu3Test implements CqlProviderTestBase {
Logger ourLog = LoggerFactory.getLogger(BaseCqlDstu3Test.class);
@Autowired
protected
DaoRegistry myDaoRegistry;
@Autowired
protected
FhirContext myFhirContext;
@Autowired
IFhirSystemDao mySystemDao;
@Override
public FhirContext getFhirContext() {
return myFhirContext;
}
@Override
public DaoRegistry getDaoRegistry() {
return myDaoRegistry;
}
protected int loadDataFromDirectory(String theDirectoryName) throws IOException {
int count = 0;
ourLog.info("Reading files in directory: {}", theDirectoryName);
ClassPathResource dir = new ClassPathResource(theDirectoryName);
Collection<File> files = FileUtils.listFiles(dir.getFile(), null, false);
ourLog.info("{} files found.", files.size());
for (File file : files) {
String filename = file.getAbsolutePath();
ourLog.info("Processing filename '{}'", filename);
if (filename.endsWith(".cql") || filename.contains("expectedresults")) {
// Ignore .cql and expectedresults files
ourLog.info("Ignoring file: '{}'", filename);
} else if (filename.endsWith(".json")) {
if (filename.contains("bundle")) {
loadBundle(filename);
} else {
loadResource(filename);
}
count++;
} else {
ourLog.info("Ignoring file: '{}'", filename);
}
}
return count;
}
protected Bundle loadBundle(String theLocation) throws IOException {
String json = stringFromResource(theLocation);
Bundle bundle = (Bundle) myFhirContext.newJsonParser().parseResource(json);
Bundle result = (Bundle) mySystemDao.transaction(null, bundle);
return result;
}
}

View File

@ -0,0 +1,82 @@
package ca.uhn.fhir.cql;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.cql.common.provider.CqlProviderTestBase;
import ca.uhn.fhir.cql.config.CqlR4Config;
import ca.uhn.fhir.cql.config.TestCqlConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig;
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
import org.apache.commons.io.FileUtils;
import org.hl7.fhir.r4.model.Bundle;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {CqlR4Config.class, TestCqlConfig.class, SubscriptionProcessorConfig.class})
public class BaseCqlR4Test extends BaseJpaR4Test implements CqlProviderTestBase {
Logger ourLog = LoggerFactory.getLogger(BaseCqlR4Test.class);
@Autowired
protected
DaoRegistry myDaoRegistry;
@Autowired
protected
FhirContext myFhirContext;
@Autowired
IFhirSystemDao mySystemDao;
@Override
public FhirContext getFhirContext() {
return myFhirContext;
}
@Override
public DaoRegistry getDaoRegistry() {
return myDaoRegistry;
}
protected int loadDataFromDirectory(String theDirectoryName) throws IOException {
int count = 0;
ourLog.info("Reading files in directory: {}", theDirectoryName);
ClassPathResource dir = new ClassPathResource(theDirectoryName);
Collection<File> files = FileUtils.listFiles(dir.getFile(), null, false);
ourLog.info("{} files found.", files.size());
for (File file : files) {
String filename = file.getAbsolutePath();
ourLog.info("Processing filename '{}'", filename);
if (filename.endsWith(".cql") || filename.contains("expectedresults")) {
// Ignore .cql and expectedresults files
ourLog.info("Ignoring file: '{}'", filename);
} else if (filename.endsWith(".json")) {
if (filename.contains("bundle")) {
loadBundle(filename);
} else {
loadResource(filename);
}
count++;
} else {
ourLog.info("Ignoring file: '{}'", filename);
}
}
return count;
}
protected Bundle loadBundle(String theLocation) throws IOException {
String json = stringFromResource(theLocation);
Bundle bundle = (Bundle) myFhirContext.newJsonParser().parseResource(json);
Bundle result = (Bundle) mySystemDao.transaction(null, bundle);
return result;
}
}

View File

@ -0,0 +1,44 @@
package ca.uhn.fhir.cql.common.evaluation;
import org.cqframework.cql.cql2elm.LibraryManager;
import org.cqframework.cql.cql2elm.ModelManager;
import org.cqframework.cql.elm.execution.Library;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Collection;
import static org.junit.Assert.assertNotNull;
@ExtendWith(MockitoExtension.class)
public class LibraryLoaderTest {
@Mock
LibraryManager libraryManager;
@Mock
ModelManager modelManager;
@InjectMocks
LibraryLoader libraryLoader;
@Test
public void testGetLibraryManagerReturnsInjectedMock() {
LibraryManager libraryManager = libraryLoader.getLibraryManager();
assertNotNull("results of call to getLibraryManager() should NOT be NULL!", libraryManager);
}
@Test
public void testGetModelManagerReturnsInjectedMock() {
ModelManager modelManager = libraryLoader.getModelManager();
assertNotNull("results of call to getModelManager() should NOT be NULL!", modelManager);
}
@Test
public void testGetLibrariesReturnsNonNullCollection() {
Collection<Library> libraries = libraryLoader.getLibraries();
assertNotNull("results of call to getLibraries() should NOT be NULL!", libraries);
}
}

View File

@ -0,0 +1,58 @@
package ca.uhn.fhir.cql.common.helper;
import ca.uhn.fhir.parser.DataFormatException;
import org.junit.jupiter.api.Test;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
public class DateHelperTest {
@Test
public void testDateHelperProperlyResolvesValidDate() {
DateHelper dateHelper = new DateHelper();
Date result = dateHelper.resolveRequestDate("2001-01-29");
assertNotNull("result should not be NULL!", result);
GregorianCalendar cal = new GregorianCalendar();
cal.setTimeInMillis(result.getTime());
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
assertEquals("Got the wrong parsed Date value from initial date String", "2001-01-29", formatter.format(cal.getTime()));
}
@Test
public void testDateHelperProperlyResolvesValidDateWithYearOnly() {
DateHelper dateHelper = new DateHelper();
Date result = dateHelper.resolveRequestDate("2001");
assertNotNull("result should not be NULL!", result);
}
@Test
public void testDateHelperProperlyDetectsBlankDate() {
DateHelper dateHelper = new DateHelper();
Date result = null;
try {
result = dateHelper.resolveRequestDate(null);
fail();
} catch (IllegalArgumentException e) {
assertNull("result should be NULL!", result);
}
}
@Test
public void testDateHelperProperlyDetectsNonNumericDateCharacters() {
DateHelper dateHelper = new DateHelper();
Date result = null;
try {
result = dateHelper.resolveRequestDate("aaa-bbb-ccc");
fail();
} catch (DataFormatException e) {
assertNull("result should be NULL!", result);
}
}
}

View File

@ -0,0 +1,215 @@
package ca.uhn.fhir.cql.common.helper;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.cql.common.provider.CqlProviderTestBase;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import org.apache.commons.lang3.StringUtils;
import org.cqframework.cql.cql2elm.CqlTranslator;
import org.cqframework.cql.cql2elm.CqlTranslatorException;
import org.cqframework.cql.cql2elm.LibraryManager;
import org.cqframework.cql.cql2elm.LibrarySourceLoader;
import org.cqframework.cql.cql2elm.ModelManager;
import org.cqframework.cql.cql2elm.NamespaceManager;
import org.cqframework.cql.elm.execution.Library;
import org.cqframework.cql.elm.tracking.TrackBack;
import org.hl7.elm.r1.VersionedIdentifier;
import org.junit.After;
import org.junit.Before;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import static org.junit.Assert.*;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TranslatorHelperTest implements CqlProviderTestBase {
private static String sampleCql = "library ASF_FHIR version '1.0.0'\n" +
"using FHIR version '4.0.0'\n";
@Mock
LibraryManager libraryManager;
@Mock
ModelManager modelManager;
@Mock
NamespaceManager namespaceManager;
@Mock
LibrarySourceLoader librarySourceLoader;
//LibraryManager libraryManager;
//ModelManager modelManager;
//NamespaceManager namespaceManager;
//LibrarySourceLoader librarySourceLoader;
//@BeforeEach
//@BeforeAll
@Before
public void createMocks() {
MockitoAnnotations.openMocks(this);
//libraryManager = Mockito.mock(LibraryManager.class);
//modelManager = Mockito.mock(ModelManager.class);
//namespaceManager = Mockito.mock(NamespaceManager.class);
//librarySourceLoader = Mockito.mock(LibrarySourceLoader.class);
}
//@AfterAll
//@After
@AfterEach
public void resetMocks() {
reset(libraryManager);
reset(modelManager);
reset(namespaceManager);
reset(librarySourceLoader);
}
@Test
public void testGetTranslatorWhereNoNamespaces() {
when(libraryManager.getNamespaceManager()).thenReturn(namespaceManager);
when(namespaceManager.hasNamespaces()).thenReturn(false);
CqlTranslator translator = TranslatorHelper.getTranslator(sampleCql, libraryManager, modelManager);
assertNotNull("translator should not be NULL!", translator);
}
//@Test
// NOTE: This runs OK by itself, but always fails when running as part of this Class!
public void testGetTranslatorWhereHasNamespaces() {
when(libraryManager.getNamespaceManager()).thenReturn(namespaceManager);
when(libraryManager.getLibrarySourceLoader()).thenReturn(librarySourceLoader);
when(namespaceManager.hasNamespaces()).thenReturn(true);
CqlTranslator translator = TranslatorHelper.getTranslator(sampleCql, libraryManager, modelManager);
assertNotNull("translator should not be NULL!", translator);
}
//@Test
// NOTE: This runs OK by itself, but always fails when running as part of this Class!
public void testGetNullPointerExceptionFromGetTranslatorWhenCqlIsBlankString() {
when(libraryManager.getNamespaceManager()).thenReturn(namespaceManager);
when(libraryManager.getLibrarySourceLoader()).thenReturn(librarySourceLoader);
when(namespaceManager.hasNamespaces()).thenReturn(true);
CqlTranslator translator = null;
try {
translator = TranslatorHelper.getTranslator(" ", libraryManager, modelManager);
fail();
} catch(NullPointerException e) {
assertNull("translator should be NULL!", translator);
}
}
//@Test
// NOTE: This one Fails when using the mockito-core library. It wants the mockito-inline library instead,
// but when we replace the -core with teh -inline one, it causes failures in the hapi-fhir-android project!
// The used MockMaker SubclassByteBuddyMockMaker does not support the creation of static mocks
// Mockito's inline mock maker supports static mocks based on the Instrumentation API.
// You can simply enable this mock mode, by placing the 'mockito-inline' artifact where you are currently using 'mockito-core'.
// Note that Mockito's inline mock maker is not supported on Android.
public void testGetIllegalArgumentExceptionFromGetTranslatorWhenCqlIsInvalid() {
ArrayList<CqlTranslator.Options> options = new ArrayList<>();
options.add(CqlTranslator.Options.EnableAnnotations);
options.add(CqlTranslator.Options.EnableLocators);
options.add(CqlTranslator.Options.DisableListDemotion);
options.add(CqlTranslator.Options.DisableListPromotion);
options.add(CqlTranslator.Options.DisableMethodInvocation);
CqlTranslator translator = null;
try {
MockedStatic<CqlTranslator> cqlTranslator = mockStatic(CqlTranslator.class);
when(CqlTranslator.fromStream(any(InputStream.class), any(ModelManager.class), any(LibraryManager.class), Matchers.<CqlTranslator.Options>anyVararg())).thenThrow(IOException.class);
translator = TranslatorHelper.getTranslator(new ByteArrayInputStream("INVALID-FILENAME".getBytes(StandardCharsets.UTF_8)), libraryManager, modelManager);
fail();
} catch(IllegalArgumentException | IOException e) {
assertTrue(e instanceof IllegalArgumentException);
assertNull("translator should be NULL!", translator);
}
}
@Test
public void testFromTranslateLibrary() {
when(libraryManager.getNamespaceManager()).thenReturn(namespaceManager);
when(libraryManager.getLibrarySourceLoader()).thenReturn(librarySourceLoader);
when(namespaceManager.hasNamespaces()).thenReturn(true);
ArrayList<CqlTranslator.Options> options = new ArrayList<>();
options.add(CqlTranslator.Options.EnableAnnotations);
options.add(CqlTranslator.Options.EnableLocators);
options.add(CqlTranslator.Options.DisableListDemotion);
options.add(CqlTranslator.Options.DisableListPromotion);
options.add(CqlTranslator.Options.DisableMethodInvocation);
Library library = null;
try {
library = TranslatorHelper.translateLibrary("INVALID-FILENAME", libraryManager, modelManager);
} catch(Exception e) {
e.printStackTrace();
fail();
}
assertNotNull("library should not be NULL!", library);
}
@Test
public void testGetIllegalArgumentExceptionFromReadLibraryWhenCqlIsBlankString() {
Library library = null;
try {
library = TranslatorHelper.readLibrary(new ByteArrayInputStream("INVALID-XML-DOCUMENT".getBytes()));
fail();
} catch(IllegalArgumentException e) {
assertNull("library should be NULL!", library);
}
}
@Test
public void testTranslatorReadLibrary() {
Library library = null;
try {
library = TranslatorHelper.readLibrary(new ByteArrayInputStream(stringFromResource("dstu3/hedis-ig/library-asf-elm.xml").getBytes()));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
assertNotNull("library should not be NULL!", library);
}
@Test
public void testErrorsToStringWithEmptyList() {
ArrayList<CqlTranslatorException> errors = new ArrayList<>();
String result = TranslatorHelper.errorsToString(errors);
assertTrue(StringUtils.isEmpty(result));
}
@Test
public void testErrorsToStringWithNonEmptyList() {
ArrayList<CqlTranslatorException> errors = new ArrayList<>();
CqlTranslatorException e = new CqlTranslatorException("Exception");
TrackBack trackBack = new TrackBack(new VersionedIdentifier(), 0, 0, 0, 0);
e.setLocator(trackBack);
errors.add(e);
String result = TranslatorHelper.errorsToString(errors);
assertTrue(!StringUtils.isEmpty(result));
assertEquals("null [0:0, 0:0] Exception", result);
}
@Override
public FhirContext getFhirContext() {
return null;
}
@Override
public DaoRegistry getDaoRegistry() {
return null;
}
}

View File

@ -0,0 +1,46 @@
package ca.uhn.fhir.cql.common.provider;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public interface CqlProviderTestBase {
FhirContext getFhirContext();
DaoRegistry getDaoRegistry();
default IBaseResource loadResource(String theLocation) throws IOException {
String json = stringFromResource(theLocation);
IBaseResource resource = getFhirContext().newJsonParser().parseResource(json);
IFhirResourceDao<IBaseResource> dao = getDaoRegistry().getResourceDao(resource.getIdElement().getResourceType());
if (dao == null) {
return null;
} else {
dao.update(resource);
return resource;
}
}
default String stringFromResource(String theLocation) throws IOException {
InputStream is = null;
if (theLocation.startsWith(File.separator)) {
is = new FileInputStream(theLocation);
} else {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
Resource resource = resourceLoader.getResource(theLocation);
is = resource.getInputStream();
}
return IOUtils.toString(is, Charsets.UTF_8);
}
}

View File

@ -0,0 +1,11 @@
package ca.uhn.fhir.cql.config;
import ca.uhn.fhir.jpa.subscription.channel.config.SubscriptionChannelConfig;
import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({SubscriptionSubmitterConfig.class, SubscriptionChannelConfig.class})
public class TestCqlConfig {
}

View File

@ -0,0 +1,148 @@
package ca.uhn.fhir.cql.dstu3;
import ca.uhn.fhir.cql.BaseCqlDstu3Test;
import ca.uhn.fhir.cql.common.provider.CqlProviderFactory;
import ca.uhn.fhir.cql.dstu3.provider.MeasureOperationsProvider;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.rp.dstu3.LibraryResourceProvider;
import ca.uhn.fhir.jpa.rp.dstu3.MeasureResourceProvider;
import ca.uhn.fhir.jpa.rp.dstu3.ValueSetResourceProvider;
import ca.uhn.fhir.util.StopWatch;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.Library;
import org.hl7.fhir.dstu3.model.Measure;
import org.hl7.fhir.dstu3.model.MeasureReport;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class CqlProviderDstu3Test extends BaseCqlDstu3Test {
private static final Logger ourLog = LoggerFactory.getLogger(CqlProviderDstu3Test.class);
@Autowired
IFhirResourceDao<Measure> myMeasureDao;
@Autowired
IFhirResourceDao<Library> myLibraryDao;
@Autowired
CqlProviderFactory myCqlProviderFactory;
@Autowired
private LibraryResourceProvider myLibraryResourceProvider;
@Autowired
private MeasureResourceProvider myMeasureResourceProvider;
@Autowired
private ValueSetResourceProvider myValueSetResourceProvider;
@Autowired
private MeasureOperationsProvider myMeasureOperationsProvider;
@BeforeEach
public void before() throws IOException {
// Load terminology for measure tests (HEDIS measures)
loadBundle("dstu3/hedis-ig/hedis-valuesets-bundle.json");
// Load libraries
loadResource("dstu3/hedis-ig/library/library-fhir-model-definition.json");
loadResource("dstu3/hedis-ig/library/library-fhir-helpers.json");
}
/*
See dstu3/library-asf-cql.txt to see the cql encoded within library-asf-logic.json
See dstu3/library-asf-elm.xml to see the elm encoded within library-asf-logic.json
To help explain what's being measured here. Specifically how to interpret the contents of library-asf-logic.json.
From https://www.ncqa.org/wp-content/uploads/2020/02/20200212_17_ASF.pdf
ValueSet: "Alcohol Counseling and Treatment": 'http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1437'
ValueSet: "Alcohol Screening": 'http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1337'
ValueSet: "Alcohol use disorder": 'http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1339'
ValueSet: "Dementia": 'http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1074'
Diagnosis: Alcohol Use Disorder (2.16.840.1.113883.3.464.1004.1339)
Diagnosis: Dementia (2.16.840.1.113883.3.464.1004.1074)
Encounter, Performed: Hospice Encounter (2.16.840.1.113883.3.464.1004.1761)
Intervention, Order: Hospice Intervention (2.16.840.1.113883.3.464.1004.1762)
Intervention, Performed: Alcohol Counseling or Other Follow Up Care
(2.16.840.1.113883.3.464.1004.1437)
Intervention, Performed: Hospice Intervention (2.16.840.1.113883.3.464.1004.1762)
Direct Reference Codes:
Assessment, Performed: How often have you had five or more drinks in one day during the past year
[Reported] (LOINC version 2.63 Code 88037-7)
Assessment, Performed: How often have you had four or more drinks in one day during the past year
[Reported] (LOINC version 2.63 Code 75889-6)
Assessment, Performed: Total score [AUDIT-C] (LOINC version 2.63 Code 75626-2)
*/
@Test
public void testHedisIGEvaluatePatientMeasure() throws IOException {
loadResource("dstu3/hedis-ig/library/library-asf-logic.json");
// Load the measure for ASF: Unhealthy Alcohol Use Screening and Follow-up (ASF)
loadResource("dstu3/hedis-ig/measure-asf.json");
Bundle result = loadBundle("dstu3/hedis-ig/test-patient-6529-data.json");
assertNotNull(result);
List<Bundle.BundleEntryComponent> entries = result.getEntry();
assertThat(entries, hasSize(22));
assertEquals(entries.get(0).getResponse().getStatus(), "201 Created");
assertEquals(entries.get(21).getResponse().getStatus(), "201 Created");
IdType measureId = new IdType("Measure", "measure-asf");
String patient = "Patient/Patient-6529";
String periodStart = "2003-01-01";
String periodEnd = "2003-12-31";
// First run to absorb startup costs
MeasureReport report = myMeasureOperationsProvider.evaluateMeasure(measureId, periodStart, periodEnd, null, null,
patient, null, null, null, null, null, null);
// Assert it worked
assertThat(report.getGroup(), hasSize(1));
assertThat(report.getGroup().get(0).getPopulation(), hasSize(3));
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(report));
// Now timed runs
int runCount = 10;
StopWatch sw = new StopWatch();
for (int i = 0; i < runCount; ++i) {
myMeasureOperationsProvider.evaluateMeasure(measureId, periodStart, periodEnd, null, null,
patient, null, null, null, null, null, null);
}
ourLog.info("Called evaluateMeasure() {} times: average time per call: {}", runCount, sw.formatMillisPerOperation(runCount));
}
@Test
public void testHedisIGEvaluatePopulationMeasure() throws IOException {
loadResource("dstu3/hedis-ig/library/library-asf-logic.json");
// Load the measure for ASF: Unhealthy Alcohol Use Screening and Follow-up (ASF)
loadResource("dstu3/hedis-ig/measure-asf.json");
loadBundle("dstu3/hedis-ig/test-patient-6529-data.json");
// Add a second patient with the same data
loadBundle("dstu3/hedis-ig/test-patient-9999-x-data.json");
IdType measureId = new IdType("Measure", "measure-asf");
String periodStart = "2003-01-01";
String periodEnd = "2003-12-31";
// First run to absorb startup costs
MeasureReport report = myMeasureOperationsProvider.evaluateMeasure(measureId, periodStart, periodEnd, null, "population",
null, null, null, null, null, null, null);
// Assert it worked
assertThat(report.getGroup(), hasSize(1));
assertThat(report.getGroup().get(0).getPopulation(), hasSize(3));
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(report));
// Now timed runs
int runCount = 10;
StopWatch sw = new StopWatch();
for (int i = 0; i < runCount; ++i) {
myMeasureOperationsProvider.evaluateMeasure(measureId, periodStart, periodEnd, null, "population",
null, null, null, null, null, null, null);
}
ourLog.info("Called evaluateMeasure() {} times: average time per call: {}", runCount, sw.formatMillisPerOperation(runCount));
}
}

View File

@ -0,0 +1,26 @@
package ca.uhn.fhir.cql.dstu3;
import ca.uhn.fhir.cql.BaseCqlDstu3Test;
import ca.uhn.fhir.cql.common.provider.CqlProviderLoader;
import ca.uhn.fhir.rest.server.provider.ResourceProviderFactory;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
class CqlProviderLoaderDstu3Test extends BaseCqlDstu3Test {
private static final Logger ourLog = LoggerFactory.getLogger(CqlProviderLoaderDstu3Test.class);
@Autowired
CqlProviderLoader myCqlProviderLoader;
@Autowired
private ResourceProviderFactory myResourceProviderFactory;
@Test
public void testContextLoads() {
myCqlProviderLoader.loadProvider();
myResourceProviderFactory.createProviders();
ourLog.info("The CqlProviderLoader loaded and was able to create Providers.");
}
}

View File

@ -0,0 +1,26 @@
package ca.uhn.fhir.cql.r4;
import ca.uhn.fhir.cql.BaseCqlR4Test;
import ca.uhn.fhir.cql.common.provider.CqlProviderLoader;
import ca.uhn.fhir.rest.server.provider.ResourceProviderFactory;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
public class CqlProviderLoaderR4Test extends BaseCqlR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(CqlProviderLoaderR4Test.class);
@Autowired
CqlProviderLoader myCqlProviderLoader;
@Autowired
private ResourceProviderFactory myResourceProviderFactory;
@Test
public void contextLoads() {
myCqlProviderLoader.loadProvider();
myResourceProviderFactory.createProviders();
ourLog.info("The CqlProviderLoader loaded and was able to create Providers.");
}
}

View File

@ -0,0 +1,72 @@
package ca.uhn.fhir.cql.r4;
import ca.uhn.fhir.cql.BaseCqlR4Test;
import ca.uhn.fhir.cql.common.provider.CqlProviderTestBase;
import ca.uhn.fhir.cql.r4.provider.MeasureOperationsProvider;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import org.hl7.fhir.r4.model.Bundle;
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.MeasureReport;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
public class CqlProviderR4Test extends BaseCqlR4Test implements CqlProviderTestBase {
private static final Logger ourLog = LoggerFactory.getLogger(CqlProviderR4Test.class);
private static final IdType libraryId = new IdType("Library", "library-mrp-logic");
private static final IdType measureId = new IdType("Measure", "measure-asf");
private static final String measure = "Measure/measure-asf";
private static final String patient = "Patient/Patient-6529";
private static final String periodStart = "2000-01-01";
private static final String periodEnd = "2019-12-31";
private static final Object syncObject = new Object();
private static boolean bundlesLoaded = false;
@Autowired
IFhirResourceDao<Measure> myMeasureDao;
@Autowired
IFhirResourceDao<Library> myLibraryDao;
@Autowired
MeasureOperationsProvider myMeasureOperationsProvider;
public synchronized void loadBundles() throws IOException {
if (!bundlesLoaded) {
Bundle result = loadBundle("dstu3/hedis-ig/test-patient-6529-data.json");
bundlesLoaded = true;
}
}
@Test
public void testHedisIGEvaluateMeasureWithTimeframe() throws IOException {
loadBundles();
loadResource("r4/hedis-ig/library-asf-logic.json");
loadResource("r4/hedis-ig/measure-asf.json");
MeasureReport report = myMeasureOperationsProvider.evaluateMeasure(measureId, periodStart, periodEnd, measure, "patient",
patient, null, null, null, null, null, null);
// Assert it worked
assertThat(report.getGroup(), hasSize(1));
assertThat(report.getGroup().get(0).getPopulation(), hasSize(3));
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(report));
}
@Test
public void testHedisIGEvaluateMeasureNoTimeframe() throws IOException {
loadBundles();
loadResource("r4/hedis-ig/library-asf-logic.json");
loadResource("r4/hedis-ig/measure-asf.json");
MeasureReport report = myMeasureOperationsProvider.evaluateMeasure(measureId, null, null, measure, "patient",
patient, null, null, null, null, null, null);
// Assert it worked
assertThat(report.getGroup(), hasSize(1));
assertThat(report.getGroup().get(0).getPopulation(), hasSize(3));
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(report));
}
}

View File

@ -0,0 +1,237 @@
library ASF_FHIR version '1.0.0'
/*
Unhealthy Alcohol Use Screening and Follow-up (ASF)
*/
using FHIR version '3.0.0'
include FHIRHelpers version '3.0.0' called FHIRHelpers
/*
Measure Description
The percentage of members 18 years of age and older who were screened for
unhealthy alcohol use using a standardized tool and, if screened
positive, received appropriate follow-up care. Two rates are reported.
1. Unhealthy Alcohol Use Screening. The percentage of members who had a
systematic screening for unhealthy alcohol use.
2. Counseling or Other Follow-up. The percentage of members who screened
positive for unhealthy alcohol use and received brief counseling or
other follow-up care within 2 months of a positive screening.
*/
codesystem "LOINC": 'http://loinc.org'
codesystem "CQFramework": 'http://cqframework.info/codesystem/placeholder'
// Update
valueset "Alcohol Counseling and Treatment": 'http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1437'
valueset "Alcohol Screening": 'http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1337'
valueset "Alcohol use disorder": 'http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1339'
valueset "Dementia": 'http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1074'
code "Alcoholic drinks per drinking day - Reported": '11287-0' from "LOINC"
code "AUDIT Total Score (in points)": '75624-7' from "LOINC"
code "AUDIT-C Total Score (in points)": '75626-2' from "LOINC"
/*
This library has an explicit parameter which is the product line.
Recognized normal arguments are {'commercial', 'medicaid', 'medicare'}.
If one of these normal arguments is given, the patient will only be
considered to be in the Initial Population if they have an appropriate
continuous enrollment in that kind of medical plan.
If instead a null argument is given, their enrollment status will have no
effect on whether they are considered to be in the Initial Population.
If instead some other argument is given (an unrecognized plan type),
the patient will unconditionally NOT be in the Initial Population.
*/
parameter "Product Line" String
/*
This library has an explicit parameter which is the measurement year.
While the actual parameter's type accepts all intervals, this library
expects it will only be given arguments corresponding exactly to one whole
calendar year, and it will not behave properly otherwise; 2017 for example:
Interval[DateTime(2017,1,1,0,0,0,0), DateTime(2018,1,1,0,0,0,0))
*/
parameter "Measurement Period" Interval<DateTime>
/*
This library evaluates with respect to exactly 1 candidate patient at a time,
that patient being given by the special context parameter Patient.
*/
context Patient
define "Initial Population":
AgeInYearsAt(start of "Measurement Period") >= 18
/*
Exclusions
*/
define "Denominator Exclusion":
exists (
[Condition: "Alcohol use disorder"] AlcoholUse
where AlcoholUse.clinicalStatus in { 'active', 'recurrence' }
and AlcoholUse.assertedDate during day of Interval[start of "Measurement Period" - 1 year, end of "Measurement Period"]
)
or exists (
[Condition: "Dementia"] D
where D.clinicalStatus in { 'active', 'recurrence' }
and D.assertedDate during day of Interval[start of "Measurement Period", end of "Measurement Period" - 60 days]
)
/*
Denominators and Numerators
*/
// Unhealthy Alcohol Use Screening
define "Denominator 1":
// "Initial Population"
true
// Unhealthy Alcohol Use Screening
define "Numerator 1":
// "Initial Population"
exists ( "AUDIT-C Assessment" )
or exists ( "AUDIT Assessment" )
or (
"Patient is Male"
and exists ( "Five or more drinks per day Assessment" )
)
or (
"Patient is Female"
and (
exists ( "Four or more drinks per day Assessment" )
or exists ( "Five or more drinks per day Assessment" )
)
)
or (
"Patient is 65 or Over"
and (
exists ( "Four or more drinks per day Assessment" )
or exists ( "Five or more drinks per day Assessment" )
)
)
// Note: The spec doesn't include the over 65 test here but does in dependent N/D 2.
define "AUDIT-C Assessment":
[Observation: "AUDIT-C Total Score (in points)"] A
where A.status in { 'final', 'amended', 'corrected' }
and A.effective in day of "Measurement Period"
and A.value is not null
define "AUDIT Assessment":
[Observation: "AUDIT Total Score (in points)"] A
where A.status in { 'final', 'amended', 'corrected' }
and A.effective in day of "Measurement Period"
and A.value is not null
define "Patient is Male":
Patient.gender = 'male'
define "Five or more drinks per day Assessment":
[Observation: "Alcoholic drinks per drinking day - Reported"] A
where A.status in { 'final', 'amended', 'corrected' }
and A.effective in day of "Measurement Period"
and A.value >= 5 '{drinks}/d'
define "Patient is Female":
Patient.gender = 'female'
define "Patient is 65 or Over":
AgeInYearsAt(start of "Measurement Period")>= 65
define "Four or more drinks per day Assessment":
[Observation: "Alcoholic drinks per drinking day - Reported"] A
where A.status in { 'final', 'amended', 'corrected' }
and A.effective in day of "Measurement Period"
and A.value >= 4 '{drinks}/d'
// Counseling or Other Follow-Up on Positive Screen
/*
Initial Population
Product lines -- Commercial, Medicaid, Medicare (report each product line separately).
*/
define "Denominator 2":
// "Initial Population"
exists "Positive Assessment for Unhealthy Alcohol Use"
// Counseling or Other Follow-Up on Positive Screen
define "Numerator 2":
// "Initial Population"
exists (
"Initial Positive Assessment for Unhealthy Alcohol Use" A
with "Followup After Positive Screen" F
such that
if F is Observation then F.effective 2 months or less on or after day of A.effective
else F.performed."end" 2 months or less on or after day of A.effective
)
define "Positive Assessment for Unhealthy Alcohol Use":
(
"AUDIT Assessment" A
where A.value >= 8
)
union (
"AUDIT-C Assessment" A
where ("Patient is Male" and A.value >= 4)
or ("Patient is Female" and A.value >= 3)
)
union (
"Five or more drinks per day Assessment" A
where "Patient is Male"
and A.value >= 1
)
union (
"Four or more drinks per day Assessment" A
where ("Patient is Female" or "Patient is 65 or Over")
and A.value >= 1
)
define "Followup After Positive Screen":
(
[Procedure: "Alcohol Counseling and Treatment"] Proc
where Proc.status = 'completed'
)
union (
[Observation: "Alcohol Counseling and Treatment"] Obs
where Obs.status in { 'final', 'amended', 'corrected' }
)
union (
[Procedure: "Alcohol Screening"] Proc
where Proc.status = 'completed'
)
union (
[Observation: "Alcohol Screening"] Obs
where Obs.status in { 'final', 'amended', 'corrected' }
)
define "Initial Positive Assessment for Unhealthy Alcohol Use":
{
First(
"Positive Assessment for Unhealthy Alcohol Use" A
sort by effective.value
)
}
/*
Stratifiers
*/
define "Stratifier 1":
AgeInYearsAt(start of "Measurement Period")in Interval[18, 44]
define "Stratifier 2":
AgeInYearsAt(start of "Measurement Period")in Interval[45, 64]
define "Stratifier 3":
AgeInYearsAt(start of "Measurement Period")>= 65

View File

@ -0,0 +1,736 @@
<?xml version="1.0" encoding="UTF-8"?>
<library xmlns="urn:hl7-org:elm:r1" xmlns:t="urn:hl7-org:elm-types:r1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:fhir="http://hl7.org/fhir" xmlns:a="urn:hl7-org:cql-annotations:r1">
<annotation startLine="124" startChar="3" endLine="124" endChar="50" message="Could not resolve membership operator for terminology target of the retrieve." errorType="semantic" errorSeverity="warning" xsi:type="a:CqlToElmError"/>
<annotation startLine="130" startChar="3" endLine="130" endChar="48" message="Could not resolve membership operator for terminology target of the retrieve." errorType="semantic" errorSeverity="warning" xsi:type="a:CqlToElmError"/>
<annotation startLine="139" startChar="3" endLine="139" endChar="63" message="Could not resolve membership operator for terminology target of the retrieve." errorType="semantic" errorSeverity="warning" xsi:type="a:CqlToElmError"/>
<annotation startLine="151" startChar="3" endLine="151" endChar="63" message="Could not resolve membership operator for terminology target of the retrieve." errorType="semantic" errorSeverity="warning" xsi:type="a:CqlToElmError"/>
<identifier id="ASF_FHIR" version="1.0.0"/>
<schemaIdentifier id="urn:hl7-org:elm" version="r1"/>
<usings>
<def localIdentifier="System" uri="urn:hl7-org:elm-types:r1"/>
<def localIdentifier="FHIR" uri="http://hl7.org/fhir" version="3.0.0"/>
</usings>
<includes>
<def localIdentifier="FHIRHelpers" path="FHIRHelpers" version="3.0.0"/>
</includes>
<parameters>
<def name="Product Line" accessLevel="Public">
<parameterTypeSpecifier name="t:String" xsi:type="NamedTypeSpecifier"/>
</def>
<def name="Measurement Period" accessLevel="Public">
<parameterTypeSpecifier xsi:type="IntervalTypeSpecifier">
<pointType name="t:DateTime" xsi:type="NamedTypeSpecifier"/>
</parameterTypeSpecifier>
</def>
</parameters>
<codeSystems>
<def name="LOINC" id="http://loinc.org" accessLevel="Public"/>
<def name="CQFramework" id="http://cqframework.info/codesystem/placeholder" accessLevel="Public"/>
</codeSystems>
<valueSets>
<def name="Alcohol Counseling and Treatment" id="http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1437" accessLevel="Public"/>
<def name="Alcohol Screening" id="http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1337" accessLevel="Public"/>
<def name="Alcohol use disorder" id="http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1339" accessLevel="Public"/>
<def name="Dementia" id="http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1074" accessLevel="Public"/>
</valueSets>
<codes>
<def name="Alcoholic drinks per drinking day - Reported" id="11287-0" accessLevel="Public">
<codeSystem name="LOINC"/>
</def>
<def name="AUDIT Total Score (in points)" id="75624-7" accessLevel="Public">
<codeSystem name="LOINC"/>
</def>
<def name="AUDIT-C Total Score (in points)" id="75626-2" accessLevel="Public">
<codeSystem name="LOINC"/>
</def>
</codes>
<statements>
<def name="Patient" context="Patient">
<expression xsi:type="SingletonFrom">
<operand dataType="fhir:Patient" xsi:type="Retrieve"/>
</expression>
</def>
<def name="Initial Population" context="Patient" accessLevel="Public">
<expression xsi:type="GreaterOrEqual">
<operand precision="Year" xsi:type="CalculateAgeAt">
<operand xsi:type="ToDateTime">
<operand path="birthDate.value" xsi:type="Property">
<source name="Patient" xsi:type="ExpressionRef"/>
</operand>
</operand>
<operand xsi:type="Start">
<operand name="Measurement Period" xsi:type="ParameterRef"/>
</operand>
</operand>
<operand valueType="t:Integer" value="18" xsi:type="Literal"/>
</expression>
</def>
<def name="Denominator Exclusion" context="Patient" accessLevel="Public">
<expression xsi:type="Or">
<operand xsi:type="Exists">
<operand xsi:type="Query">
<source alias="AlcoholUse">
<expression dataType="fhir:Condition" codeProperty="code" xsi:type="Retrieve">
<codes name="Alcohol use disorder" xsi:type="ValueSetRef"/>
</expression>
</source>
<where xsi:type="And">
<operand xsi:type="In">
<operand name="ToString" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="clinicalStatus" scope="AlcoholUse" xsi:type="Property"/>
</operand>
<operand xsi:type="List">
<element valueType="t:String" value="active" xsi:type="Literal"/>
<element valueType="t:String" value="recurrence" xsi:type="Literal"/>
</operand>
</operand>
<operand precision="Day" xsi:type="In">
<operand name="ToDateTime" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="assertedDate" scope="AlcoholUse" xsi:type="Property"/>
</operand>
<operand lowClosed="true" highClosed="true" xsi:type="Interval">
<low xsi:type="Subtract">
<operand xsi:type="Start">
<operand name="Measurement Period" xsi:type="ParameterRef"/>
</operand>
<operand value="1" unit="year" xsi:type="Quantity"/>
</low>
<high xsi:type="End">
<operand name="Measurement Period" xsi:type="ParameterRef"/>
</high>
</operand>
</operand>
</where>
</operand>
</operand>
<operand xsi:type="Exists">
<operand xsi:type="Query">
<source alias="D">
<expression dataType="fhir:Condition" codeProperty="code" xsi:type="Retrieve">
<codes name="Dementia" xsi:type="ValueSetRef"/>
</expression>
</source>
<where xsi:type="And">
<operand xsi:type="In">
<operand name="ToString" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="clinicalStatus" scope="D" xsi:type="Property"/>
</operand>
<operand xsi:type="List">
<element valueType="t:String" value="active" xsi:type="Literal"/>
<element valueType="t:String" value="recurrence" xsi:type="Literal"/>
</operand>
</operand>
<operand precision="Day" xsi:type="In">
<operand name="ToDateTime" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="assertedDate" scope="D" xsi:type="Property"/>
</operand>
<operand lowClosed="true" highClosed="true" xsi:type="Interval">
<low xsi:type="Start">
<operand name="Measurement Period" xsi:type="ParameterRef"/>
</low>
<high xsi:type="Subtract">
<operand xsi:type="End">
<operand name="Measurement Period" xsi:type="ParameterRef"/>
</operand>
<operand value="60" unit="days" xsi:type="Quantity"/>
</high>
</operand>
</operand>
</where>
</operand>
</operand>
</expression>
</def>
<def name="Denominator 1" context="Patient" accessLevel="Public">
<expression valueType="t:Boolean" value="true" xsi:type="Literal"/>
</def>
<def name="AUDIT-C Assessment" context="Patient" accessLevel="Public">
<expression xsi:type="Query">
<source alias="A">
<expression dataType="fhir:Observation" codeProperty="code" xsi:type="Retrieve">
<codes xsi:type="ToList">
<operand name="AUDIT-C Total Score (in points)" xsi:type="CodeRef"/>
</codes>
</expression>
</source>
<where xsi:type="And">
<operand xsi:type="And">
<operand xsi:type="In">
<operand name="ToString" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="status" scope="A" xsi:type="Property"/>
</operand>
<operand xsi:type="List">
<element valueType="t:String" value="final" xsi:type="Literal"/>
<element valueType="t:String" value="amended" xsi:type="Literal"/>
<element valueType="t:String" value="corrected" xsi:type="Literal"/>
</operand>
</operand>
<operand precision="Day" xsi:type="In">
<operand name="ToDateTime" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:dateTime" xsi:type="As">
<operand path="effective" scope="A" xsi:type="Property"/>
</operand>
</operand>
<operand name="Measurement Period" xsi:type="ParameterRef"/>
</operand>
</operand>
<operand xsi:type="Not">
<operand xsi:type="IsNull">
<operand path="value" scope="A" xsi:type="Property"/>
</operand>
</operand>
</where>
</expression>
</def>
<def name="AUDIT Assessment" context="Patient" accessLevel="Public">
<expression xsi:type="Query">
<source alias="A">
<expression dataType="fhir:Observation" codeProperty="code" xsi:type="Retrieve">
<codes xsi:type="ToList">
<operand name="AUDIT Total Score (in points)" xsi:type="CodeRef"/>
</codes>
</expression>
</source>
<where xsi:type="And">
<operand xsi:type="And">
<operand xsi:type="In">
<operand name="ToString" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="status" scope="A" xsi:type="Property"/>
</operand>
<operand xsi:type="List">
<element valueType="t:String" value="final" xsi:type="Literal"/>
<element valueType="t:String" value="amended" xsi:type="Literal"/>
<element valueType="t:String" value="corrected" xsi:type="Literal"/>
</operand>
</operand>
<operand precision="Day" xsi:type="In">
<operand name="ToDateTime" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:dateTime" xsi:type="As">
<operand path="effective" scope="A" xsi:type="Property"/>
</operand>
</operand>
<operand name="Measurement Period" xsi:type="ParameterRef"/>
</operand>
</operand>
<operand xsi:type="Not">
<operand xsi:type="IsNull">
<operand path="value" scope="A" xsi:type="Property"/>
</operand>
</operand>
</where>
</expression>
</def>
<def name="Patient is Male" context="Patient" accessLevel="Public">
<expression xsi:type="Equal">
<operand name="ToString" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="gender" xsi:type="Property">
<source name="Patient" xsi:type="ExpressionRef"/>
</operand>
</operand>
<operand valueType="t:String" value="male" xsi:type="Literal"/>
</expression>
</def>
<def name="Five or more drinks per day Assessment" context="Patient" accessLevel="Public">
<expression xsi:type="Query">
<source alias="A">
<expression dataType="fhir:Observation" codeProperty="code" xsi:type="Retrieve">
<codes xsi:type="ToList">
<operand name="Alcoholic drinks per drinking day - Reported" xsi:type="CodeRef"/>
</codes>
</expression>
</source>
<where xsi:type="And">
<operand xsi:type="And">
<operand xsi:type="In">
<operand name="ToString" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="status" scope="A" xsi:type="Property"/>
</operand>
<operand xsi:type="List">
<element valueType="t:String" value="final" xsi:type="Literal"/>
<element valueType="t:String" value="amended" xsi:type="Literal"/>
<element valueType="t:String" value="corrected" xsi:type="Literal"/>
</operand>
</operand>
<operand precision="Day" xsi:type="In">
<operand name="ToDateTime" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:dateTime" xsi:type="As">
<operand path="effective" scope="A" xsi:type="Property"/>
</operand>
</operand>
<operand name="Measurement Period" xsi:type="ParameterRef"/>
</operand>
</operand>
<operand xsi:type="GreaterOrEqual">
<operand name="ToQuantity" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:Quantity" xsi:type="As">
<operand path="value" scope="A" xsi:type="Property"/>
</operand>
</operand>
<operand value="5" unit="{drinks}/d" xsi:type="Quantity"/>
</operand>
</where>
</expression>
</def>
<def name="Patient is Female" context="Patient" accessLevel="Public">
<expression xsi:type="Equal">
<operand name="ToString" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="gender" xsi:type="Property">
<source name="Patient" xsi:type="ExpressionRef"/>
</operand>
</operand>
<operand valueType="t:String" value="female" xsi:type="Literal"/>
</expression>
</def>
<def name="Four or more drinks per day Assessment" context="Patient" accessLevel="Public">
<expression xsi:type="Query">
<source alias="A">
<expression dataType="fhir:Observation" codeProperty="code" xsi:type="Retrieve">
<codes xsi:type="ToList">
<operand name="Alcoholic drinks per drinking day - Reported" xsi:type="CodeRef"/>
</codes>
</expression>
</source>
<where xsi:type="And">
<operand xsi:type="And">
<operand xsi:type="In">
<operand name="ToString" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="status" scope="A" xsi:type="Property"/>
</operand>
<operand xsi:type="List">
<element valueType="t:String" value="final" xsi:type="Literal"/>
<element valueType="t:String" value="amended" xsi:type="Literal"/>
<element valueType="t:String" value="corrected" xsi:type="Literal"/>
</operand>
</operand>
<operand precision="Day" xsi:type="In">
<operand name="ToDateTime" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:dateTime" xsi:type="As">
<operand path="effective" scope="A" xsi:type="Property"/>
</operand>
</operand>
<operand name="Measurement Period" xsi:type="ParameterRef"/>
</operand>
</operand>
<operand xsi:type="GreaterOrEqual">
<operand name="ToQuantity" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:Quantity" xsi:type="As">
<operand path="value" scope="A" xsi:type="Property"/>
</operand>
</operand>
<operand value="4" unit="{drinks}/d" xsi:type="Quantity"/>
</operand>
</where>
</expression>
</def>
<def name="Patient is 65 or Over" context="Patient" accessLevel="Public">
<expression xsi:type="GreaterOrEqual">
<operand precision="Year" xsi:type="CalculateAgeAt">
<operand xsi:type="ToDateTime">
<operand path="birthDate.value" xsi:type="Property">
<source name="Patient" xsi:type="ExpressionRef"/>
</operand>
</operand>
<operand xsi:type="Start">
<operand name="Measurement Period" xsi:type="ParameterRef"/>
</operand>
</operand>
<operand valueType="t:Integer" value="65" xsi:type="Literal"/>
</expression>
</def>
<def name="Numerator 1" context="Patient" accessLevel="Public">
<expression xsi:type="Or">
<operand xsi:type="Or">
<operand xsi:type="Or">
<operand xsi:type="Or">
<operand xsi:type="Exists">
<operand name="AUDIT-C Assessment" xsi:type="ExpressionRef"/>
</operand>
<operand xsi:type="Exists">
<operand name="AUDIT Assessment" xsi:type="ExpressionRef"/>
</operand>
</operand>
<operand xsi:type="And">
<operand name="Patient is Male" xsi:type="ExpressionRef"/>
<operand xsi:type="Exists">
<operand name="Five or more drinks per day Assessment" xsi:type="ExpressionRef"/>
</operand>
</operand>
</operand>
<operand xsi:type="And">
<operand name="Patient is Female" xsi:type="ExpressionRef"/>
<operand xsi:type="Or">
<operand xsi:type="Exists">
<operand name="Four or more drinks per day Assessment" xsi:type="ExpressionRef"/>
</operand>
<operand xsi:type="Exists">
<operand name="Five or more drinks per day Assessment" xsi:type="ExpressionRef"/>
</operand>
</operand>
</operand>
</operand>
<operand xsi:type="And">
<operand name="Patient is 65 or Over" xsi:type="ExpressionRef"/>
<operand xsi:type="Or">
<operand xsi:type="Exists">
<operand name="Four or more drinks per day Assessment" xsi:type="ExpressionRef"/>
</operand>
<operand xsi:type="Exists">
<operand name="Five or more drinks per day Assessment" xsi:type="ExpressionRef"/>
</operand>
</operand>
</operand>
</expression>
</def>
<def name="Positive Assessment for Unhealthy Alcohol Use" context="Patient" accessLevel="Public">
<expression xsi:type="Union">
<operand xsi:type="Union">
<operand xsi:type="Query">
<source alias="A">
<expression name="AUDIT Assessment" xsi:type="ExpressionRef"/>
</source>
<where xsi:type="GreaterOrEqual">
<operand name="ToQuantity" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:Quantity" xsi:type="As">
<operand path="value" scope="A" xsi:type="Property"/>
</operand>
</operand>
<operand xsi:type="ToQuantity">
<operand valueType="t:Integer" value="8" xsi:type="Literal"/>
</operand>
</where>
</operand>
<operand xsi:type="Query">
<source alias="A">
<expression name="AUDIT-C Assessment" xsi:type="ExpressionRef"/>
</source>
<where xsi:type="Or">
<operand xsi:type="And">
<operand name="Patient is Male" xsi:type="ExpressionRef"/>
<operand xsi:type="GreaterOrEqual">
<operand name="ToQuantity" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:Quantity" xsi:type="As">
<operand path="value" scope="A" xsi:type="Property"/>
</operand>
</operand>
<operand xsi:type="ToQuantity">
<operand valueType="t:Integer" value="4" xsi:type="Literal"/>
</operand>
</operand>
</operand>
<operand xsi:type="And">
<operand name="Patient is Female" xsi:type="ExpressionRef"/>
<operand xsi:type="GreaterOrEqual">
<operand name="ToQuantity" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:Quantity" xsi:type="As">
<operand path="value" scope="A" xsi:type="Property"/>
</operand>
</operand>
<operand xsi:type="ToQuantity">
<operand valueType="t:Integer" value="3" xsi:type="Literal"/>
</operand>
</operand>
</operand>
</where>
</operand>
</operand>
<operand xsi:type="Union">
<operand xsi:type="Query">
<source alias="A">
<expression name="Five or more drinks per day Assessment" xsi:type="ExpressionRef"/>
</source>
<where xsi:type="And">
<operand name="Patient is Male" xsi:type="ExpressionRef"/>
<operand xsi:type="GreaterOrEqual">
<operand name="ToQuantity" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:Quantity" xsi:type="As">
<operand path="value" scope="A" xsi:type="Property"/>
</operand>
</operand>
<operand xsi:type="ToQuantity">
<operand valueType="t:Integer" value="1" xsi:type="Literal"/>
</operand>
</operand>
</where>
</operand>
<operand xsi:type="Query">
<source alias="A">
<expression name="Four or more drinks per day Assessment" xsi:type="ExpressionRef"/>
</source>
<where xsi:type="And">
<operand xsi:type="Or">
<operand name="Patient is Female" xsi:type="ExpressionRef"/>
<operand name="Patient is 65 or Over" xsi:type="ExpressionRef"/>
</operand>
<operand xsi:type="GreaterOrEqual">
<operand name="ToQuantity" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:Quantity" xsi:type="As">
<operand path="value" scope="A" xsi:type="Property"/>
</operand>
</operand>
<operand xsi:type="ToQuantity">
<operand valueType="t:Integer" value="1" xsi:type="Literal"/>
</operand>
</operand>
</where>
</operand>
</operand>
</expression>
</def>
<def name="Denominator 2" context="Patient" accessLevel="Public">
<expression xsi:type="Exists">
<operand name="Positive Assessment for Unhealthy Alcohol Use" xsi:type="ExpressionRef"/>
</expression>
</def>
<def name="Initial Positive Assessment for Unhealthy Alcohol Use" context="Patient" accessLevel="Public">
<expression xsi:type="List">
<element xsi:type="First">
<source xsi:type="Query">
<source alias="A">
<expression name="Positive Assessment for Unhealthy Alcohol Use" xsi:type="ExpressionRef"/>
</source>
<sort>
<by direction="asc" xsi:type="ByExpression">
<expression path="value" xsi:type="Property">
<source name="effective" xsi:type="IdentifierRef"/>
</expression>
</by>
</sort>
</source>
</element>
</expression>
</def>
<def name="Followup After Positive Screen" context="Patient" accessLevel="Public">
<expression xsi:type="Union">
<operand xsi:type="As">
<operand xsi:type="Union">
<operand xsi:type="As">
<operand xsi:type="Union">
<operand xsi:type="As">
<operand xsi:type="Query">
<source alias="Proc">
<expression dataType="fhir:Procedure" codeProperty="code" xsi:type="Retrieve">
<codes name="Alcohol Counseling and Treatment" xsi:type="ValueSetRef"/>
</expression>
</source>
<where xsi:type="Equal">
<operand name="ToString" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="status" scope="Proc" xsi:type="Property"/>
</operand>
<operand valueType="t:String" value="completed" xsi:type="Literal"/>
</where>
</operand>
<asTypeSpecifier xsi:type="ListTypeSpecifier">
<elementType xsi:type="ChoiceTypeSpecifier">
<choice name="fhir:Procedure" xsi:type="NamedTypeSpecifier"/>
<choice name="fhir:Observation" xsi:type="NamedTypeSpecifier"/>
</elementType>
</asTypeSpecifier>
</operand>
<operand xsi:type="As">
<operand xsi:type="Query">
<source alias="Obs">
<expression dataType="fhir:Observation" codeProperty="code" xsi:type="Retrieve">
<codes name="Alcohol Counseling and Treatment" xsi:type="ValueSetRef"/>
</expression>
</source>
<where xsi:type="In">
<operand name="ToString" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="status" scope="Obs" xsi:type="Property"/>
</operand>
<operand xsi:type="List">
<element valueType="t:String" value="final" xsi:type="Literal"/>
<element valueType="t:String" value="amended" xsi:type="Literal"/>
<element valueType="t:String" value="corrected" xsi:type="Literal"/>
</operand>
</where>
</operand>
<asTypeSpecifier xsi:type="ListTypeSpecifier">
<elementType xsi:type="ChoiceTypeSpecifier">
<choice name="fhir:Procedure" xsi:type="NamedTypeSpecifier"/>
<choice name="fhir:Observation" xsi:type="NamedTypeSpecifier"/>
</elementType>
</asTypeSpecifier>
</operand>
</operand>
<asTypeSpecifier xsi:type="ListTypeSpecifier">
<elementType xsi:type="ChoiceTypeSpecifier">
<choice name="fhir:Procedure" xsi:type="NamedTypeSpecifier"/>
<choice name="fhir:Observation" xsi:type="NamedTypeSpecifier"/>
</elementType>
</asTypeSpecifier>
</operand>
<operand xsi:type="As">
<operand xsi:type="Query">
<source alias="Proc">
<expression dataType="fhir:Procedure" codeProperty="code" xsi:type="Retrieve">
<codes name="Alcohol Screening" xsi:type="ValueSetRef"/>
</expression>
</source>
<where xsi:type="Equal">
<operand name="ToString" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="status" scope="Proc" xsi:type="Property"/>
</operand>
<operand valueType="t:String" value="completed" xsi:type="Literal"/>
</where>
</operand>
<asTypeSpecifier xsi:type="ListTypeSpecifier">
<elementType xsi:type="ChoiceTypeSpecifier">
<choice name="fhir:Procedure" xsi:type="NamedTypeSpecifier"/>
<choice name="fhir:Observation" xsi:type="NamedTypeSpecifier"/>
</elementType>
</asTypeSpecifier>
</operand>
</operand>
<asTypeSpecifier xsi:type="ListTypeSpecifier">
<elementType xsi:type="ChoiceTypeSpecifier">
<choice name="fhir:Procedure" xsi:type="NamedTypeSpecifier"/>
<choice name="fhir:Observation" xsi:type="NamedTypeSpecifier"/>
</elementType>
</asTypeSpecifier>
</operand>
<operand xsi:type="As">
<operand xsi:type="Query">
<source alias="Obs">
<expression dataType="fhir:Observation" codeProperty="code" xsi:type="Retrieve">
<codes name="Alcohol Screening" xsi:type="ValueSetRef"/>
</expression>
</source>
<where xsi:type="In">
<operand name="ToString" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="status" scope="Obs" xsi:type="Property"/>
</operand>
<operand xsi:type="List">
<element valueType="t:String" value="final" xsi:type="Literal"/>
<element valueType="t:String" value="amended" xsi:type="Literal"/>
<element valueType="t:String" value="corrected" xsi:type="Literal"/>
</operand>
</where>
</operand>
<asTypeSpecifier xsi:type="ListTypeSpecifier">
<elementType xsi:type="ChoiceTypeSpecifier">
<choice name="fhir:Procedure" xsi:type="NamedTypeSpecifier"/>
<choice name="fhir:Observation" xsi:type="NamedTypeSpecifier"/>
</elementType>
</asTypeSpecifier>
</operand>
</expression>
</def>
<def name="Numerator 2" context="Patient" accessLevel="Public">
<expression xsi:type="Exists">
<operand xsi:type="Query">
<source alias="A">
<expression name="Initial Positive Assessment for Unhealthy Alcohol Use" xsi:type="ExpressionRef"/>
</source>
<relationship alias="F" xsi:type="With">
<expression name="Followup After Positive Screen" xsi:type="ExpressionRef"/>
<suchThat xsi:type="If">
<condition asType="t:Boolean" xsi:type="As">
<operand xsi:type="Is">
<operand name="F" xsi:type="AliasRef"/>
<isTypeSpecifier name="fhir:Observation" xsi:type="NamedTypeSpecifier"/>
</operand>
</condition>
<then precision="Day" xsi:type="In">
<operand name="ToDateTime" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:dateTime" xsi:type="As">
<operand path="effective" scope="F" xsi:type="Property"/>
</operand>
</operand>
<operand lowClosed="true" highClosed="true" xsi:type="Interval">
<low name="ToDateTime" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:dateTime" xsi:type="As">
<operand path="effective" scope="A" xsi:type="Property"/>
</operand>
</low>
<high xsi:type="Add">
<operand name="ToDateTime" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:dateTime" xsi:type="As">
<operand path="effective" scope="A" xsi:type="Property"/>
</operand>
</operand>
<operand value="2" unit="months" xsi:type="Quantity"/>
</high>
</operand>
</then>
<else precision="Day" xsi:type="In">
<operand name="ToDateTime" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand path="end" xsi:type="Property">
<source path="performed" scope="F" xsi:type="Property"/>
</operand>
</operand>
<operand lowClosed="true" highClosed="true" xsi:type="Interval">
<low name="ToDateTime" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:dateTime" xsi:type="As">
<operand path="effective" scope="A" xsi:type="Property"/>
</operand>
</low>
<high xsi:type="Add">
<operand name="ToDateTime" libraryName="FHIRHelpers" xsi:type="FunctionRef">
<operand asType="fhir:dateTime" xsi:type="As">
<operand path="effective" scope="A" xsi:type="Property"/>
</operand>
</operand>
<operand value="2" unit="months" xsi:type="Quantity"/>
</high>
</operand>
</else>
</suchThat>
</relationship>
</operand>
</expression>
</def>
<def name="Stratifier 1" context="Patient" accessLevel="Public">
<expression xsi:type="In">
<operand precision="Year" xsi:type="CalculateAgeAt">
<operand xsi:type="ToDateTime">
<operand path="birthDate.value" xsi:type="Property">
<source name="Patient" xsi:type="ExpressionRef"/>
</operand>
</operand>
<operand xsi:type="Start">
<operand name="Measurement Period" xsi:type="ParameterRef"/>
</operand>
</operand>
<operand lowClosed="true" highClosed="true" xsi:type="Interval">
<low valueType="t:Integer" value="18" xsi:type="Literal"/>
<high valueType="t:Integer" value="44" xsi:type="Literal"/>
</operand>
</expression>
</def>
<def name="Stratifier 2" context="Patient" accessLevel="Public">
<expression xsi:type="In">
<operand precision="Year" xsi:type="CalculateAgeAt">
<operand xsi:type="ToDateTime">
<operand path="birthDate.value" xsi:type="Property">
<source name="Patient" xsi:type="ExpressionRef"/>
</operand>
</operand>
<operand xsi:type="Start">
<operand name="Measurement Period" xsi:type="ParameterRef"/>
</operand>
</operand>
<operand lowClosed="true" highClosed="true" xsi:type="Interval">
<low valueType="t:Integer" value="45" xsi:type="Literal"/>
<high valueType="t:Integer" value="64" xsi:type="Literal"/>
</operand>
</expression>
</def>
<def name="Stratifier 3" context="Patient" accessLevel="Public">
<expression xsi:type="GreaterOrEqual">
<operand precision="Year" xsi:type="CalculateAgeAt">
<operand xsi:type="ToDateTime">
<operand path="birthDate.value" xsi:type="Property">
<source name="Patient" xsi:type="ExpressionRef"/>
</operand>
</operand>
<operand xsi:type="Start">
<operand name="Measurement Period" xsi:type="ParameterRef"/>
</operand>
</operand>
<operand valueType="t:Integer" value="65" xsi:type="Literal"/>
</expression>
</def>
</statements>
</library>

View File

@ -0,0 +1 @@
The library json files in this folder were downloaded from https://github.com/cqframework/hedis-ig/tree/master/resources/library

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,37 @@
{
"resourceType": "Library",
"id": "library-fhir-model-definition",
"text": {
"status": "generated"
},
"identifier": [
{
"use": "official",
"value": "FHIR"
}
],
"version": "3.0.1",
"title": "FHIR Model Definition",
"status": "draft",
"type": {
"coding": [
{
"code": "model-definition"
}
]
},
"date": "2016-07-08",
"copyright": "The HEDIS measure specifications were developed by and are owned by NCQA. The HEDIS measure specifications are not clinical guidelines and do not establish a standard of medical care. NCQA makes no representations, warranties, or endorsement about the quality of any organization or physician that uses or reports performance measures and NCQA has no liability to anyone who relies on such measure specifications. These materials may not be modified by anyone other than NCQA. Anyone desiring to use or reproduce the materials without modification for a non-commercial purpose may do so without obtaining any approval from NCQA. **Any commercial use (including but not limited to vendors using the measure specifications with a product or service, including calculation of measure results) must be approved by NCQA and are subject to a license at the discretion of NCQA**. &copy; 2019 NCQA, all rights reserved.",
"description": "Model definition for the FHIR Model",
"topic": [
{
"text": "FHIR"
}
],
"content": [
{
"contentType": "application/xml",
"url": "http://cqlrepository.org/fhirmodel-modelinfo.xml"
}
]
}

View File

@ -0,0 +1,118 @@
{
"resourceType": "Measure",
"id": "measure-asf",
"status": "active",
"experimental": true,
"library": [
{
"reference": "Library/library-asf-logic"
}
],
"scoring": {
"coding": [
{
"code": "proportion"
}
]
},
"group": [
{
"identifier": {
"value": "ASF-cohort"
},
"population": [
{
"identifier": {
"value": "initial-population"
},
"code": {
"coding": [
{
"code": "initial-population"
}
]
},
"criteria": "Initial Population"
},
{
"identifier": {
"value": "numerator 1"
},
"code": {
"coding": [
{
"code": "numerator"
}
]
},
"criteria": "Numerator 1"
},
{
"identifier": {
"value": "denominator 1"
},
"code": {
"coding": [
{
"code": "denominator"
}
]
},
"criteria": "Denominator 1"
},
{
"identifier": {
"value": "numerator 2"
},
"code": {
"coding": [
{
"code": "numerator"
}
]
},
"criteria": "Numerator 2"
},
{
"identifier": {
"value": "denominator 2"
},
"code": {
"coding": [
{
"code": "denominator"
}
]
},
"criteria": "Denominator 2"
}
],
"stratifier": [
{
"identifier": {
"value": "stratifier 1"
},
"criteria": "Stratifier 1"
},
{
"identifier": {
"value": "stratifier 2"
},
"criteria": "Stratifier 2"
},
{
"identifier": {
"value": "stratifier 3"
},
"criteria": "Stratifier 3"
},
{
"identifier": {
"value": "stratifier 4"
},
"criteria": "Stratifier 4"
}
]
}
]
}

View File

@ -0,0 +1,235 @@
library ASF_FHIR version '1.0.0'
/*
Unhealthy Alcohol Use Screening and Follow-up (ASF)
*/
using FHIR version '4.0.0'
include FHIRHelpers version '4.0.0' called FHIRHelpers
/*
Measure Description
The percentage of members 18 years of age and older who were screened for
unhealthy alcohol use using a standardized tool and, if screened
positive, received appropriate follow-up care. Two rates are reported.
1. Unhealthy Alcohol Use Screening. The percentage of members who had a
systematic screening for unhealthy alcohol use.
2. Counseling or Other Follow-up. The percentage of members who screened
positive for unhealthy alcohol use and received brief counseling or
other follow-up care within 2 months of a positive screening.
*/
codesystem "LOINC": 'http://loinc.org'
codesystem "CQFramework": 'http://cqframework.info/codesystem/placeholder'
// Update
valueset "Alcohol Counseling and Treatment": 'http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1437'
valueset "Alcohol Screening": 'http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1337'
valueset "Alcohol use disorder": 'http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1339'
valueset "Dementia": 'http://ncqa.org/hedis/ValueSet/2.16.840.1.113883.3.464.1004.1074'
code "Alcoholic drinks per drinking day - Reported": '11287-0' from "LOINC"
code "AUDIT Total Score (in points)": '75624-7' from "LOINC"
code "AUDIT-C Total Score (in points)": '75626-2' from "LOINC"
/*
This library has an explicit parameter which is the product line.
Recognized normal arguments are {'commercial', 'medicaid', 'medicare'}.
If one of these normal arguments is given, the patient will only be
considered to be in the Initial Population if they have an appropriate
continuous enrollment in that kind of medical plan.
If instead a null argument is given, their enrollment status will have no
effect on whether they are considered to be in the Initial Population.
If instead some other argument is given (an unrecognized plan type),
the patient will unconditionally NOT be in the Initial Population.
*/
parameter "Product Line" String
/*
This library has an explicit parameter which is the measurement year.
While the actual parameter's type accepts all intervals, this library
expects it will only be given arguments corresponding exactly to one whole
calendar year, and it will not behave properly otherwise; 2017 for example:
Interval[DateTime(2017,1,1,0,0,0,0), DateTime(2018,1,1,0,0,0,0))
*/
parameter "Measurement Period" Interval<DateTime>
/*
This library evaluates with respect to exactly 1 candidate patient at a time,
that patient being given by the special context parameter Patient.
*/
context Patient
define "Initial Population":
AgeInYearsAt(start of "Measurement Period") >= 18
/*
Exclusions
define "Denominator Exclusion":
exists (
[Condition: "Alcohol use disorder"] AlcoholUse
where AlcoholUse.assertedDate during day of Interval[start of "Measurement Period" - 1 year, end of "Measurement Period"]
)
or exists (
[Condition: "Dementia"] D
where D.assertedDate during day of Interval[start of "Measurement Period", end of "Measurement Period" - 60 days]
)
*/
/*
Denominators and Numerators
*/
// Unhealthy Alcohol Use Screening
define "Denominator 1":
// "Initial Population"
true
// Unhealthy Alcohol Use Screening
define "Numerator 1":
// "Initial Population"
exists ( "AUDIT-C Assessment" )
or exists ( "AUDIT Assessment" )
or (
"Patient is Male"
and exists ( "Five or more drinks per day Assessment" )
)
or (
"Patient is Female"
and (
exists ( "Four or more drinks per day Assessment" )
or exists ( "Five or more drinks per day Assessment" )
)
)
or (
"Patient is 65 or Over"
and (
exists ( "Four or more drinks per day Assessment" )
or exists ( "Five or more drinks per day Assessment" )
)
)
// Note: The spec doesn't include the over 65 test here but does in dependent N/D 2.
define "AUDIT-C Assessment":
[Observation: "AUDIT-C Total Score (in points)"] A
where A.status in { 'final', 'amended', 'corrected' }
and A.effective in day of "Measurement Period"
and A.value is not null
define "AUDIT Assessment":
[Observation: "AUDIT Total Score (in points)"] A
where A.status in { 'final', 'amended', 'corrected' }
and A.effective in day of "Measurement Period"
and A.value is not null
define "Patient is Male":
Patient.gender = 'male'
define "Five or more drinks per day Assessment":
[Observation: "Alcoholic drinks per drinking day - Reported"] A
where A.status in { 'final', 'amended', 'corrected' }
and A.effective in day of "Measurement Period"
and A.value >= 5 '{drinks}/d'
define "Patient is Female":
Patient.gender = 'female'
define "Patient is 65 or Over":
AgeInYearsAt(start of "Measurement Period")>= 65
define "Four or more drinks per day Assessment":
[Observation: "Alcoholic drinks per drinking day - Reported"] A
where A.status in { 'final', 'amended', 'corrected' }
and A.effective in day of "Measurement Period"
and A.value >= 4 '{drinks}/d'
// Counseling or Other Follow-Up on Positive Screen
/*
Initial Population
Product lines -- Commercial, Medicaid, Medicare (report each product line separately).
*/
define "Denominator 2":
// "Initial Population"
exists "Positive Assessment for Unhealthy Alcohol Use"
// Counseling or Other Follow-Up on Positive Screen
define "Numerator 2":
// "Initial Population"
exists (
"Initial Positive Assessment for Unhealthy Alcohol Use" A
with "Followup After Positive Screen" F
such that
if F is Observation then F.effective 2 months or less on or after day of A.effective
else F.performed."end" 2 months or less on or after day of A.effective
)
define "Positive Assessment for Unhealthy Alcohol Use":
(
"AUDIT Assessment" A
where A.value >= 8
)
union (
"AUDIT-C Assessment" A
where ("Patient is Male" and A.value >= 4)
or ("Patient is Female" and A.value >= 3)
)
union (
"Five or more drinks per day Assessment" A
where "Patient is Male"
and A.value >= 1
)
union (
"Four or more drinks per day Assessment" A
where ("Patient is Female" or "Patient is 65 or Over")
and A.value >= 1
)
define "Followup After Positive Screen":
(
[Procedure: "Alcohol Counseling and Treatment"] Proc
where Proc.status = 'completed'
)
union (
[Observation: "Alcohol Counseling and Treatment"] Obs
where Obs.status in { 'final', 'amended', 'corrected' }
)
union (
[Procedure: "Alcohol Screening"] Proc
where Proc.status = 'completed'
)
union (
[Observation: "Alcohol Screening"] Obs
where Obs.status in { 'final', 'amended', 'corrected' }
)
define "Initial Positive Assessment for Unhealthy Alcohol Use":
{
First(
"Positive Assessment for Unhealthy Alcohol Use" A
sort by effective.value
)
}
/*
Stratifiers
*/
define "Stratifier 1":
AgeInYearsAt(start of "Measurement Period")in Interval[18, 44]
define "Stratifier 2":
AgeInYearsAt(start of "Measurement Period")in Interval[45, 64]
define "Stratifier 3":
AgeInYearsAt(start of "Measurement Period")>= 65

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,116 @@
{
"resourceType": "Measure",
"id": "measure-asf",
"status": "active",
"experimental": true,
"library": [
"http://ncqa.org/fhir/hedis/Library/library-asf-logic"
],
"scoring": {
"coding": [
{
"code": "proportion"
}
]
},
"group": [
{
"identifier": {
"value": "ASF-cohort"
},
"population": [
{
"identifier": {
"value": "initial-population"
},
"code": {
"coding": [
{
"code": "initial-population"
}
]
},
"criteria": "Initial Population"
},
{
"identifier": {
"value": "numerator 1"
},
"code": {
"coding": [
{
"code": "numerator"
}
]
},
"criteria": "Numerator 1"
},
{
"identifier": {
"value": "denominator 1"
},
"code": {
"coding": [
{
"code": "denominator"
}
]
},
"criteria": "Denominator 1"
},
{
"identifier": {
"value": "numerator 2"
},
"code": {
"coding": [
{
"code": "numerator"
}
]
},
"criteria": "Numerator 2"
},
{
"identifier": {
"value": "denominator 2"
},
"code": {
"coding": [
{
"code": "denominator"
}
]
},
"criteria": "Denominator 2"
}
],
"stratifier": [
{
"identifier": {
"value": "stratifier 1"
},
"criteria": "Stratifier 1"
},
{
"identifier": {
"value": "stratifier 2"
},
"criteria": "Stratifier 2"
},
{
"identifier": {
"value": "stratifier 3"
},
"criteria": "Stratifier 3"
},
{
"identifier": {
"value": "stratifier 4"
},
"criteria": "Stratifier 4"
}
]
}
]
}

View File

@ -34,14 +34,14 @@ public interface ISchedulerService {
void logStatusForUnitTest();
/**
* Only one instance of this task will fire across the whole cluster (when running in a clustered environment).
* This task will execute locally (and should execute on all nodes of the cluster if there is a cluster)
* @param theIntervalMillis How many milliseconds between passes should this job run
* @param theJobDefinition The Job to fire
*/
void scheduleLocalJob(long theIntervalMillis, ScheduledJobDefinition theJobDefinition);
/**
* This task will execute locally (and should execute on all nodes of the cluster if there is a cluster)
* Only one instance of this task will fire across the whole cluster (when running in a clustered environment).
* @param theIntervalMillis How many milliseconds between passes should this job run
* @param theJobDefinition The Job to fire
*/

View File

@ -0,0 +1,146 @@
package ca.uhn.fhir.jpa.config;
/*-
* #%L
* HAPI FHIR JPA Server Test Utilities
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.jpa.binstore.IBinaryStorageSvc;
import ca.uhn.fhir.jpa.binstore.MemoryBinaryStorageSvcImpl;
import ca.uhn.fhir.jpa.search.HapiLuceneAnalysisConfigurer;
import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener;
import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.validation.ResultSeverityEnum;
import net.ttddyy.dsproxy.listener.SingleQueryCountHolder;
import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.apache.commons.dbcp2.BasicDataSource;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.search.backend.lucene.cfg.LuceneBackendSettings;
import org.hibernate.search.backend.lucene.cfg.LuceneIndexSettings;
import org.hibernate.search.engine.cfg.BackendSettings;
import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Lazy;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import javax.sql.DataSource;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
// TODO KBD Can we remove this Class entirely and just use a Generic one (same for TestJpaR4Config)?
@Import(TestJpaConfig.class)
public class TestJpaDstu3Config extends BaseJavaConfigDstu3 {
private static final Logger ourLog = LoggerFactory.getLogger(TestJpaDstu3Config.class);
@Autowired
FhirContext myFhirContext;
@Bean
public CircularQueueCaptureQueriesListener captureQueriesListener() {
return new CircularQueueCaptureQueriesListener();
}
@Bean
public DataSource dataSource() {
BasicDataSource retVal = new BasicDataSource();
retVal.setDriver(new org.h2.Driver());
retVal.setUrl("jdbc:h2:mem:testdb_dstu3");
retVal.setMaxWaitMillis(10000);
retVal.setUsername("");
retVal.setPassword("");
SLF4JLogLevel level = SLF4JLogLevel.INFO;
DataSource dataSource = ProxyDataSourceBuilder
.create(retVal)
// .logQueryBySlf4j(level, "SQL")
.logSlowQueryBySlf4j(10, TimeUnit.SECONDS)
// .countQuery(new ThreadQueryCountHolder())
// .beforeQuery(new BlockLargeNumbersOfParamsListener())
.afterQuery(captureQueriesListener())
.afterQuery(new CurrentThreadCaptureQueriesListener())
.countQuery(singleQueryCountHolder())
.build();
return dataSource;
}
@Bean
public SingleQueryCountHolder singleQueryCountHolder() {
return new SingleQueryCountHolder();
}
@Override
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
LocalContainerEntityManagerFactoryBean retVal = super.entityManagerFactory();
retVal.setPersistenceUnitName("PU_HapiFhirJpaDstu3");
retVal.setDataSource(dataSource());
retVal.setJpaProperties(jpaProperties());
return retVal;
}
@Bean
public Properties jpaProperties() {
Properties extraProperties = new Properties();
extraProperties.put("hibernate.format_sql", "false");
extraProperties.put("hibernate.show_sql", "false");
extraProperties.put("hibernate.hbm2ddl.auto", "update");
extraProperties.put("hibernate.dialect", H2Dialect.class.getName());
extraProperties.put(BackendSettings.backendKey(BackendSettings.TYPE), "lucene");
extraProperties.put(BackendSettings.backendKey(LuceneBackendSettings.ANALYSIS_CONFIGURER), HapiLuceneAnalysisConfigurer.class.getName());
extraProperties.put(BackendSettings.backendKey(LuceneIndexSettings.DIRECTORY_TYPE), "local-heap");
extraProperties.put(BackendSettings.backendKey(LuceneBackendSettings.LUCENE_VERSION), "LUCENE_CURRENT");
extraProperties.put(HibernateOrmMapperSettings.ENABLED, "true");
return extraProperties;
}
/**
* Bean which validates incoming requests
*/
@Bean
@Lazy
public RequestValidatingInterceptor requestValidatingInterceptor() {
RequestValidatingInterceptor requestValidator = new RequestValidatingInterceptor();
requestValidator.setFailOnSeverity(ResultSeverityEnum.ERROR);
requestValidator.setAddResponseHeaderOnSeverity(null);
requestValidator.setAddResponseOutcomeHeaderOnSeverity(ResultSeverityEnum.INFORMATION);
requestValidator.addValidatorModule(instanceValidator());
return requestValidator;
}
@Bean
public IBinaryStorageSvc binaryStorage() {
return new MemoryBinaryStorageSvcImpl();
}
@Bean
public DefaultProfileValidationSupport validationSupportChainDstu3() {
return new DefaultProfileValidationSupport(myFhirContext);
}
}

View File

@ -50,6 +50,7 @@ import javax.sql.DataSource;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
// TODO KBD Can we remove this Class entirely and just use a Generic one (same for TestJpaDstu3Config)?
@Import(TestJpaConfig.class)
public class TestJpaR4Config extends BaseJavaConfigR4 {
private static final Logger ourLog = LoggerFactory.getLogger(TestJpaR4Config.class);

View File

@ -0,0 +1,28 @@
package ca.uhn.fhir.jpa.test;
/*-
* #%L
* HAPI FHIR JPA Server Test Utilities
* %%
* Copyright (C) 2014 - 2020 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.jpa.config.TestJpaDstu3Config;
import org.springframework.test.context.ContextConfiguration;
@ContextConfiguration(classes = {TestJpaDstu3Config.class})
public abstract class BaseJpaDstu3Test extends BaseJpaTest {
}

View File

@ -1327,7 +1327,7 @@ public class FhirInstanceValidatorDstu3Test {
ValidationResult results = myVal.validateWithResult(input);
List<SingleValidationMessage> outcome = logResultsAndReturnNonInformationalOnes(results);
assertEquals(1, outcome.size());
assertEquals(2, outcome.size());
assertThat(outcome.toString(), containsString("value should not start or finish with whitespace"));
}

View File

@ -44,7 +44,7 @@ public class CustomResourceGenerationTest extends BaseTest {
assertEquals(3, result.getMessages().size());
assertEquals("Error parsing JSON: the primitive value must be a boolean", result.getMessages().get(0).getMessage());
assertEquals("This property must be an Array, not a a primitive property", result.getMessages().get(1).getMessage());
assertEquals("This property must be an Array, not a primitive property", result.getMessages().get(1).getMessage());
assertEquals("Unrecognised property '@id1'", result.getMessages().get(2).getMessage());
}

View File

@ -369,7 +369,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest {
ValidationResult result = val.validateWithResult(operationDefinition);
List<SingleValidationMessage> all = logResultsAndReturnAll(result);
assertFalse(result.isSuccessful());
assertEquals("This property must be an Array, not a a primitive property", all.get(0).getMessage());
assertEquals("This property must be an Array, not a primitive property", all.get(0).getMessage());
}
@Test

52
pom.xml
View File

@ -701,6 +701,20 @@
<id>janol77</id>
<name>Alejandro Medina</name>
</developer>
<developer>
<id>KevinDougan-SmileCDR</id>
<name>Kevin Dougan</name>
</developer>
<developer>
<id>jpercival</id>
<name>Jonathan Percival</name>
<organization>Alphora</organization>
</developer>
<developer>
<id>brynrhodes</id>
<name>Bryn Rhodes</name>
<organization>Alphora</organization>
</developer>
<developer>
<id>MarcelPa</id>
<name>Marcel P</name>
@ -716,7 +730,7 @@
<properties>
<fhir_core_version>5.2.16</fhir_core_version>
<fhir_core_version>5.2.20</fhir_core_version>
<ucum_version>1.0.3</ucum_version>
<surefire_jvm_args>-Dfile.encoding=UTF-8 -Xmx2048m</surefire_jvm_args>
@ -740,6 +754,8 @@
<commons_text_version>1.7</commons_text_version>
<commons_io_version>2.6</commons_io_version>
<commons_lang3_version>3.9</commons_lang3_version>
<com_jamesmurty_utils_version>1.2</com_jamesmurty_utils_version>
<cql_version>1.5.0</cql_version>
<derby_version>10.14.2.0</derby_version>
<!--<derby_version>10.15.1.3</derby_version>-->
<error_prone_core_version>2.5.1</error_prone_core_version>
@ -795,6 +811,14 @@
<ebay_cors_filter_version>1.0.1</ebay_cors_filter_version>
<elastic_apm_version>1.13.0</elastic_apm_version>
<!-- CQL Support -->
<!-- FIXME KBD: Change all of these -SNAPSHOT versions to release versions
(e.g. 1.3.1-SNAPSHOT -> 1.3.1) before finalizing this work! -->
<cqf-tooling.version>1.3.1-SNAPSHOT</cqf-tooling.version>
<cql-engine.version>1.5.1-SNAPSHOT</cql-engine.version>
<cql-evaluator.version>1.1.0-SNAPSHOT</cql-evaluator.version>
<cqframework.version>1.5.2-SNAPSHOT</cqframework.version>
<!-- Site properties -->
<fontawesomeVersion>5.4.1</fontawesomeVersion>
</properties>
@ -867,10 +891,10 @@
<version>0.3.1</version>
</dependency>
<dependency>
<groupId>io.dogote</groupId>
<artifactId>json-patch</artifactId>
<version>1.15</version>
</dependency>
<groupId>io.dogote</groupId>
<artifactId>json-patch</artifactId>
<version>1.15</version>
</dependency>
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
@ -901,6 +925,11 @@
<artifactId>ph-commons</artifactId>
<version>${ph_commons_version}</version>
</dependency>
<dependency>
<groupId>com.jamesmurty.utils</groupId>
<artifactId>java-xmlbuilder</artifactId>
<version>${com_jamesmurty_utils_version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
@ -1177,12 +1206,12 @@
<artifactId>apache-jena-libs</artifactId>
<version>${jena_version}</version>
<type>pom</type>
<exclusions>
<exclusions>
<exclusion>
<groupId>com.github.jsonld-java</groupId>
<artifactId>jsonld-java</artifactId>
</exclusion>
</exclusions>
<groupId>com.github.jsonld-java</groupId>
<artifactId>jsonld-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
@ -2556,7 +2585,7 @@
<activeByDefault>true</activeByDefault>
</activation>
<modules>
<module>hapi-fhir-bom</module>
<module>hapi-fhir-bom</module>
<module>hapi-deployable-pom</module>
<module>hapi-fhir-base</module>
<module>hapi-fhir-docs</module>
@ -2582,6 +2611,7 @@
<module>hapi-fhir-structures-r5</module>
<module>hapi-fhir-validation-resources-r5</module>
<module>hapi-fhir-jpaserver-api</module>
<module>hapi-fhir-jpaserver-cql</module>
<module>hapi-fhir-jpaserver-model</module>
<module>hapi-fhir-jpaserver-searchparam</module>
<module>hapi-fhir-jpaserver-subscription</module>