Clinical reasoning module (#4347)

* Add new Clinical Reasoning module

* Add config to get test up and running

* Removed FhirClient from tests

* First passing test

* Remove unused utilities

* Build updates

* exclude xxp3 and substitute with unbundled version

* Refactor to use provider factory and loader pattern

* First passing dstu3 test

* CR r4 test config and immunization tests

* Add base test classes

* Exclude transitive dependency xmlpull

* More dependency cleanup

* Refactor to remove JPA dependencies, rename module to hapi-fhir-storage-cr

* Fixes for duplicate xpp3 classes

* Remove more references to jpa

* Add mock FHIR server for terminology endpoint tests

* fix cql file for making providerr4 tests pass

* fix pom

* fix generic

* Adding r4 test cases for measure operations.

* Fix endpoint tests

* javadoc comment, temporarily comment out test results for practitioner sync

* javadoc typo fix

* adding java docs for cr module WIP

* Updates to clinical reasoning documentation

* More documentation updates

* docs now compile

* fix test

* DqmR4IT now successfully starts up, but the test does not pass

* Fix additionalData test

* Make measure service bean overridable

* adding clinical reasoning javadoc information

* Rename interfaces to start with I

* More styleguide updates

* Reformat all code in the storage-cr module

* More coding convention updates

* Remove bunches of unused code, reogranize so that fhir version in package name is consistent

* Clean up some static array initializations

* add practitioner patient list

* refactor product-line constant

* test dependency update

* add change log for cr module

* Add hapi-validation to the pom

* Exclude dependencies that are causing duplicates

* More dependencies and exclusions

* Make some base MeasureService components protected

* Update to latest cql compiler release

* fix measure service for null subject & practitioner and open up practitioner test

* Rev to release versions

* Add remaining Dstu3 tests

* update hedis bundle from policyHolder to beneficiary

* update snapshot

* Delete old CQL module

* Post-master-merge recovery

* Fix Java 17 feature usage issue

* update changelog with more information about new functionality

* Remove support chain wrapper usage

* Add javadocs to IResourceLoader

* added the property versioning for antlr jar.

* addressing code review comments.

* addressing code review comments.

* Fix typos in changelog

* Remove azure pipeline for cql module, add it for cr module

* Address exception comments, add Msg.code calls to exceptions

* Fix self-assignment bug

* Add storage-cr module to bom

* addressing coding comments.

* Addressed the variable naming convention to camelCase.

* Snapshot version bump

Co-authored-by: Brenin Rhodes <brenin@alphora.com>
Co-authored-by: Jonathan Percival <jonathan.i.percival@gmail.com>
Co-authored-by: justin.mckelvy <justin.mckelvy@smilecdr.com>
Co-authored-by: Ken Stevens <ken@smilecdr.com>
Co-authored-by: Chalma Maadaadi <chalma@alphora.com>
Co-authored-by: mdnazmulkarim <nazmul.karim@smilecdr.com>
Co-authored-by: Tadgh <garygrantgraham@gmail.com>
This commit is contained in:
Justin McKelvy 2023-01-10 19:49:57 +00:00 committed by GitHub
parent 65bf8d47ce
commit 8c48a6fb91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
211 changed files with 101029 additions and 49674 deletions

View File

@ -57,8 +57,6 @@ stages:
# Put to top, but kept in order here
# - name: hapi_fhir_jpaserver_base
# module: hapi-fhir-jpaserver-base
- name: hapi_fhir_jpaserver_cql
module: hapi-fhir-jpaserver-cql
- name: hapi_fhir_jpaserver_elastic_test_utilities
module: hapi-fhir-jpaserver-elastic-test-utilities
- name: hapi_fhir_jpaserver_mdm
@ -106,6 +104,8 @@ stages:
module: hapi-fhir-storage-mdm
- name: hapi_fhir_storage_test_utilities
module: hapi-fhir-storage-test-utilities
- name: hapi_fhir_storage_cr
module: hapi-fhir-storage-cr
- name: hapi_fhir_structures_dstu2
module: hapi-fhir-structures-dstu2
- name: hapi_fhir_structures_dstu2_1

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,14 +4,14 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-bom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<packaging>pom</packaging>
<name>HAPI FHIR BOM</name>
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -152,6 +152,11 @@
<artifactId>hapi-fhir-storage-batch2-jobs</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>hapi-fhir-storage-cr</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>hapi-fhir-storage-mdm</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -0,0 +1,4 @@
---
type: add
issue: 4294
title: "The `hapi-fhir-jpaserver-cql` module has been replaced with a module called `hapi-fhir-storage-cr`. This was done to enable CQL evaluation and other clinical reasoning operations on non-JPA servers (like servers using MongoDb). Additionally, the new module supports some additional features for Measure evaluation, such as stratifiers and CQL language-level 1.5. The CQL engine and related components have also been upgraded to the latest versions."

View File

@ -75,10 +75,6 @@ page.server_jpa_mdm.mdm_details=MDM Technical Details
page.server_jpa_mdm.mdm_expansion=MDM Search Expansion
page.server_jpa_mdm.mdm_customizations=MDM Customizations
section.server_jpa_cql.title=JPA Server: CQL
page.server_jpa_cql.cql=CQL Getting Started
page.server_jpa_cql.cql_measure=CQL Measure
section.server_jpa_partitioning.title=JPA Server: Partitioning and Multitenancy
page.server_jpa_partitioning.partitioning=Partitioning and Multitenancy
page.server_jpa_partitioning.partition_interceptor_examples=Partition Interceptor Examples

View File

@ -1,43 +0,0 @@
# CQL Getting Started
## Introduction
Clinical Quality Language (CQL) is a high-level, domain-specific language focused on clinical quality and targeted at measure and decision support artifact authors. HAPI embeds a [CQL engine](https://github.com/DBCG/cql_engine) allowing the evaluation of clinical knowledge artifacts that use CQL to describe their logic.
A more detailed description of CQL is available at the [CQL Specification Implementation Guide](https://cql.hl7.org/)
The FHIR [Clinical Reasoning module](http://www.hl7.org/fhir/clinicalreasoning-module.html) defines a set of resources, profiles, operations, etc. that can be used to work with clinical knowledge within FHIR. HAPI provides implementation for some of those operations, described in more detail below.
## Working Example
A complete working example of HAPI CQL can be found in the [JPA Server Starter](/hapi-fhir/docs/server_jpa/get_started.html) project. You may wish to browse its source to see how it is set up.
## Overview
To get up and running with HAPI CQL, you can enable it using the `hapi.properties` file in the JPA Server Starter by setting `hapi.fhir.enable_cql` key to `true`. If you are running your own server follow the instructions below to [enable it in HAPI FHIR directly](#cql-settings).
Once you've enabled CQL processing, the next step is to load the appropriate knowledge artifact resources into your server.
## CQL Settings
There are two Spring beans available that add CQL processing to HAPI. You can enable CQL processing by importing the appropriate version for your server configuration.
* `ca.uhn.fhir.cql.config.CqlDstu3Config`
* `ca.uhn.fhir.cql.config.CqlR4Config`
## Clinical Reasoning Operations
HAPI provides implementations for some operations in DSTU3 and R4:
[CQL Measure](cql_measure.html)
## Roadmap
Further development of the CQL capabilities in HAPI is planned:
* Additional features and performance enhancements for Measure evaluation
* Additional FHIR Clinical Reasoning Module operations:
* Library $evaluate
* PlanDefinition $apply
* Support for the CPG IG Operations
* $cql

View File

@ -1,406 +0,0 @@
# CQL Measure
## Introduction
The FHIR Clinical Reasoning Module defines the [Measure resource](https://www.hl7.org/fhir/measure.html) and several [associated operations](https://www.hl7.org/fhir/measure-operations.html). The Measure Resource represents a structured, computable definition of a health-related measure such as a clinical quality measure, public health indicator, or population analytics measure. These Measures can then be used for reporting, analytics, and data-exchange purposes.
Electronic Clinical Quality Measures (eCQMs) in FHIR are represented as a FHIR Measure resource containing metadata and terminology, a population criteria section, and at least one FHIR Library resource containing a data criteria section as well as the logic used to define the population criteria. The population criteria section typically contains initial population criteria, denominator criteria, and numerator criteria sub-components, among others. This is elaborated upon in greater detail in the [CQF Measures IG](http://hl7.org/fhir/us/cqfmeasures). An example of an eCQM as defined in FHIR looks like:
```json
{
"resourceType" : "Measure",
"library" : [
"http://hl7.org/fhir/us/cqfmeasures/Library/EXMLogic"
],
"group" : [
{
"population" : [
{
"code" : {
"coding" : [
{
"code" : "initial-population"
}
]
},
"criteria" : {
"language" : "text/cql.identifier",
"expression" : "Initial Population"
}
},
{
"code" : {
"coding" : [
{
"code" : "numerator"
}
]
},
"criteria" : {
"language" : "text/cql.identifier",
"expression" : "Numerator"
}
},
{
"code" : {
"coding" : [
{
"code" : "denominator"
}
]
},
"criteria" : {
"language" : "text/cql.identifier",
"expression" : "Denominator"
}
}
]
}
]
}
```
Measures are then scored according the whether a subjects (or subjects) are members of the various populations.
For example, a Measure for Breast Cancer screening might define an Initial Population (via CQL expressions) of "all women", a Denominator of "women over 35", and a Numerator of "women over 35 who have had breast cancer screenings in the past year". If the Measure is evaluated against a population of 100 women, 50 are over 35, and of those 25 have had breast cancer screenings in the past year, the final score would be 50%<sup>1</sup> (total number in numerator / total number in the denominator).
1. There are several methods for scoring Measures, this is meant only as an example.
## Operations
HAPI implements the [$evaluate-measure](https://www.hl7.org/fhir/operation-measure-evaluate-measure.html) operation. Support for additional operations is planned.
## Evaluate Measure
The `$evaluate-measure` operation is used to execute a Measure as specified by the relevant FHIR Resources against a subject or set of subjects. This implementation currently focuses primarily on supporting the narrower evaluation requirements defined by the [CQF Measures IG](http://hl7.org/fhir/us/cqfmeasures). Some support for extensions defined by other IGs is included as well, and the implementation aims to support a wider range of functionality in the future.
### Example Measure
Several example Measures are available in the [ecqm-content-r4](https://github.com/cqframework/ecqm-content-r4) IG. Full Bundles with all the required supporting resources are available [here](https://github.com/cqframework/ecqm-content-r4/tree/master/bundles/measure). You can download a Bundle and load it on your server as a transaction:
```bash
POST http://your-server-base/fhir BreastCancerScreeningFHIR-bundle.json
```
These Bundles also include example Patient clinical data so once posted Measure evaluation can be invoked with:
```bash
GET http://your-server-base/fhir/Measure/BreastCancerScreeningFHIR/$evaluate-measure?periodStart=2019-01-01&periodEnd=2019-12-31&subject=numerator&reportType=subject
```
### Measure Features
The FHIR Measure specification defines several different types of Measures and various parameters for controlling the Measure evaluation. This section describes the features supported by HAPI.
#### Reporting Period
The `periodStart` and `periodEnd` parameters are used to control the Reporting Period for which a report is generated. This corresponds to `Measurement Period` defined in the CQL logic, as defined by the conformance requirements in the CQF Measures IG. Both `periodStart` and `periodEnd` must be used or neither must be used.
If neither are used the default reporting period specified in the CQL logic is used, as shown here
```cql
parameter "Measurement Period" Interval<DateTime>
default Interval[@2019-01-01T00:00:00.0, @2020-01-01T00:00:00.0)
```
If neither are used and there is no default reporting period in the CQL logic an error is thrown.
A request using `periodStart` and `periodEnd` looks like:
```bash
GET fhir/Measure/<MeasureId>/$evaluate-measure?periodStart=2019-01-01&periodEnd=2019-12-31
```
`periodStart` and `periodEnd` support Dates (YYYY, YYYY-MM, or YYYY-MM-DD) and DateTimes (YYYY-MM-DDThh:mm:ss+zz:zz)
#### Report Types
Measure report types determine what data is returned from the evaluation. This is controlled with the `reportType` parameter on the $evaluate-measure Operation
| Report Type | Supported | Description |
| ------------ | :----------------: | -------------------------------------------------------------------------------------------------------------- |
| subject | :white_check_mark: | Measure report for a single subject (e.g. one patient). Includes additional detail, such as evaluatedResources |
| subject-list | :white_check_mark: | Measure report including the list of subjects in each population (e.g. all the patients in the "numerator") |
| population | :white_check_mark: | Summary measure report for a population |
NOTE: There's an open issue on the FHIR specification to align these names to the MeasureReportType value set.
A request using `reportType` looks like:
```bash
GET fhir/Measure/<MeasureId>/$evaluate-measure?reportType=subject-list
```
#### Subject Types
The subject of a measure evaluation is controlled with the `subject` (R4+) and `patient` (DSTU3) operation parameters. Currently the only subject type supported by HAPI is Patient. This means that all Measure evaluation and reporting happens with respect to a Patient or set of Patient resources.
| Subject Type | Supported | Description |
| ----------------- | :------------------: | ----------------- |
| Patient | :white_check_mark: | A Patient |
| Practitioner | :white_large_square: | A Practitioner |
| Organization | :white_large_square: | An Organization |
| Location | :white_large_square: | A Location |
| Device | :white_large_square: | A Device |
| Group<sup>1</sup> | :white_large_square: | A set of subjects |
1. See next section
A request using `subject` looks like:
```bash
GET fhir/Measure/<MeasureId>/$evaluate-measure?subject=Patient/123
```
##### Selecting a set of Patients
The set of Patients used for Measure evaluation is controlled with the `subject` (R4+) or `patient` (DSTU3), and `practitioner` parameters. The two parameters are mutually exclusive.
| Parameter | Supported | Description |
| ----------------------------------------------------- | :------------------: | ----------------------------------------------------------------------- |
| Not specified | :white_check_mark: | All Patients on the server |
| `subject=XXX` or `subject=Patient/XXX` | :white_check_mark: | A single Patient |
| `practitioner=XXX` or `practitioner=Practitioner/XXX` | :white_check_mark: | All Patients whose `generalPractitioner` is the referenced Practitioner |
| `subject=Group/XXX`<sup>1</sup> | :white_large_square: | A Group containing subjects |
| `subject=XXX` AND `practitioner=XXX` | :x: | Not a valid combination |
1. Referencing a Group of Patients as the subject is defined in the ATR IG and is on the roadmap. This will allow much more control over which Patients are included in the evaluated set.
A request using `practitioner` looks like:
```bash
GET fhir/Measure/<MeasureId>/$evaluate-measure?practitioner=Practitioner/XYZ
```
#### ReportType, Subject, Practitioner Matrix
The following table shows the combinations of the `subject` (or `patient`), `practitioner` and `reportType` parameters that are valid
| | subject reportType | subject-list reportType | population reportType |
| ---------------- | :----------------: | :-------------------------------: | :-------------------------------: |
| subject parameter | :white_check_mark: | :white_check_mark: <sup>1,2</sup> | :white_check_mark: <sup>1,2</sup> |
| practitioner parameter | :x:<sup>3</sup> | :white_check_mark: | :white_check_mark: |
1. Including the subject parameter restricts the Measure evaluation to a single Patient. Omit the `subject` (or `patient`) parameter to get report for multiple Patients. The subject-list and population report types have less detail than a subject report.
2. A Group `subject` with a subject-list or population `reportType` will be a valid combination once Group support is implemented.
3. A practitioner have may zero, one, or many patients so a practitioner report always assumes a set.
#### Scoring Methods
The Measure scoring method determines how a Measure score is calculated. It is set with the [scoring](https://www.hl7.org/fhir/measure-definitions.html#Measure.scoring) element on the Measure resource.
The HAPI implementation conforms to the requirements defined by the CQF Measures IG. A more detailed description of each scoring method is linked in the table below.
| Scoring Method | Supported | Description |
| ------------------- | :------------------: | ---------------------------------------------------------------------------------------------------------------------- |
| proportion | :white_check_mark: | [Proportion Measures](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#proportion-measures) |
| ratio | :white_check_mark: | [Ratio Measures](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#ratio-measures) |
| continuous-variable | :white_check_mark: | [Continuous Variable](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#continuous-variable-measure) |
| cohort | :white_check_mark:* | [Cohort](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#cohort-definitions) |
| composite | :white_large_square: | See below |
* The cohort Measure scoring support is partial. The HAPI implementation does not yet return the required Measure observations
An example Measure resource with `scoring` defined looks like:
```json
{
"resourceType": "Measure",
"scoring": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/measure-scoring",
"code": "proportion",
"display": "Proportion"
} ]
}
}
```
##### Composite Scoring
A composite Measure is scored by combining and/or aggregating the results of other Measures. The [compositeScoring](https://www.hl7.org/fhir/measure-definitions.html#Measure.compositeScoring) element is used to control how composite Measures are scored. HAPI does not currently support any composite scoring method.
| Composite Scoring Method | Supported | Description |
| ------------------------ | :------------------: | ---------------------------------------------------------------------------------------------- |
| opportunity | :white_large_square: | Combines Numerators and Denominators for each component Measure |
| all-or-nothing | :white_large_square: | Includes individuals that are in the numerator for all component Measures |
| linear | :white_large_square: | Gives an individual score based on the number of numerators in which they appear |
| weighted | :white_large_square: | Gives an individual a cored based on a weighted factor for each numerator in which they appear |
#### Populations
The HAPI implementation uses the populations defined by the CQF Measures IG for each scoring type. A matrix of the supported populations is shown in the [Criteria Names](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#criteria-names) section of the CQF Measures IG.
#### Population Criteria
The logical criteria used for determining each Measure population is defined by the [Measure.group.population.criteria](https://hl7.org/fhir/R4/measure-definitions.html#Measure.group.population.criteria) element. The Measure specification allows population criteria to be defined using FHIR Path, CQL, or other languages as appropriate. The HAPI implementation currently only supports using CQL. The relationship between a Measure Population and CQL is illustrated in the [Population Criteria](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#population-criteria) section of the CQF Measures IG.
An example Measure resource with a population criteria referencing a CQL identifier looks like:
```json
{
"resourceType": "Measure",
"group": [ {
"population": [ {
"code": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/measure-population",
"code": "initial-population",
"display": "Initial Population"
} ]
},
"criteria": {
"language": "text/cql.identifier",
"expression": "Initial Population"
}
}]
}]
}
```
##### Criteria Expression Type
| Expression Type | Supported |
| --------------- | :------------------: |
| CQL | :white_check_mark: |
| FHIR Path | :white_large_square: |
#### Supplemental Data Elements
Supplemental Data Elements are used to report additional information about the subjects that may not be included in the in the Population criteria definitions. For example, it may be of interest to report the gender of all subjects for informational purposes. Supplemental data elements are defined by the [Measure.supplementalData](http://www.hl7.org/fhir/measure-definitions.html#Measure.supplementalData) element, and are reported as Observations in the evaluatedResources of the MeasureReport.
Supplemental Data Elements can be specified as either CQL definitions or FHIR Path expressions.
| Expression Type | Supported |
| --------------- | :------------------: |
| CQL | :white_check_mark: |
| FHIR Path | :white_large_square: |
An example Measure resource with some supplemental data elements set looks like:
```json
{
"resourceType": "Measure",
"supplementalData": [ {
"code": {
"text": "sde-ethnicity"
},
"criteria": {
"language": "text/cql.identifier",
"expression": "SDE Ethnicity"
}
}]
}
```
#### Stratifiers
Stratifiers are used divide Measure populations into segments of interest. For example, it may be of interest to compare the Measure score between different age groups or genders. Each stratum within a stratification is scored the same way as the overall population. Stratifiers are defined using the [Measure.group.stratifier](http://hl7.org/fhir/R4/measure-definitions.html#Measure.group.stratifier) element.
HAPI does not implement stratifier support but it's on the roadmap.
An example Measure resource with a stratifier set looks like:
```json
{
"resourceType": "Measure",
"group": [ {
"stratifier": [ {
"code": {
"text": "Stratum 1"
},
"criteria": {
"language": "text/cql.identifier",
"expression": "Stratification 1"
}
}]
}]
}
```
##### Stratifier Expression Support
As with Populations and Supplemental Data Elements the criteria used for Stratification may be defined with CQL or FHIR Path.
| Expression Type | Supported |
| --------------- | :------------------: |
| CQL | :white_large_square: |
| FHIR Path | :white_large_square: |
##### Stratifier Component Support
The Measure specification also supports multi-dimensional stratification, for cases where more than one data element is needed.
| Stratifier Type | Supported |
| ---------------- | :------------------: |
| Single Component | :white_large_square: |
| Multi Component | :white_large_square: |
#### Evaluated Resources
A FHIR MeasureReport permits referencing the Resources used when evaluating in the [MeasureReport.evaluatedResource](https://www.hl7.org/fhir/measurereport-definitions.html#MeasureReport.evaluatedResource) element. HAPI includes these resources when generating `subject` reports for a single Patient. Evaluated resources for `population` or `subject-list` reports are not included. For large populations this could quickly become an extremely large number of resources.
The evaluated resources will not include every resource on the HAPI server for a given subject. Rather, it includes only the resources that were retrieved from the server by the CQL logic that was evaluated. This corresponds to the data-requirements for a given Measure. As an example, consider the following CQL:
```cql
valueset "Example Value Set" : 'http://fhir.org/example-value-set'
define "Example Observations":
[Observation : "Example Value Set"]
```
That CQL will only select Observation Resources that have a code in the "Example Value Set". Those Observations will be reported in the Evaluated Resources while any others will not.
#### Last Received On
The `lastReceivedOn` parameter is the date the Measure was evaluated and reported. It is used to limit the number of resources reported in the Measure report for individual reports. It is currently not supported by HAPI.
#### Extensions
A number of extensions to Measure evaluation defined by various IGs are supported. They are described briefly in the table below.
| Extension | Description |
| --------- | ----------- |
| http://hl7.org/fhir/us/cqframework/cqfmeasures/StructureDefinition/cqfm-productLine | Used to evaluate different product lines (e.g. Medicare, Private, etc.) |
| http://hl7.org/fhir/StructureDefinition/cqf-measureInfo | Used to demark a Measure Observation |
| http://hl7.org/fhir/us/davinci-deqm/StructureDefinition/extension-populationReference | Used to specify the population that triggered a particular `evaluatedResource`|
There's not currently a way to configure which extensions are enabled. All supported extensions are always enabled.
## Architecture
Below are a few diagrams that show the overall architecture of Measure evaluation and how it fits into the HAPI FHIR Server.
### Component Diagram
This is a simplified component diagram of the Measure evaluation architecture
![Measure Evaluation Architecture](/hapi-fhir/docs/images/ref_measure_architecture_drawio.svg)
### Sequence Chart
This sequence chart approximates the Measure evaluation logic implemented by HAPI.
![Measure Evaluation Sequence Chart](/hapi-fhir/docs/images/measure_evaluation_sequence.png)
## FAQs
Q: I get an error saying HAPI can't locate my library, and I've verified it's on the server.
A: HAPI follows the [Library conformance requirements](https://build.fhir.org/ig/HL7/cqf-measures/measure-conformance.html#conformance-requirement-3-1) defined by the CQF Measures IG, meaning the Library must have a `logic-library` type, the name and versions of the FHIR Library and CQL Library must match, and the url of the Library must end in the name of the Library.
FHIR Libraries generated from CQL via the IG Publisher follow these requirements automatically.
Q: Does HAPI support partitions for evaluation?
A: Yes, though the Measure and associated Resources must be in the same partition as the clinical data being used.
## Roadmap
* Complete cohort implementation
* Support for stratifiers
* Support for Group subjects
* Support for FHIRPath expressions in Stratifiers, Supplemental Data Elements, and Population Criteria
* `$data-requirements`, `$collect-data`, `$submit-data`, and `$care-gaps` operations
* Support for more extensions defined in the CQF Measures, CPG, and ATR IGs

View File

@ -11,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -1,130 +0,0 @@
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

@ -1,218 +0,0 @@
<?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>6.3.6-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>
<exclusions>
<!-- we exclude this because we don't want javax.mail and will use jakarta.mail instead -->
<exclusion>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
</exclusion>
<exclusion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client</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>
<exclusion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client</artifactId>
</exclusion>
<exclusion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu2</artifactId>
</exclusion>
<exclusion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-hl7org-dstu2</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>info.cqframework</groupId>
<artifactId>quick</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>
<exclusions>
<exclusion>
<groupId>javax.activation</groupId>
<artifactId>javax.activation-api</artifactId>
</exclusion>
</exclusions>
</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>
<!-- test -->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-caching-testing</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<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>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-storage-test-utilities</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-jpaserver-test-utilities</artifactId>
<version>${project.version}</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

@ -1,123 +0,0 @@
package ca.uhn.fhir.cql.common.evaluation;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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(Msg.code(1657) + errorsToString(errors));
}
try {
CqlTranslator translator = getTranslator("", libraryManager, modelManager);
if (translator.getErrors().size() > 0) {
throw new IllegalArgumentException(Msg.code(1658) + errorsToString(translator.getErrors()));
}
return readLibrary(new ByteArrayInputStream(
translator.convertToXml(translatedLibrary).getBytes(StandardCharsets.UTF_8)));
} catch (JAXBException e) {
throw new IllegalArgumentException(Msg.code(1659) + 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

@ -1,104 +0,0 @@
package ca.uhn.fhir.cql.common.evaluation;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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(Msg.code(1655) + "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

@ -1,85 +0,0 @@
package ca.uhn.fhir.cql.common.evaluation;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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(Msg.code(1656) + "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

@ -1,45 +0,0 @@
package ca.uhn.fhir.cql.common.helper;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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 paramName, String date) {
if (StringUtils.isBlank(date)) {
throw new IllegalArgumentException(Msg.code(1662) + paramName + " parameter cannot be blank!");
}
return new DateTimeType(date).getValue();
}
}

View File

@ -1,102 +0,0 @@
package ca.uhn.fhir.cql.common.helper;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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(Msg.code(1660) + "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(Msg.code(1661) + 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

@ -1,63 +0,0 @@
package ca.uhn.fhir.cql.common.helper;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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

@ -1,38 +0,0 @@
package ca.uhn.fhir.cql.common.provider;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.rest.api.server.RequestDetails;
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 {
DataProvider createDataProvider(String model, String version, RequestDetails theRequestDetails);
DataProvider createDataProvider(String model, String version, String url, String user, String pass, RequestDetails theRequestDetails);
DataProvider createDataProvider(String model, String version, TerminologyProvider terminologyProvider, RequestDetails theRequestDetails);
TerminologyProvider createTerminologyProvider(String model, String version, String url, String user,
String pass);
}

View File

@ -1,76 +0,0 @@
package ca.uhn.fhir.cql.common.provider;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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 org.opencds.cqf.cql.evaluator.cql2elm.content.LibraryContentType;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.function.Function;
public class LibraryContentProvider<LibraryType, AttachmentType>
implements org.opencds.cqf.cql.evaluator.cql2elm.content.LibraryContentProvider {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LibraryContentProvider.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 LibraryContentProvider(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 getLibraryContent(VersionedIdentifier versionedIdentifier, LibraryContentType libraryContentType){
// TODO: Support loading ELM
if (libraryContentType != LibraryContentType.CQL) {
return null;
}
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

@ -1,104 +0,0 @@
package ca.uhn.fhir.cql.common.provider;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.rest.api.server.RequestDetails;
import java.util.function.Function;
public interface LibraryResolutionProvider<LibraryType> {
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;
}
LibraryType resolveLibraryById(String libraryId, RequestDetails theRequestDetails);
LibraryType resolveLibraryByName(String libraryName, String libraryVersion);
LibraryType resolveLibraryByCanonicalUrl(String libraryUrl, RequestDetails theRequestDetails);
// 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;
}
// Hmmm... Probably need to think through this use case a bit more.
// Should we throw an exception? Should this be a different interface?
void update(LibraryType library);
}

View File

@ -1,114 +0,0 @@
package ca.uhn.fhir.cql.common.retrieve;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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 ca.uhn.fhir.rest.api.server.RequestDetails;
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);
private final DaoRegistry registry;
private final RequestDetails myRequestDetails;
@Autowired
public JpaFhirRetrieveProvider(DaoRegistry registry, SearchParameterResolver searchParameterResolver, RequestDetails theRequestDetails) {
super(searchParameterResolver);
this.registry = registry;
myRequestDetails = theRequestDetails;
}
@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 = ca.uhn.fhir.jpa.searchparam.SearchParameterMap.newSynchronous();
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, myRequestDetails);
if (bundleProvider.size() == null) {
return resolveResourceList(bundleProvider.getResources(0, 10000));
}
List<IBaseResource> resourceList = bundleProvider.getAllResources();
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

@ -1,55 +0,0 @@
package ca.uhn.fhir.cql.config;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.cqframework.cql.elm.execution.Library;
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>();
}
@Bean(name="globalLibraryCache")
Map<org.cqframework.cql.elm.execution.VersionedIdentifier, Library> globalLibraryCache() {
return new ConcurrentHashMap<org.cqframework.cql.elm.execution.VersionedIdentifier, Library>();
}
}

View File

@ -1,103 +0,0 @@
package ca.uhn.fhir.cql.config;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.listener.ElmCacheResourceChangeListener;
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.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import org.cqframework.cql.cql2elm.CqlTranslatorOptions;
import org.cqframework.cql.cql2elm.model.Model;
import org.cqframework.cql.elm.execution.Library;
import org.hl7.elm.r1.VersionedIdentifier;
import org.opencds.cqf.cql.engine.fhir.model.Dstu3FhirModelResolver;
import org.opencds.cqf.cql.engine.model.ModelResolver;
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;
import java.util.Map;
@Configuration
public class CqlDstu3Config extends BaseCqlConfig {
@Lazy
@Bean
TerminologyProvider terminologyProvider(ITermReadSvc theITermReadSvc, DaoRegistry theDaoRegistry, IValidationSupport theValidationSupport) {
return new JpaTerminologyProvider(theITermReadSvc, theDaoRegistry, theValidationSupport);
}
@Lazy
@Bean
EvaluationProviderFactory evaluationProviderFactory(FhirContext theFhirContext, DaoRegistry theDaoRegistry,
TerminologyProvider theLocalSystemTerminologyProvider, ModelResolver modelResolver) {
return new ProviderFactory(theFhirContext, theDaoRegistry, theLocalSystemTerminologyProvider, modelResolver);
}
@Lazy
@Bean
LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> libraryResolutionProvider() {
return new LibraryResolutionProviderImpl();
}
@Lazy
@Bean
public MeasureOperationsProvider measureOperationsProvider() {
return new MeasureOperationsProvider();
}
@Lazy
@Bean
public ModelResolver fhirModelResolver() {
return new CachingModelResolverDecorator(new Dstu3FhirModelResolver());
}
@Lazy
@Bean
public LibraryHelper libraryHelper(Map<VersionedIdentifier, Model> globalModelCache, Map<org.cqframework.cql.elm.execution.VersionedIdentifier, Library> globalLibraryCache, CqlTranslatorOptions cqlTranslatorOptions) {
return new LibraryHelper(globalModelCache, globalLibraryCache, cqlTranslatorOptions);
}
@Bean
public CqlTranslatorOptions cqlTranslatorOptions() {
return CqlTranslatorOptions.defaultOptions().withCompatibilityLevel("1.3");
}
@Bean
public ElmCacheResourceChangeListener elmCacheResourceChangeListener(IResourceChangeListenerRegistry resourceChangeListenerRegistry, IFhirResourceDao<org.hl7.fhir.dstu3.model.Library> libraryDao, Map<org.cqframework.cql.elm.execution.VersionedIdentifier, Library> globalLibraryCache) {
ElmCacheResourceChangeListener listener = new ElmCacheResourceChangeListener(libraryDao, globalLibraryCache);
resourceChangeListenerRegistry.registerResourceResourceChangeListener("Library", SearchParameterMap.newSynchronous(), listener, 1000);
return listener;
}
}

View File

@ -1,115 +0,0 @@
package ca.uhn.fhir.cql.config;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.evaluation.ProviderFactory;
import ca.uhn.fhir.cql.r4.helper.LibraryHelper;
import ca.uhn.fhir.cql.r4.listener.ElmCacheResourceChangeListener;
import ca.uhn.fhir.cql.r4.provider.JpaTerminologyProvider;
import ca.uhn.fhir.cql.r4.provider.LibraryResolutionProviderImpl;
import ca.uhn.fhir.cql.r4.provider.MeasureOperationsProvider;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import org.cqframework.cql.cql2elm.CqlTranslatorOptions;
import org.cqframework.cql.cql2elm.model.Model;
import org.cqframework.cql.elm.execution.Library;
import org.hl7.elm.r1.VersionedIdentifier;
import org.opencds.cqf.cql.engine.fhir.model.R4FhirModelResolver;
import org.opencds.cqf.cql.engine.model.ModelResolver;
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;
import java.util.Map;
@Configuration
public class CqlR4Config extends BaseCqlConfig {
@Override
@Lazy
@Bean
CqlProviderFactory cqlProviderFactory() {
return new CqlProviderFactory();
}
@Lazy
@Bean
TerminologyProvider terminologyProvider(ITermReadSvc theITermReadSvc, DaoRegistry theDaoRegistry,
IValidationSupport theValidationSupport) {
return new JpaTerminologyProvider(theITermReadSvc, theDaoRegistry, theValidationSupport);
}
@Lazy
@Bean
EvaluationProviderFactory evaluationProviderFactory(FhirContext theFhirContext, DaoRegistry theDaoRegistry,
TerminologyProvider theLocalSystemTerminologyProvider, ModelResolver modelResolver) {
return new ProviderFactory(theFhirContext, theDaoRegistry, theLocalSystemTerminologyProvider, modelResolver);
}
@Lazy
@Bean
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResolutionProvider() {
return new LibraryResolutionProviderImpl();
}
@Lazy
@Bean
public MeasureOperationsProvider measureOperationsProvider() {
return new MeasureOperationsProvider();
}
@Lazy
@Bean
public ModelResolver fhirModelResolver() {
return new CachingModelResolverDecorator(new R4FhirModelResolver());
}
@Lazy
@Bean
public LibraryHelper libraryHelper(Map<VersionedIdentifier, Model> globalModelCache,
Map<org.cqframework.cql.elm.execution.VersionedIdentifier, Library> globalLibraryCache,
CqlTranslatorOptions cqlTranslatorOptions) {
return new LibraryHelper(globalModelCache, globalLibraryCache, cqlTranslatorOptions);
}
@Lazy
@Bean
public CqlTranslatorOptions cqlTranslatorOptions() {
return CqlTranslatorOptions.defaultOptions();
}
@Bean
public ElmCacheResourceChangeListener elmCacheResourceChangeListener(IResourceChangeListenerRegistry resourceChangeListenerRegistry, IFhirResourceDao<org.hl7.fhir.r4.model.Library> libraryDao, Map<org.cqframework.cql.elm.execution.VersionedIdentifier, Library> globalLibraryCache) {
ElmCacheResourceChangeListener listener = new ElmCacheResourceChangeListener(libraryDao, globalLibraryCache);
resourceChangeListenerRegistry.registerResourceResourceChangeListener("Library", SearchParameterMap.newSynchronous(), listener, 1000);
return listener;
}
}

View File

@ -1,69 +0,0 @@
package ca.uhn.fhir.cql.dstu3.builder;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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

@ -1,732 +0,0 @@
package ca.uhn.fhir.cql.dstu3.evaluation;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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.api.server.RequestDetails;
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.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.Quantity;
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 final Interval measurementPeriod;
private final DaoRegistry registry;
public MeasureEvaluation(DaoRegistry registry, Interval measurementPeriod) {
this.registry = registry;
this.measurementPeriod = measurementPeriod;
}
public MeasureReport evaluatePatientMeasure(Measure measure, Context context, String patientId, RequestDetails theRequestDetails) {
logger.info("Generating individual report");
if (patientId == null) {
return evaluatePopulationMeasure(measure, context, theRequestDetails);
}
Patient patient = registry.getResourceDao(Patient.class).read(new IdType(patientId), theRequestDetails);
// 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, RequestDetails theRequestDetails) {
logger.info("Generating patient-list report");
List<Patient> patients = practitionerRef == null ? getAllPatients(theRequestDetails) : getPractitionerPatients(practitionerRef, theRequestDetails);
boolean isSingle = false;
return evaluate(measure, context, patients, MeasureReport.MeasureReportType.PATIENTLIST, isSingle);
}
private List<Patient> getPractitionerPatients(String practitionerRef, RequestDetails theRequestDetails) {
SearchParameterMap map = SearchParameterMap.newSynchronous();
map.add("general-practitioner", new ReferenceParam(
practitionerRef.startsWith("Practitioner/") ? practitionerRef : "Practitioner/" + practitionerRef));
List<Patient> patients = new ArrayList<>();
IBundleProvider patientProvider = registry.getResourceDao("Patient").search(map, theRequestDetails);
List<IBaseResource> patientList = patientProvider.getAllResources();
patientList.forEach(x -> patients.add((Patient) x));
return patients;
}
private List<Patient> getAllPatients(RequestDetails theRequestDetails) {
List<Patient> patients = new ArrayList<>();
IBundleProvider patientProvider = registry.getResourceDao("Patient").search(SearchParameterMap.newSynchronous(), theRequestDetails);
List<IBaseResource> patientList = patientProvider.getAllResources();
patientList.forEach(x -> patients.add((Patient) x));
return patients;
}
public MeasureReport evaluatePopulationMeasure(Measure measure, Context context, RequestDetails theRequestDetails) {
logger.info("Generating summary report");
boolean isSingle = false;
return evaluate(measure, context, getAllPatients(theRequestDetails), 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(Msg.code(1648) + 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(Msg.code(1649) + "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());
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 Quantity(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();
codeHashSet.add(id);
if (!resources.containsKey(id)) {
resources.put(id, r);
}
}
}
context.clearEvaluatedResources();
}
}

View File

@ -1,135 +0,0 @@
package ca.uhn.fhir.cql.dstu3.evaluation;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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 ca.uhn.fhir.rest.api.server.RequestDetails;
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 final LibraryLoader libraryLoader;
private final LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> libraryResourceProvider;
private final EvaluationProviderFactory providerFactory;
private DataProvider dataProvider;
private final 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, RequestDetails theRequestDetails) {
this.measure = measure;
this.libraryHelper.loadLibraries(measure, this.libraryLoader, this.libraryResourceProvider, theRequestDetails);
// resolve primary library
Library library = this.libraryHelper.resolvePrimaryLibrary(measure, libraryLoader, this.libraryResourceProvider, theRequestDetails);
// 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(Msg.code(1647) + "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, theRequestDetails);
context.registerDataProvider(def.getRight(), dataProvider);
}
// resolve the measurement period
measurementPeriod = new Interval(DateHelper.resolveRequestDate("periodStart", periodStart), true,
DateHelper.resolveRequestDate("periodEnd", 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

@ -1,85 +0,0 @@
package ca.uhn.fhir.cql.dstu3.evaluation;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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 ca.uhn.fhir.rest.api.server.RequestDetails;
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 {
private final DaoRegistry registry;
private final TerminologyProvider defaultTerminologyProvider;
private final FhirContext fhirContext;
private final 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, RequestDetails theRequestDetails) {
return this.createDataProvider(model, version, null, null, null, theRequestDetails);
}
public DataProvider createDataProvider(String model, String version, String url, String user, String pass, RequestDetails theRequestDetails) {
TerminologyProvider terminologyProvider = this.createTerminologyProvider(model, version, url, user, pass);
return this.createDataProvider(model, version, terminologyProvider, theRequestDetails);
}
public DataProvider createDataProvider(String model, String version, TerminologyProvider terminologyProvider, RequestDetails theRequestDetails) {
if (model.equals("FHIR") && version.startsWith("3")) {
JpaFhirRetrieveProvider retrieveProvider = new JpaFhirRetrieveProvider(this.registry,
new SearchParameterResolver(this.fhirContext), theRequestDetails);
retrieveProvider.setTerminologyProvider(terminologyProvider);
retrieveProvider.setExpandValueSets(true);
return new CompositeDataProvider(this.fhirModelResolver, retrieveProvider);
}
throw new IllegalArgumentException(Msg.code(1650) + 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

@ -1,197 +0,0 @@
package ca.uhn.fhir.cql.dstu3.helper;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
import ca.uhn.fhir.cql.common.provider.LibraryContentProvider;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.apache.commons.lang3.StringUtils;
import org.cqframework.cql.cql2elm.CqlTranslatorOptions;
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.Reference;
import org.hl7.fhir.dstu3.model.RelatedArtifact;
import org.hl7.fhir.dstu3.model.Resource;
import org.opencds.cqf.cql.engine.execution.LibraryLoader;
import org.opencds.cqf.cql.evaluator.cql2elm.model.CacheAwareModelManager;
import org.opencds.cqf.cql.evaluator.engine.execution.CacheAwareLibraryLoaderDecorator;
import org.opencds.cqf.cql.evaluator.engine.execution.TranslatingLibraryLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class LibraryHelper {
private static final Logger ourLog = LoggerFactory.getLogger(LibraryHelper.class);
private final Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache;
private final Map<VersionedIdentifier, Library> libraryCache;
private final CqlTranslatorOptions translatorOptions;
public LibraryHelper(Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache, Map<VersionedIdentifier, Library> libraryCache, CqlTranslatorOptions translatorOptions) {
this.modelCache = modelCache;
this.libraryCache = libraryCache;
this.translatorOptions = translatorOptions;
}
public org.opencds.cqf.cql.engine.execution.LibraryLoader createLibraryLoader(
LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> provider) {
ModelManager modelManager = new CacheAwareModelManager(this.modelCache);
List<org.opencds.cqf.cql.evaluator.cql2elm.content.LibraryContentProvider> contentProviders = Collections.singletonList(new LibraryContentProvider<org.hl7.fhir.dstu3.model.Library, Attachment>(
provider, x -> x.getContent(), x -> x.getContentType(), x -> x.getData()));
return new CacheAwareLibraryLoaderDecorator(new TranslatingLibraryLoader(modelManager, contentProviders, translatorOptions), libraryCache);
}
public List<Library> loadLibraries(Measure measure,
LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> libraryResourceProvider, RequestDetails theRequestDetails) {
List<org.cqframework.cql.elm.execution.Library> libraries = new ArrayList<Library>();
List<String> messages = new ArrayList<>();
// 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, theRequestDetails);
if (library != null) {
if (isLogicLibrary(library)) {
libraries.add(libraryLoader
.load(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion())));
} else {
String message = "Skipping library " + library.getId() + " is not a logic library. Probably missing type.coding.system=\"http://hl7.org/fhir/library-type\"";
messages.add(message);
ourLog.warn(message);
}
}
}
if (libraries.isEmpty()) {
throw new IllegalArgumentException(Msg.code(1651) + String
.format("Could not load library source for libraries referenced in %s:\n%s", measure.getId(), StringUtils.join("\n", messages)));
}
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(), theRequestDetails);
if (library != null) {
if (isLogicLibrary(library)) {
libraries.add(libraryLoader
.load(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion())));
} else {
ourLog.warn("Library {} not included as part of evaluation context. Only Libraries with the 'logic-library' type are included.", library.getId());
}
}
}
}
}
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,
LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> libraryResourceProvider, RequestDetails theRequestDetails) {
// Library library = null;
org.hl7.fhir.dstu3.model.Library fhirLibrary = libraryResourceProvider.resolveLibraryById(libraryId, theRequestDetails);
return libraryLoader
.load(new VersionedIdentifier().withId(fhirLibrary.getName()).withVersion(fhirLibrary.getVersion()));
}
public Library resolvePrimaryLibrary(Measure measure,
LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.dstu3.model.Library> libraryResourceProvider, RequestDetails theRequestDetails) {
// default is the first library reference
String id = measure.getLibraryFirstRep().getReferenceElement().getIdPart();
Library library = resolveLibraryById(id, libraryLoader, libraryResourceProvider, theRequestDetails);
if (library == null) {
throw new IllegalArgumentException(Msg.code(1652) + String.format("Could not resolve primary library for Measure/%s.",
measure.getIdElement().getIdPart()));
}
return library;
}
}

View File

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

View File

@ -1,154 +0,0 @@
package ca.uhn.fhir.cql.dstu3.provider;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.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.i18n.Msg;
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.jpa.term.api.ITermReadSvc;
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.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 javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Christopher Schuler on 7/17/2017.
*/
@Component
public class JpaTerminologyProvider implements TerminologyProvider {
private final ITermReadSvc myTerminologySvc;
private final DaoRegistry myDaoRegistry;
private final IValidationSupport myValidationSupport;
private IFhirResourceDao<ValueSet> myValueSetDao;
@Autowired
public JpaTerminologyProvider(ITermReadSvc theTerminologySvc, DaoRegistry theDaoRegistry, IValidationSupport theValidationSupport) {
myTerminologySvc = theTerminologySvc;
myDaoRegistry = theDaoRegistry;
myValidationSupport = theValidationSupport;
}
@PostConstruct
public void init() {
myValueSetDao = myDaoRegistry.getResourceDao(ValueSet.class);
}
@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 {
// This could possibly be refactored into a single call to the underlying HAPI Terminology service. Need to think through that..,
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(Msg.code(1643) + String.format(
"Could not expand value set %s; version and code system bindings are not supported at this time.",
valueSet.getId()));
}
}
IBundleProvider bundleProvider = myValueSetDao
.search(SearchParameterMap.newSynchronous().add(ValueSet.SP_URL, new UriParam(valueSet.getId())));
List<IBaseResource> valueSets = bundleProvider.getAllResources();
if (valueSets.isEmpty()) {
throw new IllegalArgumentException(Msg.code(1644) + String.format("Could not resolve value set %s.", valueSet.getId()));
} else if (valueSets.size() == 1) {
vs = (ValueSet) valueSets.get(0);
} else {
throw new IllegalArgumentException(Msg.code(1645) + "Found more than 1 ValueSet with url: " + valueSet.getId());
}
} else {
vs = myValueSetDao.read(new IdType(valueSet.getId()));
if (vs == null) {
throw new IllegalArgumentException(Msg.code(1646) + String.format("Could not resolve value set %s.", valueSet.getId()));
}
}
// Attempt to expand the ValueSet if it's not already expanded.
if (!(vs.hasExpansion() && vs.getExpansion().hasContains())) {
vs = (ValueSet) myTerminologySvc.expandValueSet(
new ValueSetExpansionOptions().setCount(Integer.MAX_VALUE).setFailOnMissingCodeSystem(false), vs);
}
List<Code> codes = new ArrayList<>();
// If expansion was successful, use the codes.
if (vs.hasExpansion() && vs.getExpansion().hasContains()) {
for (ValueSet.ValueSetExpansionContainsComponent vsecc : vs.getExpansion().getContains()) {
codes.add(new Code().withCode(vsecc.getCode()).withSystem(vsecc.getSystem()));
}
}
// If not, best-effort based on codes. Should probably make this configurable to match the behavior of the
// underlying terminology service implementation
else if (vs.hasCompose() && vs.getCompose().hasInclude()) {
for (ValueSet.ConceptSetComponent include : vs.getCompose().getInclude()) {
for (ValueSet.ConceptReferenceComponent concept : include.getConcept()) {
if (concept.hasCode()) {
codes.add(new Code().withCode(concept.getCode()).withSystem(include.getSystem()));
}
}
}
}
return codes;
}
@Override
public Code lookup(Code code, CodeSystemInfo codeSystem) throws ResourceNotFoundException {
LookupCodeResult cs = myTerminologySvc.lookupCode(new ValidationSupportContext(myValidationSupport), codeSystem.getId(), code.getCode());
code.setDisplay(cs.getCodeDisplay());
code.setSystem(codeSystem.getId());
return code;
}
}

View File

@ -1,122 +0,0 @@
package ca.uhn.fhir.cql.dstu3.provider;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.RequestDetails;
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, RequestDetails theRequestDetails) {
try {
return myLibraryDao.read(new IdType(libraryId), theRequestDetails);
} catch (Exception e) {
throw new IllegalArgumentException(Msg.code(1641) + String.format("Could not resolve library id %s", libraryId));
}
}
@Override
public Library resolveLibraryByCanonicalUrl(String url, RequestDetails theRequestDetails) {
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 = SearchParameterMap.newSynchronous();
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, theRequestDetails);
if (bundleProvider.size() == null || bundleProvider.size() == 0) {
return null;
}
List<IBaseResource> resourceList = bundleProvider.getAllResources();
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(Msg.code(1642) + 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 = SearchParameterMap.newSynchronous();
map.add("name", new StringParam(name, true));
ca.uhn.fhir.rest.api.server.IBundleProvider bundleProvider = myLibraryDao.search(map, new SystemRequestDetails());
if (bundleProvider.size() == null || bundleProvider.size() == 0) {
return new ArrayList<>();
}
List<IBaseResource> resourceList = bundleProvider.getAllResources();
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

@ -1,126 +0,0 @@
package ca.uhn.fhir.cql.dstu3.provider;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
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.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 {
@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 = ProviderConstants.CQL_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,
RequestDetails theRequestDetails) 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, theRequestDetails);
if (measure == null) {
throw new RuntimeException(Msg.code(1639) + "Could not find Measure/" + theId.getIdPart());
}
seed.setup(measure, periodStart, periodEnd, productLine, source, user, pass, theRequestDetails);
// 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, theRequestDetails);
case "patient-list":
return evaluator.evaluatePatientListMeasure(seed.getMeasure(), seed.getContext(), practitionerRef, theRequestDetails);
case "population":
return evaluator.evaluatePopulationMeasure(seed.getMeasure(), seed.getContext(), theRequestDetails);
default:
throw new IllegalArgumentException(Msg.code(1640) + "Invalid report type: " + reportType);
}
}
// default report type is patient
MeasureReport report = evaluator.evaluatePatientMeasure(seed.getMeasure(), seed.getContext(), patientRef, theRequestDetails);
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

@ -1,84 +0,0 @@
package ca.uhn.fhir.cql.r4.builder;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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

@ -1,714 +0,0 @@
package ca.uhn.fhir.cql.r4.evaluation;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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.api.server.RequestDetails;
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.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 final DataProvider provider;
private final Interval measurementPeriod;
private final 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, String thePractitionerRef, RequestDetails theRequestDetails) {
logger.info("Generating individual report");
if (patientId == null) {
return evaluatePopulationMeasure(measure, context, thePractitionerRef, theRequestDetails);
}
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, RequestDetails theRequestDetails) {
logger.info("Generating subject-list report");
List<Patient> patients = practitionerRef == null ? getAllPatients(theRequestDetails) : getPractitionerPatients(practitionerRef, theRequestDetails);
boolean isSingle = false;
return evaluate(measure, context, patients, MeasureReport.MeasureReportType.SUBJECTLIST, isSingle);
}
private List<Patient> getPractitionerPatients(String practitionerRef, RequestDetails theRequestDetails) {
SearchParameterMap map = SearchParameterMap.newSynchronous();
map.add("general-practitioner", new ReferenceParam(
practitionerRef.startsWith("Practitioner/") ? practitionerRef : "Practitioner/" + practitionerRef));
List<Patient> patients = new ArrayList<>();
IBundleProvider patientProvider = registry.getResourceDao("Patient").search(map, theRequestDetails);
List<IBaseResource> patientList = patientProvider.getAllResources();
patientList.forEach(x -> patients.add((Patient) x));
return patients;
}
private List<Patient> getAllPatients(RequestDetails theRequestDetails) {
List<Patient> patients = new ArrayList<>();
IBundleProvider patientProvider = registry.getResourceDao("Patient").search(SearchParameterMap.newSynchronous(), theRequestDetails);
List<IBaseResource> patientList = patientProvider.getAllResources();
patientList.forEach(x -> patients.add((Patient) x));
return patients;
}
public MeasureReport evaluatePopulationMeasure(Measure measure, Context context, String thePractitionerRef, RequestDetails theRequestDetails) {
logger.info("Generating summary report");
List<Patient> patients = thePractitionerRef == null ? getAllPatients(theRequestDetails) : getPractitionerPatients(thePractitionerRef, theRequestDetails);
boolean isSingle = false;
return evaluate(measure, context, patients, 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(Msg.code(1672) + 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(Msg.code(1673) + "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());
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 Quantity(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();
codeHashSet.add(id);
if (!resources.containsKey(id)) {
resources.put(id, r);
}
}
}
context.clearEvaluatedResources();
}
}

View File

@ -1,138 +0,0 @@
package ca.uhn.fhir.cql.r4.evaluation;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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 ca.uhn.fhir.rest.api.server.RequestDetails;
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 final LibraryLoader libraryLoader;
private final LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider;
private final EvaluationProviderFactory providerFactory;
private DataProvider dataProvider;
private final 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, RequestDetails theRequestDetails) {
this.measure = measure;
this.libraryHelper.loadLibraries(measure, this.libraryLoader, this.libraryResourceProvider, theRequestDetails);
// resolve primary library
Library library = this.libraryHelper.resolvePrimaryLibrary(measure, libraryLoader, this.libraryResourceProvider, theRequestDetails);
// 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(Msg.code(1671) + "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, theRequestDetails);
context.registerDataProvider(def.getRight(), dataProvider);
}
if (periodStart != null && periodEnd != null) {
// resolve the measurement period
measurementPeriod = new Interval(DateHelper.resolveRequestDate("periodStart", periodStart), true,
DateHelper.resolveRequestDate("periodEnd", 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

@ -1,84 +0,0 @@
package ca.uhn.fhir.cql.r4.evaluation;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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 ca.uhn.fhir.rest.api.server.RequestDetails;
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 {
private final DaoRegistry myDaoRegistry;
private final TerminologyProvider myDefaultTerminologyProvider;
private final FhirContext myFhirContext;
private final ModelResolver myModelResolver;
@Autowired
public ProviderFactory(FhirContext theFhirContext, DaoRegistry theDaoRegistry, TerminologyProvider theDefaultTerminologyProvider, ModelResolver theFhirModelResolver) {
myDefaultTerminologyProvider = theDefaultTerminologyProvider;
myDaoRegistry = theDaoRegistry;
myFhirContext = theFhirContext;
myModelResolver = theFhirModelResolver;
}
public DataProvider createDataProvider(String model, String version, RequestDetails theRequestDetails) {
return this.createDataProvider(model, version, null, null, null, theRequestDetails);
}
public DataProvider createDataProvider(String model, String version, String url, String user, String pass, RequestDetails theRequestDetails) {
TerminologyProvider terminologyProvider = this.createTerminologyProvider(model, version, url, user, pass);
return this.createDataProvider(model, version, terminologyProvider, theRequestDetails);
}
public DataProvider createDataProvider(String model, String version, TerminologyProvider terminologyProvider, RequestDetails theRequestDetails) {
if (model.equals("FHIR") && version.startsWith("4")) {
JpaFhirRetrieveProvider retrieveProvider = new JpaFhirRetrieveProvider(myDaoRegistry,
new SearchParameterResolver(myFhirContext), theRequestDetails);
retrieveProvider.setTerminologyProvider(terminologyProvider);
retrieveProvider.setExpandValueSets(true);
return new CompositeDataProvider(myModelResolver, retrieveProvider);
}
throw new IllegalArgumentException(Msg.code(1674) + 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 = myDefaultTerminologyProvider;
return new PrivateCachingTerminologyProviderDecorator(terminologyProvider);
}
}

View File

@ -1,50 +0,0 @@
package ca.uhn.fhir.cql.r4.helper;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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(Msg.code(1675) + "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(Msg.code(1676) + "CanonicalType must have a value for resource name extraction");
}
}

View File

@ -1,236 +0,0 @@
package ca.uhn.fhir.cql.r4.helper;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
import ca.uhn.fhir.cql.common.provider.LibraryContentProvider;
import ca.uhn.fhir.cql.common.provider.LibraryResolutionProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.cqframework.cql.cql2elm.CqlTranslatorOptions;
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.engine.execution.LibraryLoader;
import org.opencds.cqf.cql.evaluator.cql2elm.model.CacheAwareModelManager;
import org.opencds.cqf.cql.evaluator.engine.execution.CacheAwareLibraryLoaderDecorator;
import org.opencds.cqf.cql.evaluator.engine.execution.TranslatingLibraryLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Created by Christopher on 1/11/2017.
*/
public class LibraryHelper {
private static final Logger ourLog = LoggerFactory.getLogger(LibraryHelper.class);
private final Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache;
private final Map<VersionedIdentifier, Library> libraryCache;
private final CqlTranslatorOptions translatorOptions;
public LibraryHelper(Map<org.hl7.elm.r1.VersionedIdentifier, Model> modelCache,
Map<VersionedIdentifier, Library> libraryCache, CqlTranslatorOptions translatorOptions) {
this.modelCache = modelCache;
this.libraryCache = libraryCache;
this.translatorOptions = translatorOptions;
}
public org.opencds.cqf.cql.engine.execution.LibraryLoader createLibraryLoader(
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> provider) {
ModelManager modelManager = new CacheAwareModelManager(this.modelCache);
LibraryManager libraryManager = new LibraryManager(modelManager);
libraryManager.getLibrarySourceLoader().clearProviders();
List<org.opencds.cqf.cql.evaluator.cql2elm.content.LibraryContentProvider> contentProviders = Collections
.singletonList(new LibraryContentProvider<org.hl7.fhir.r4.model.Library, Attachment>(provider,
x -> x.getContent(), x -> x.getContentType(), x -> x.getData()));
return new CacheAwareLibraryLoaderDecorator(
new TranslatingLibraryLoader(modelManager, contentProviders, translatorOptions), libraryCache);
}
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 CacheAwareLibraryLoaderDecorator(new TranslatingLibraryLoader(modelManager, null, translatorOptions),
libraryCache);
}
public org.hl7.fhir.r4.model.Library resolveLibraryReference(
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider, String reference, RequestDetails theRequestDetails) {
// Raw references to Library/libraryId or libraryId
if (reference.startsWith("Library/") || !reference.contains("/")) {
return libraryResourceProvider.resolveLibraryById(reference.replace("Library/", ""), theRequestDetails);
}
// Full url (e.g. http://hl7.org/fhir/us/Library/FHIRHelpers)
else if (reference.contains(("/Library/"))) {
return libraryResourceProvider.resolveLibraryByCanonicalUrl(reference, theRequestDetails);
}
return null;
}
public List<org.cqframework.cql.elm.execution.Library> loadLibraries(Measure measure,
LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider, RequestDetails theRequestDetails) {
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, theRequestDetails);
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(Msg.code(1677) + String.format("Could not load library source for libraries referenced in %s.", measure.getId()));
}
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(), theRequestDetails);
if (library != null) {
if (isLogicLibrary(library)) {
libraries.add(libraryLoader
.load(new VersionedIdentifier().withId(library.getName()).withVersion(library.getVersion())));
} else {
ourLog.warn("Library {} not included as part of evaluation context. Only Libraries with the 'logic-library' type are included.", library.getId());
}
}
}
}
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, LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider, RequestDetails theRequestDetails) {
org.hl7.fhir.r4.model.Library fhirLibrary = libraryResourceProvider.resolveLibraryById(libraryId, theRequestDetails);
return libraryLoader
.load(new VersionedIdentifier().withId(fhirLibrary.getName()).withVersion(fhirLibrary.getVersion()));
}
public Library resolvePrimaryLibrary(Measure measure,
LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider, RequestDetails theRequestDetails) {
// default is the first library reference
String id = CanonicalHelper.getId(measure.getLibrary().get(0));
Library library = resolveLibraryById(id, libraryLoader, libraryResourceProvider, theRequestDetails);
if (library == null) {
throw new IllegalArgumentException(Msg.code(1678) + String.format("Could not resolve primary library for Measure/%s.", measure.getIdElement().getIdPart()));
}
return library;
}
public Library resolvePrimaryLibrary(PlanDefinition planDefinition,
LibraryLoader libraryLoader,
LibraryResolutionProvider<org.hl7.fhir.r4.model.Library> libraryResourceProvider, RequestDetails theRequestDetails) {
String id = CanonicalHelper.getId(planDefinition.getLibrary().get(0));
Library library = resolveLibraryById(id, libraryLoader, libraryResourceProvider, theRequestDetails);
if (library == null) {
throw new IllegalArgumentException(Msg.code(1679) + String.format("Could not resolve primary library for PlanDefinition/%s",
planDefinition.getIdElement().getIdPart()));
}
return library;
}
}

View File

@ -1,153 +0,0 @@
package ca.uhn.fhir.cql.r4.provider;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.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.i18n.Msg;
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.jpa.term.api.ITermReadSvc;
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 javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
@Component
public class JpaTerminologyProvider implements TerminologyProvider {
private final ITermReadSvc myTerminologySvc;
private final DaoRegistry myDaoRegistry;
private final IValidationSupport myValidationSupport;
private IFhirResourceDao<ValueSet> myValueSetDao;
@Autowired
public JpaTerminologyProvider(ITermReadSvc theTerminologySvc, DaoRegistry theDaoRegistry, IValidationSupport theValidationSupport) {
myTerminologySvc = theTerminologySvc;
myDaoRegistry = theDaoRegistry;
myValidationSupport = theValidationSupport;
}
@PostConstruct
public void init() {
myValueSetDao = myDaoRegistry.getResourceDao(ValueSet.class);
}
@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 {
// This could possibly be refactored into a single call to the underlying HAPI Terminology service. Need to think through that..,
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(Msg.code(1667) + String.format(
"Could not expand value set %s; version and code system bindings are not supported at this time.",
valueSet.getId()));
}
}
IBundleProvider bundleProvider = myValueSetDao
.search(SearchParameterMap.newSynchronous().add(ValueSet.SP_URL, new UriParam(valueSet.getId())));
List<IBaseResource> valueSets = bundleProvider.getAllResources();
if (valueSets.isEmpty()) {
throw new IllegalArgumentException(Msg.code(1668) + String.format("Could not resolve value set %s.", valueSet.getId()));
} else if (valueSets.size() == 1) {
vs = (ValueSet) valueSets.get(0);
} else {
throw new IllegalArgumentException(Msg.code(1669) + "Found more than 1 ValueSet with url: " + valueSet.getId());
}
} else {
vs = myValueSetDao.read(new IdType(valueSet.getId()));
if (vs == null) {
throw new IllegalArgumentException(Msg.code(1670) + String.format("Could not resolve value set %s.", valueSet.getId()));
}
}
// Attempt to expand the ValueSet if it's not already expanded.
if (!(vs.hasExpansion() && vs.getExpansion().hasContains())) {
vs = myTerminologySvc.expandValueSet(
new ValueSetExpansionOptions().setCount(Integer.MAX_VALUE).setFailOnMissingCodeSystem(false), vs);
}
List<Code> codes = new ArrayList<>();
// If expansion was successful, use the codes.
if (vs.hasExpansion() && vs.getExpansion().hasContains()) {
for (ValueSetExpansionContainsComponent vsecc : vs.getExpansion().getContains()) {
codes.add(new Code().withCode(vsecc.getCode()).withSystem(vsecc.getSystem()));
}
}
// If not, best-effort based on codes. Should probably make this configurable to match the behavior of the
// underlying terminology service implementation
else if (vs.hasCompose() && vs.getCompose().hasInclude()) {
for (ValueSet.ConceptSetComponent include : vs.getCompose().getInclude()) {
for (ValueSet.ConceptReferenceComponent concept : include.getConcept()) {
if (concept.hasCode()) {
codes.add(new Code().withCode(concept.getCode()).withSystem(include.getSystem()));
}
}
}
}
return codes;
}
@Override
public Code lookup(Code code, CodeSystemInfo codeSystem) throws ResourceNotFoundException {
LookupCodeResult cs = myTerminologySvc.lookupCode(new ValidationSupportContext(myValidationSupport),
codeSystem.getId(), code.getCode());
code.setDisplay(cs.getCodeDisplay());
code.setSystem(codeSystem.getId());
return code;
}
}

View File

@ -1,128 +0,0 @@
package ca.uhn.fhir.cql.r4.provider;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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.rest.api.server.SystemRequestDetails;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.RequestDetails;
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, RequestDetails theRequestDetails) {
try {
return myLibraryDao.read(new IdType(libraryId), theRequestDetails);
} catch (Exception e) {
throw new IllegalArgumentException(Msg.code(1665) + 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, new SystemRequestDetails());
org.hl7.fhir.r4.model.Library library = LibraryResolutionProvider.selectFromList(libraries, libraryVersion,
x -> x.getVersion());
if (library == null) {
throw new IllegalArgumentException(Msg.code(1666) + String.format("Could not resolve library name %s", libraryName));
}
return library;
}
@Override
public Library resolveLibraryByCanonicalUrl(String url, RequestDetails theRequestDetails) {
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 = SearchParameterMap.newSynchronous();
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, theRequestDetails);
if (bundleProvider.size() == null || bundleProvider.size() == 0) {
return null;
}
List<IBaseResource> resourceList = bundleProvider.getAllResources();
return LibraryResolutionProvider.selectFromList(resolveLibraries(resourceList), version, x -> x.getVersion());
}
private Iterable<org.hl7.fhir.r4.model.Library> getLibrariesByName(String name, RequestDetails theRequestDetails) {
// Search for libraries by name
SearchParameterMap map = SearchParameterMap.newSynchronous();
map.add("name", new StringParam(name, true));
ca.uhn.fhir.rest.api.server.IBundleProvider bundleProvider = myLibraryDao.search(map, theRequestDetails);
if (bundleProvider.size() == null || bundleProvider.size() == 0) {
return new ArrayList<>();
}
List<IBaseResource> resourceList = bundleProvider.getAllResources();
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

@ -1,129 +0,0 @@
package ca.uhn.fhir.cql.r4.provider;
/*-
* #%L
* HAPI FHIR JPA Server - Clinical Quality Language
* %%
* Copyright (C) 2014 - 2023 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.i18n.Msg;
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.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.provider.ProviderConstants;
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 = ProviderConstants.CQL_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 = "subject") String subjectRef,
@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,
RequestDetails theRequestDetails) 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, theRequestDetails);
if (measure == null) {
throw new RuntimeException(Msg.code(1663) + "Could not find Measure/" + theId.getIdPart());
}
seed.setup(measure, periodStart, periodEnd, productLine, source, user, pass, theRequestDetails);
// resolve report type
MeasureEvaluation evaluator = new MeasureEvaluation(seed.getDataProvider(), this.registry,
seed.getMeasurementPeriod());
if (reportType != null) {
switch (reportType) {
case "subject":
return evaluator.evaluatePatientMeasure(seed.getMeasure(), seed.getContext(), subjectRef, practitionerRef, theRequestDetails);
case "subject-list":
return evaluator.evaluateSubjectListMeasure(seed.getMeasure(), seed.getContext(), practitionerRef, theRequestDetails);
case "population":
return evaluator.evaluatePopulationMeasure(seed.getMeasure(), seed.getContext(), practitionerRef, theRequestDetails);
default:
throw new IllegalArgumentException(Msg.code(1664) + "Invalid report type: " + reportType);
}
}
// default report type is subject
MeasureReport report = evaluator.evaluatePatientMeasure(seed.getMeasure(), seed.getContext(), subjectRef, practitionerRef, theRequestDetails);
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

@ -1,104 +0,0 @@
package ca.uhn.fhir.cql;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.cql.common.helper.PartitionHelper;
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 ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.test.utilities.RequestDetailsHelper;
import org.apache.commons.io.FileUtils;
import org.hl7.fhir.dstu3.model.Bundle;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
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.Configuration;
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, BaseCqlDstu3Test.Config.class})
public class BaseCqlDstu3Test extends BaseJpaDstu3Test implements CqlProviderTestBase {
public static Logger ourLog = LoggerFactory.getLogger(BaseCqlDstu3Test.class);
protected final RequestDetails myRequestDetails = RequestDetailsHelper.newServletRequestDetails();
@Autowired
protected
DaoRegistry myDaoRegistry;
@Autowired
@RegisterExtension
protected PartitionHelper myPartitionHelper;
@Autowired
IFhirSystemDao mySystemDao;
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, myRequestDetails);
}
count++;
} else {
ourLog.info("Ignoring file: '{}'", filename);
}
}
return count;
}
@Override
public DaoRegistry getDaoRegistry() {
return myDaoRegistry;
}
protected Bundle loadBundle(Bundle bundle) {
return (Bundle) mySystemDao.transaction(myRequestDetails, bundle);
}
protected Bundle parseBundle(String theLocation) throws IOException {
String json = stringFromResource(theLocation);
Bundle bundle = (Bundle) myFhirContext.newJsonParser().parseResource(json);
return bundle;
}
protected Bundle loadBundle(String theLocation) throws IOException {
Bundle bundle = parseBundle(theLocation);
return loadBundle(bundle);
}
@Override
public FhirContext getTestFhirContext() {
return myFhirContext;
}
@Configuration
static class Config {
@Bean
public PartitionHelper myPartitionHelper() {
return new PartitionHelper();
}
}
}

View File

@ -1,129 +0,0 @@
package ca.uhn.fhir.cql;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.cql.common.helper.PartitionHelper;
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.config.DaoConfig;
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 ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.parser.LenientErrorHandler;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.test.utilities.RequestDetailsHelper;
import org.apache.commons.io.FileUtils;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Meta;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
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.Configuration;
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, BaseCqlR4Test.Config.class})
public class BaseCqlR4Test extends BaseJpaR4Test implements CqlProviderTestBase {
private static final Logger ourLog = LoggerFactory.getLogger(BaseCqlR4Test.class);
protected final RequestDetails myRequestDetails = RequestDetailsHelper.newServletRequestDetails();
@Autowired
@RegisterExtension
protected PartitionHelper myPartitionHelper;
@Autowired
protected
DaoRegistry myDaoRegistry;
@Autowired
IFhirSystemDao<Bundle, Meta> mySystemDao;
@Autowired
DaoConfig myDaoConfig;
@BeforeEach
public void beforeEach() {
myDaoConfig.setMaximumExpansionSize(5000);
// We load some dstu3 resources using a R4 FhirContext. Disable strict handling so this doesn't throw errors.
myFhirContext.setParserErrorHandler(new LenientErrorHandler());
}
@AfterEach
public void afterEach() {
myDaoConfig.setMaximumExpansionSize(new DaoConfig().getMaximumExpansionSize());
}
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, myRequestDetails);
}
count++;
} else {
ourLog.info("Ignoring file: '{}'", filename);
}
}
return count;
}
@Override
public DaoRegistry getDaoRegistry() {
return myDaoRegistry;
}
protected Bundle loadBundle(String theLocation) throws IOException {
Bundle bundle = parseBundle(theLocation);
return loadBundle(bundle, myRequestDetails);
}
protected Bundle parseBundle(String theLocation) throws IOException {
String json = stringFromResource(theLocation);
IParser jsonParser = myFhirContext.newJsonParser();
LenientErrorHandler lenientErrorHandler = new LenientErrorHandler();
lenientErrorHandler.setErrorOnInvalidValue(false);
jsonParser.setParserErrorHandler(lenientErrorHandler);
Bundle bundle = (Bundle) jsonParser.parseResource(json);
return bundle;
}
protected Bundle loadBundle(Bundle bundle, RequestDetails theRequestDetails) {
return (Bundle) mySystemDao.transaction(theRequestDetails, bundle);
}
@Override
public FhirContext getTestFhirContext() {
return myFhirContext;
}
@Configuration
static class Config {
@Bean
public PartitionHelper myPartitionHelper() {
return new PartitionHelper();
}
}
}

View File

@ -1,44 +0,0 @@
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

@ -1,58 +0,0 @@
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("param", "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("param", "2001");
assertNotNull("result should not be NULL!", result);
}
@Test
public void testDateHelperProperlyDetectsBlankDate() {
DateHelper dateHelper = new DateHelper();
Date result = null;
try {
result = DateHelper.resolveRequestDate("param", 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("param", "aaa-bbb-ccc");
fail();
} catch (DataFormatException e) {
assertNull("result should be NULL!", result);
}
}
}

View File

@ -1,214 +0,0 @@
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.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.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.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.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
@BeforeEach
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, "translator should not be NULL!");
}
//@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, "translator should not be NULL!");
}
//@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, "translator should be NULL!");
}
}
//@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), any())).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, "translator should be NULL!");
}
}
@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, "library should not be NULL!");
}
@Test
public void testGetIllegalArgumentExceptionFromReadLibraryWhenCqlIsBlankString() {
Library library = null;
try {
library = TranslatorHelper.readLibrary(new ByteArrayInputStream("INVALID-XML-DOCUMENT".getBytes()));
fail();
} catch (IllegalArgumentException e) {
assertNull(library, "library should be NULL!");
}
}
@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, "library should not be NULL!");
}
@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 getTestFhirContext() {
return null;
}
@Override
public DaoRegistry getDaoRegistry() {
return null;
}
}

View File

@ -1,47 +0,0 @@
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 ca.uhn.fhir.rest.api.server.RequestDetails;
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 getTestFhirContext();
DaoRegistry getDaoRegistry();
default IBaseResource loadResource(String theLocation, RequestDetails theRequestDetails) throws IOException {
String json = stringFromResource(theLocation);
IBaseResource resource = getTestFhirContext().newJsonParser().parseResource(json);
IFhirResourceDao<IBaseResource> dao = getDaoRegistry().getResourceDao(resource.getIdElement().getResourceType());
if (dao == null) {
return null;
} else {
dao.update(resource, theRequestDetails);
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

@ -1,50 +0,0 @@
package ca.uhn.fhir.cql.dstu3.provider;
import ca.uhn.fhir.cql.BaseCqlDstu3Test;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.opencds.cqf.cql.engine.runtime.Code;
import org.opencds.cqf.cql.engine.terminology.TerminologyProvider;
import org.opencds.cqf.cql.engine.terminology.ValueSetInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class JpaTerminologyProviderTest extends BaseCqlDstu3Test {
private static final Logger ourLog = LoggerFactory.getLogger(JpaTerminologyProviderTest.class);
@Autowired
protected TerminologyProvider jpaTerminologyProvider;
@Autowired
protected DaoConfig daoConfig;
@BeforeEach
public void before() throws IOException {
// Load executable (i.e. "pre-expanded") value set
loadResource("dstu3/provider/test-executable-value-set.json", myRequestDetails);
}
@Test
public void testTerminologyProviderExpand() {
ValueSetInfo valueSetInfo = new ValueSetInfo().withId("http://test.com/fhir/ValueSet/test-executable-value-set");
Iterable<Code> codeIterable = this.jpaTerminologyProvider.expand(valueSetInfo);
assertNotNull(codeIterable);
List<Code> codes = new ArrayList<>();
codeIterable.forEach(codes::add);
assertThat(codes, hasSize(2));
}
}

View File

@ -1,207 +0,0 @@
package ca.uhn.fhir.cql.r4;
import ca.uhn.fhir.cql.BaseCqlR4Test;
import ca.uhn.fhir.cql.r4.provider.MeasureOperationsProvider;
import ca.uhn.fhir.util.BundleUtil;
import org.hamcrest.Matchers;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.MeasureReport;
import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupComponent;
import org.hl7.fhir.r4.model.Quantity;
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.Date;
import java.util.List;
import java.util.Optional;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
public class CqlMeasureEvaluationR4Test extends BaseCqlR4Test {
Logger ourLog = LoggerFactory.getLogger(CqlMeasureEvaluationR4Test.class);
@Autowired
MeasureOperationsProvider myMeasureOperationsProvider;
protected void testMeasureBundle(String theLocation) throws IOException {
Bundle bundle = parseBundle(theLocation);
loadBundle(bundle, myRequestDetails);
List<Measure> measures = BundleUtil.toListOfResourcesOfType(myFhirContext, bundle, Measure.class);
if (measures == null || measures.isEmpty()) {
throw new IllegalArgumentException(String.format("No measures found for Bundle %s", theLocation));
}
List<MeasureReport> reports = BundleUtil.toListOfResourcesOfType(myFhirContext, bundle, MeasureReport.class);
if (reports == null || reports.isEmpty()) {
throw new IllegalArgumentException(String.format("No measure reports found for Bundle %s", theLocation));
}
for (MeasureReport report : reports) {
testMeasureReport(report);
}
}
protected void testMeasureReport(MeasureReport expected) {
String measureId = this.getMeasureId(expected);
String patientId = this.getPatientId(expected);
String periodStart = this.getPeriodStart(expected);
String periodEnd = this.getPeriodEnd(expected);
this.ourLog.info("Measure: %s, Patient: %s, Start: %s, End: %s", measureId, patientId, periodStart, periodEnd);
MeasureReport actual = this.myMeasureOperationsProvider.evaluateMeasure(new IdType("Measure", measureId),
periodStart, periodEnd, null,
// TODO: These are all individual reports
"subject", patientId,
// TODO: Generalize these parameters into a Parameters resource
null, null, null, null, null, null, myRequestDetails);
compareMeasureReport(expected, actual);
}
protected void compareMeasureReport(MeasureReport expected, MeasureReport actual) {
assertNotNull("expected MeasureReport can not be null", expected);
assertNotNull("actual MeasureReport can not be null", actual);
String errorLocator = String.format("Measure: %s, Subject: %s", expected.getMeasure(),
expected.getSubject().getReference());
assertEquals(expected.hasGroup(), actual.hasGroup(), errorLocator);
assertEquals(expected.getGroup().size(), actual.getGroup().size(), errorLocator);
for (MeasureReportGroupComponent mrgcExpected : expected.getGroup()) {
Optional<MeasureReportGroupComponent> mrgcActualOptional = actual.getGroup().stream()
.filter(x -> x.getId() != null && x.getId().equals(mrgcExpected.getId())).findFirst();
errorLocator = String.format("Measure: %s, Subject: %s, Group: %s", expected.getMeasure(),
expected.getSubject().getReference(), mrgcExpected.getId());
assertTrue(errorLocator, mrgcActualOptional.isPresent());
MeasureReportGroupComponent mrgcActual = mrgcActualOptional.get();
if (mrgcExpected.getMeasureScore() == null) {
assertNull(mrgcActual.getMeasureScore(), errorLocator);
} else {
assertNotNull(mrgcActual.getMeasureScore());
Quantity quantityExpected = mrgcExpected.getMeasureScore();
Quantity quantityActual = mrgcActual.getMeasureScore();
assertThat(errorLocator, quantityActual.getValue(), Matchers.comparesEqualTo(quantityExpected.getValue()));
}
}
}
// TODO: In R4 the Subject will not necessarily be a Patient.
public String getPatientId(MeasureReport measureReport) {
String[] subjectRefParts = measureReport.getSubject().getReference().split("/");
String patientId = subjectRefParts[subjectRefParts.length - 1];
return patientId;
}
public String getMeasureId(MeasureReport measureReport) {
String[] measureRefParts = measureReport.getMeasure().split("/");
String measureId = measureRefParts[measureRefParts.length - 1];
return measureId;
}
public String getPeriodStart(MeasureReport measureReport) {
Date periodStart = measureReport.getPeriod().getStart();
if (periodStart != null) {
return toDateString(periodStart);
}
return null;
}
public String getPeriodEnd(MeasureReport measureReport) {
Date periodEnd = measureReport.getPeriod().getEnd();
if (periodEnd != null) {
return toDateString(periodEnd);
}
return null;
}
public String toDateString(Date date) {
return new DateTimeType(date).getValueAsString();
}
// @Test - No test results in this bundle yet
// public void test_EXM74_102000() throws IOException {
// this.testMeasureBundle("r4/connectathon/EXM74-10.2.000-bundle.json");
// }
@Test
public void test_EXM105_82000() throws IOException {
this.testMeasureBundle("r4/connectathon/EXM105-8.2.000-bundle.json");
}
@Test
public void test_EXM108_83000() throws IOException {
this.testMeasureBundle("r4/connectathon/EXM108-8.3.000-bundle.json");
}
// @Test - No test results in this bundle yet
// public void test_EXM111_91000() throws IOException {
// this.testMeasureBundle("r4/connectathon/EXM111-9.1.000-bundle.json");
// }
// @Test - The test data for the denominator exclusion appears to be invalid
// public void test_EXM124_82000() throws IOException {
// this.testMeasureBundle("r4/connectathon/EXM124-8.2.000-bundle.json");
// }
@Test
public void test_EXM124_90000() throws IOException {
this.testMeasureBundle("r4/connectathon/EXM124-9.0.000-bundle.json");
}
@Test
public void test_EXM125_73000() throws IOException {
this.testMeasureBundle("r4/connectathon/EXM125-7.3.000-bundle.json");
}
@Test
public void test_EXM130_73000() throws IOException {
this.testMeasureBundle("r4/connectathon/EXM130-7.3.000-bundle.json");
}
// @Test - No test results in this bundle yet
// public void test_EXM149_92000() throws IOException {
// this.testMeasureBundle("r4/connectathon/EXM149-9.2.000-bundle.json");
// }
// @Test - Missing Adult outpatient encounters Library
// public void test_EXM153_92000() throws IOException {
// this.testMeasureBundle("r4/connectathon/EXM153-9.2.000-bundle.json");
// }
// @Test - Missing Encounter data for Numerator test
// public void test_EXM347_43000() throws IOException {
// this.testMeasureBundle("r4/connectathon/EXM347-4.3.000-bundle.json");
// }
// @Test - No test results in this bundle yet
// public void test_EXM349_210000() throws IOException {
// this.testMeasureBundle("r4/connectathon/EXM349-2.10.000-bundle.json");
// }
// @Test - No test results in this bundle yet
// public void test_EXM506_22000() throws IOException {
// this.testMeasureBundle("r4/connectathon/EXM506-2.2.000-bundle.json");
// }
// @Test - No test results in this bundle yet
// public void test_EXM529_10000() throws IOException {
// this.testMeasureBundle("r4/connectathon/EXM529-1.0.000-bundle.json");
// }
}

View File

@ -1,79 +0,0 @@
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;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class CqlProviderR4Test extends BaseCqlR4Test implements CqlProviderTestBase {
private static final Logger ourLog = LoggerFactory.getLogger(CqlProviderR4Test.class);
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 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", myRequestDetails);
loadResource("r4/hedis-ig/measure-asf.json", myRequestDetails);
myPartitionHelper.clear();
MeasureReport report = myMeasureOperationsProvider.evaluateMeasure(measureId, periodStart, periodEnd, measure, "subject",
patient, null, null, null, null, null, null, myRequestDetails);
// Assert it worked
assertTrue(myPartitionHelper.wasCalled());
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", myRequestDetails);
loadResource("r4/hedis-ig/measure-asf.json", myRequestDetails);
myPartitionHelper.clear();
MeasureReport report = myMeasureOperationsProvider.evaluateMeasure(measureId, null, null, measure, "subject",
patient, null, null, null, null, null, null, myRequestDetails);
// Assert it worked
assertTrue(myPartitionHelper.wasCalled());
assertThat(report.getGroup(), hasSize(1));
assertThat(report.getGroup().get(0).getPopulation(), hasSize(3));
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(report));
}
}

View File

@ -1,50 +0,0 @@
package ca.uhn.fhir.cql.r4.provider;
import ca.uhn.fhir.cql.BaseCqlR4Test;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.opencds.cqf.cql.engine.runtime.Code;
import org.opencds.cqf.cql.engine.terminology.TerminologyProvider;
import org.opencds.cqf.cql.engine.terminology.ValueSetInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class JpaTerminologyProviderTest extends BaseCqlR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(JpaTerminologyProviderTest.class);
@Autowired
protected TerminologyProvider jpaTerminologyProvider;
@Autowired
protected DaoConfig daoConfig;
@BeforeEach
public void before() throws IOException {
// Load executable (i.e. "pre-expanded") value set
loadResource("r4/provider/test-executable-value-set.json", myRequestDetails);
}
@Test
public void testTerminologyProviderExpand() {
ValueSetInfo valueSetInfo = new ValueSetInfo().withId("http://test.com/fhir/ValueSet/test-executable-value-set");
Iterable<Code> codeIterable = this.jpaTerminologyProvider.expand(valueSetInfo);
assertNotNull(codeIterable);
List<Code> codes = new ArrayList<>();
codeIterable.forEach(codes::add);
assertThat(codes, hasSize(2));
}
}

View File

@ -1,118 +0,0 @@
{
"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

@ -1,39 +0,0 @@
{
"resourceType": "ValueSet",
"id": "test-executable-value-set",
"meta" : {
"profile" : [
"http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-executablevalueset"
]
},
"url": "http://test.com/fhir/ValueSet/test-executable-value-set",
"extension" : [
{
"url" : "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeCapability",
"valueCode" : "executable"
},
{
"url" : "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeRepresentationLevel",
"valueCode" : "executable"
},
{
"url" : "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-usageWarning",
"valueString" : "This value set contains a point-in-time expansion enumerating the codes that meet the value set intent. As new versions of the code systems used by the value set are released, the contents of this expansion will need to be updated to incorporate newly defined codes that meet the value set intent. Before, and periodically during production use, the value set expansion contents SHOULD be updated. The value set expansion specifies the timestamp when the expansion was produced, SHOULD contain the parameters used for the expansion, and SHALL contain the codes that are obtained by evaluating the value set definition. If this is ONLY an executable value set, a distributable definition of the value set must be obtained to compute the updated expansion."
}
],
"expansion": {
"timestamp" : "2020-03-26T17:39:09-06:00",
"contains" : [
{
"system" : "http://test.com/codesystem/test",
"code" : "1234",
"display" : "1234"
},
{
"system" : "http://test.com/codesystem/test",
"code" : "ABCD",
"display" : "ABCD"
}
]
}
}

View File

@ -1,76 +0,0 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>TRACE</level>
</filter>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] %msg%n</pattern>
</encoder>
</appender>
<logger name="org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator" additivity="false"
level="info">
<appender-ref ref="STDOUT"/>
</logger>
<logger name="ca.uhn.fhir.jpa.dao.FhirResourceDaoSubscriptionDstu2" additivity="false" level="info">
<appender-ref ref="STDOUT"/>
</logger>
<logger name="org.eclipse.jetty.websocket" additivity="false" level="info">
<appender-ref ref="STDOUT"/>
</logger>
<!-- TODO GGG HS can probably eventually disable this. -->
<logger name="org.hibernate.search.backend.lucene.infostream" additivity="true" level="trace">
<appender-ref ref="STDOUT"/>
</logger>
<logger name="org.hibernate.search.query" additivity="true" level="trace">
<appender-ref ref="STDOUT"/>
</logger>
j
<logger name="org.hibernate.event.internal.DefaultPersistEventListener" additivity="true" level="info">
<appender-ref ref="STDOUT"/>
</logger>
<logger name="org.elasticsearch.client" additivity="true" level="trace">
<appender-ref ref="STDOUT"/>
</logger>
<logger name="org.eclipse" additivity="false" level="error">
</logger>
<logger name="ca.uhn.fhir.rest.client" additivity="false" level="info">
<appender-ref ref="STDOUT"/>
</logger>
<logger name="ca.uhn.fhir.jpa.dao" additivity="false" level="info">
<appender-ref ref="STDOUT"/>
</logger>
<!-- set to debug to enable term expansion logs -->
<logger name="ca.uhn.fhir.jpa.term" additivity="false" level="debug">
<appender-ref ref="STDOUT"/>
</logger>
<!-- Set to 'trace' to enable SQL logging -->
<logger name="org.hibernate.SQL" additivity="false" level="info">
<appender-ref ref="STDOUT"/>
</logger>
<!-- Set to 'trace' to enable SQL Value logging -->
<logger name="org.hibernate.type" additivity="false" level="info">
<appender-ref ref="STDOUT"/>
</logger>
<logger name="org.hibernate.search.elasticsearch.request" additivity="false" level="trace">
<appender-ref ref="STDOUT"/>
</logger>
<logger name="org.springframework.test.context.cache" additivity="false" level="debug">
<appender-ref ref="STDOUT"/>
</logger>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

View File

@ -1,235 +0,0 @@
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

View File

@ -1,116 +0,0 @@
{
"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

@ -1,39 +0,0 @@
{
"resourceType": "ValueSet",
"id": "test-executable-value-set",
"meta" : {
"profile" : [
"http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-executablevalueset"
]
},
"url": "http://test.com/fhir/ValueSet/test-executable-value-set",
"extension" : [
{
"url" : "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeCapability",
"valueCode" : "executable"
},
{
"url" : "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-knowledgeRepresentationLevel",
"valueCode" : "executable"
},
{
"url" : "http://hl7.org/fhir/uv/cpg/StructureDefinition/cpg-usageWarning",
"valueString" : "This value set contains a point-in-time expansion enumerating the codes that meet the value set intent. As new versions of the code systems used by the value set are released, the contents of this expansion will need to be updated to incorporate newly defined codes that meet the value set intent. Before, and periodically during production use, the value set expansion contents SHOULD be updated. The value set expansion specifies the timestamp when the expansion was produced, SHOULD contain the parameters used for the expansion, and SHALL contain the codes that are obtained by evaluating the value set definition. If this is ONLY an executable value set, a distributable definition of the value set must be obtained to compute the updated expansion."
}
],
"expansion": {
"timestamp" : "2020-03-26T17:39:09-06:00",
"contains" : [
{
"system" : "http://test.com/codesystem/test",
"code" : "1234",
"display" : "1234"
},
{
"system" : "http://test.com/codesystem/test",
"code" : "ABCD",
"display" : "ABCD"
}
]
}
}

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -20,6 +20,25 @@
<dependency>
<groupId>org.fhir</groupId>
<artifactId>ucum</artifactId>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
<!-- remove xpp3 fat jar and replace it with non-bundled versions -->
<exclusion>
<groupId>xpp3</groupId>
<artifactId>xpp3</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- This dependency replaces the version of xpp3 that's bundled with Ucum with one that's Java 9+ compatible -->
<dependency>
<groupId>org.codelibs</groupId>
<artifactId>xpp3</artifactId>
<version>1.1.4c.0</version>
</dependency>
<dependency>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -59,9 +59,21 @@
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
<!-- remove xpp3 fat jar and replace it with non-bundled versions -->
<exclusion>
<groupId>xpp3</groupId>
<artifactId>xpp3</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- These two dependencies replaces the version of xpp3 that's bundled with Ucum with one that's Java 9+ compatible -->
<dependency>
<groupId>org.codelibs</groupId>
<artifactId>xpp3</artifactId>
<version>1.1.4c.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -20,7 +20,7 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-caching-api</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>hapi-fhir</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-okhttp</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.3.6-SNAPSHOT</version>
<version>6.3.7-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-server-jersey</artifactId>

Some files were not shown because too many files have changed in this diff Show More