From a9af2595cab105458ec687b75cdadba858415111 Mon Sep 17 00:00:00 2001
From: chubert-sb <85900348+chubert-sb@users.noreply.github.com>
Date: Thu, 6 Jun 2024 14:27:52 -0400
Subject: [PATCH] MAT-7030: update cqmMapper for missing properties (#70)
* MAT-7030: update cqm mapper to include missing properties
* MAT-7030: change ElmDependencyUtil to return for all libraries
* MAT-7030: update tests
* MAT-7030: overwrite population sets for qrda generation
* MAT-7030: remove unused mapper
---
pom.xml | 2 +-
.../cms/madie/services/CqmMeasureMapper.java | 273 +++++++++++++++---
.../gov/cms/madie/services/QrdaService.java | 7 +-
.../gov/cms/madie/util/ElmDependencyUtil.java | 27 +-
.../cms/madie/util/ElmDependencyUtilTest.java | 17 +-
5 files changed, 273 insertions(+), 53 deletions(-)
diff --git a/pom.xml b/pom.xml
index e4f311a..9687c74 100644
--- a/pom.xml
+++ b/pom.xml
@@ -67,7 +67,7 @@
gov.cms.madie
madie-java-models
- 0.6.34-SNAPSHOT
+ 0.6.37-SNAPSHOT
gov.cms.madie.packaging
diff --git a/src/main/java/gov/cms/madie/services/CqmMeasureMapper.java b/src/main/java/gov/cms/madie/services/CqmMeasureMapper.java
index e129269..3156f19 100644
--- a/src/main/java/gov/cms/madie/services/CqmMeasureMapper.java
+++ b/src/main/java/gov/cms/madie/services/CqmMeasureMapper.java
@@ -1,12 +1,16 @@
package gov.cms.madie.services;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
import gov.cms.madie.Exceptions.CqmConversionException;
import gov.cms.madie.dto.SourceDataCriteria;
import gov.cms.madie.models.cqm.*;
import gov.cms.madie.models.cqm.datacriteria.basetypes.DataElement;
import gov.cms.madie.models.cqm.datacriteria.*;
-import gov.cms.madie.models.measure.QdmMeasure;
+import gov.cms.madie.models.measure.*;
import gov.cms.madie.util.ElmDependencyUtil;
+import org.apache.commons.lang3.StringUtils;
import org.json.JSONObject;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@@ -20,6 +24,7 @@
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface CqmMeasureMapper {
+ ObjectMapper mapper = new ObjectMapper();
@Mapping(target = "hqmf_set_id", source = "measure.measureSetId")
@Mapping(target = "hqmf_version_number", source = "measure.versionId")
@@ -35,6 +40,7 @@ public interface CqmMeasureMapper {
@Mapping(target = "calculation_method", expression = "java(getCalculationMethod(measure))")
@Mapping(target = "measure_period", expression = "java(getMeasurePeriod(measure))")
@Mapping(target = "cql_libraries", expression = "java(getCqlLibraries(measure, elms))")
+ @Mapping(target = "population_sets", expression = "java(getPopulationSets(measure))")
@Mapping(
target = "source_data_criteria",
expression = "java(getSourceDataCriteria(dataCriteria))")
@@ -46,42 +52,46 @@ default String getCalculationMethod(QdmMeasure measure) {
}
default MeasurePeriod getMeasurePeriod(QdmMeasure measure) {
- JSONObject low = new JSONObject();
- low.put(
- "value",
- measure
- .getMeasurementPeriodStart()
- .toInstant()
- .atZone(ZoneId.of("UTC"))
- .toLocalDateTime()
- .format(DateTimeFormatter.ofPattern("yyyyMMddHH")));
-
- JSONObject high = new JSONObject();
- high.put(
- "value",
- measure
- .getMeasurementPeriodEnd()
- .toInstant()
- .atZone(ZoneId.of("UTC"))
- .toLocalDateTime()
- .format(DateTimeFormatter.ofPattern("yyyyMMddHH")));
+ PeriodPoint low =
+ PeriodPoint.builder()
+ .value(
+ measure
+ .getMeasurementPeriodStart()
+ .toInstant()
+ .atZone(ZoneId.of("UTC"))
+ .toLocalDateTime()
+ .format(DateTimeFormatter.ofPattern("yyyyMMddHH"))
+ .toString())
+ .build();
+ PeriodPoint high =
+ PeriodPoint.builder()
+ .value(
+ measure
+ .getMeasurementPeriodEnd()
+ .toInstant()
+ .atZone(ZoneId.of("UTC"))
+ .toLocalDateTime()
+ .format(DateTimeFormatter.ofPattern("yyyyMMddHH"))
+ .toString())
+ .build();
- return MeasurePeriod.builder().low(low.toString()).high(high.toString()).build();
+ return MeasurePeriod.builder().low(low).high(high).build();
}
default List getCqlLibraries(QdmMeasure measure, List elms) {
- List statements =
+ Map> statements =
ElmDependencyUtil.findDependencies(elms, measure.getCqlLibraryName());
- List finalStatements = statements;
return elms.stream()
- .map(elm -> buildCQLLibrary(elm, measure.getCqlLibraryName(), finalStatements))
+ .map(elm -> buildCQLLibrary(elm, measure.getCqlLibraryName(), statements))
.collect(Collectors.toList());
}
default CQLLibrary buildCQLLibrary(
- String elm, String measureLibraryName, List statementDependencies) {
+ String elm,
+ String measureLibraryName,
+ Map> statementDependencies) {
JSONObject elmJson = new JSONObject(elm);
if (elmJson.has("library") && elmJson.getJSONObject("library").has("valueSets")) {
@@ -115,19 +125,11 @@ default CQLLibrary buildCQLLibrary(
// true for all non-composite measures
.is_top_level(true)
.statement_dependencies(
- statementDependencies.stream()
- .filter(
- statementDependency ->
- statementDependency
- .getStatement_name()
- .equals(
- elmJson
- .getJSONObject("library")
- .getJSONObject("identifier")
- .getString("id")))
- .collect(Collectors.toList()))
+ statementDependencies.get(
+ elmJson.getJSONObject("library").getJSONObject("identifier").getString("id")))
.build();
+ cqlLibrary.setElm(elmJson.toMap());
cqlLibrary.set_main_library(measureLibraryName.equals(cqlLibrary.getLibrary_name()));
return cqlLibrary;
@@ -135,7 +137,7 @@ default CQLLibrary buildCQLLibrary(
default List getSourceDataCriteria(List dataCriteria) {
if (CollectionUtils.isEmpty(dataCriteria)) {
- return null;
+ return Collections.emptyList();
}
return dataCriteria.stream()
@@ -261,4 +263,201 @@ default DataElement instantiateModel(String type) {
throw new CqmConversionException("Unsupported data type: " + type);
}
}
+
+ default List getPopulationSets(QdmMeasure measure) {
+ if (CollectionUtils.isEmpty(measure.getGroups())) {
+ return null;
+ }
+
+ String measureScoring = null;
+ if (!StringUtils.isEmpty(measure.getScoring())) {
+ measureScoring = measure.getScoring().replaceAll(" +", "");
+ }
+ List populationSets = new ArrayList<>();
+ for (int i = 0; i < measure.getGroups().size(); i++) {
+ String groupId = measure.getGroups().get(i).getId();
+ PopulationSet populationSet =
+ PopulationSet.builder()
+ .id(groupId)
+ .title("Population Criteria Section")
+ .population_set_id(groupId)
+ .populations(
+ generateCqmPopulations(
+ measure.getGroups().get(i).getPopulations(),
+ measure.getCqlLibraryName(),
+ measureScoring))
+ .stratifications(
+ generateCqmStratifications(
+ measure.getGroups().get(i).getStratifications(),
+ measure.getCqlLibraryName(),
+ i))
+ .supplemental_data_elements(
+ generateCqmSupplementalDataElements(
+ measure.getSupplementalData(), measure.getCqlLibraryName()))
+ .build();
+ if (!StringUtils.isEmpty(measureScoring)
+ && (measureScoring.equals("ContinuousVariable") || measureScoring.equals("Ratio"))) {
+ populationSet.setObservations(generateCqmObservations(measure));
+ }
+ populationSets.add(populationSet);
+ }
+ return populationSets;
+ }
+
+ default String mapPopulationName(PopulationType populationName) {
+ switch (populationName) {
+ case INITIAL_POPULATION:
+ return "IPP";
+ case DENOMINATOR:
+ return "DENOM";
+ case DENOMINATOR_EXCLUSION:
+ return "DENEX";
+ case DENOMINATOR_EXCEPTION:
+ return "DENEXCEP";
+ case NUMERATOR:
+ return "NUMER";
+ case NUMERATOR_EXCLUSION:
+ return "NUMEX";
+ case MEASURE_POPULATION:
+ return "MSRPOPL";
+ case MEASURE_POPULATION_EXCLUSION:
+ return "MSRPOPLEX";
+ default:
+ return populationName.name();
+ }
+ }
+
+ default PopulationMap determinePopulationType(Map acc, String measureScoring) {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
+ try {
+ switch (measureScoring) {
+ case "Cohort":
+ acc.put("@class", CohortPopulationMap.class);
+ return mapper.readValue(mapper.writeValueAsString(acc), CohortPopulationMap.class);
+ case "ContinuousVariable":
+ acc.put("@class", ContinuousVariablePopulationMap.class);
+ return mapper.readValue(
+ mapper.writeValueAsString(acc), ContinuousVariablePopulationMap.class);
+ case "Proportion":
+ acc.put("@class", ProportionPopulationMap.class);
+ return mapper.readValue(mapper.writeValueAsString(acc), ProportionPopulationMap.class);
+ case "Ratio":
+ acc.put("@class", RatioPopulationMap.class);
+ return mapper.readValue(mapper.writeValueAsString(acc), RatioPopulationMap.class);
+ default:
+ return null;
+ }
+
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ default PopulationMap generateCqmPopulations(
+ List populations, String cqlLibraryName, String measureScoring) {
+ if (StringUtils.isEmpty(measureScoring)) {
+ return null;
+ }
+ Map acc = new HashMap<>();
+ for (Population population : populations) {
+ String key = mapPopulationName(population.getName());
+ acc.put(
+ key,
+ StatementReference.builder()
+ .id(population.getId())
+ .library_name(cqlLibraryName)
+ .statement_name(population.getDefinition())
+ .hqmf_id(null)
+ .build());
+ }
+ return determinePopulationType(acc, measureScoring);
+ }
+
+ default List generateCqmStratifications(
+ List stratifications, String cqlLibraryName, int groupIndex) {
+ List cqmStratifications = new ArrayList<>();
+ if (stratifications != null) {
+ for (int i = 0; i < stratifications.size(); i++) {
+ Stratification stratification = stratifications.get(i);
+ CqmStratification cqmStratification =
+ CqmStratification.builder()
+ .id(UUID.randomUUID().toString())
+ .hqmf_id(null)
+ .stratification_id(
+ String.format("PopulationSet_%d_Stratification_%d", groupIndex + 1, i + 1))
+ .title(String.format("PopSet%d Stratification %d", groupIndex + 1, i + 1))
+ .statement(
+ StatementReference.builder()
+ .id(stratification.getId())
+ .library_name(cqlLibraryName)
+ .statement_name(stratification.getCqlDefinition())
+ .hqmf_id(null)
+ .build())
+ .build();
+ cqmStratifications.add(cqmStratification);
+ }
+ }
+ return cqmStratifications;
+ }
+
+ default List generateCqmSupplementalDataElements(
+ List supplementalDataElements, String cqlLibraryName) {
+ List statementReferences = new ArrayList<>();
+ for (DefDescPair element : supplementalDataElements) {
+ statementReferences.add(
+ StatementReference.builder()
+ .id(UUID.randomUUID().toString())
+ .library_name(cqlLibraryName)
+ .statement_name(element.getDefinition())
+ .hqmf_id(null)
+ .build());
+ }
+ return statementReferences;
+ }
+
+ default List generateCqmObservations(QdmMeasure measure) {
+ List cqmObservations = new ArrayList<>();
+ if (!CollectionUtils.isEmpty(measure.getGroups())) {
+ for (Group group : measure.getGroups()) {
+ if (!CollectionUtils.isEmpty(group.getMeasureObservations())) {
+ for (MeasureObservation observation : group.getMeasureObservations()) {
+ cqmObservations.add(
+ Observation.builder()
+ .id(observation.getId())
+ .hqmf_id(null)
+ .aggregation_type(observation.getAggregateMethod())
+ .observation_function(
+ StatementReference.builder()
+ .id(UUID.randomUUID().toString())
+ .library_name(measure.getCqlLibraryName())
+ .statement_name(observation.getDefinition())
+ .hqmf_id(null)
+ .build())
+ .observation_parameter(
+ StatementReference.builder()
+ .id(UUID.randomUUID().toString())
+ .library_name(measure.getCqlLibraryName())
+ .statement_name(
+ getAssociatedPopulationDefinition(
+ observation.getCriteriaReference(), group.getPopulations()))
+ .hqmf_id(null)
+ .build())
+ .build());
+ }
+ }
+ }
+ }
+ return cqmObservations;
+ }
+
+ default String getAssociatedPopulationDefinition(
+ String criteriaReference, List populations) {
+ for (Population population : populations) {
+ if (population.getId().equals(criteriaReference)) {
+ return population.getDefinition();
+ }
+ }
+ return null;
+ }
}
diff --git a/src/main/java/gov/cms/madie/services/QrdaService.java b/src/main/java/gov/cms/madie/services/QrdaService.java
index 13ac10e..904022d 100644
--- a/src/main/java/gov/cms/madie/services/QrdaService.java
+++ b/src/main/java/gov/cms/madie/services/QrdaService.java
@@ -7,6 +7,7 @@
import gov.cms.madie.dto.qrda.QrdaExportResponseDto;
import gov.cms.madie.dto.qrda.QrdaRequestDTO;
import gov.cms.madie.dto.SourceDataCriteria;
+import gov.cms.madie.models.cqm.CqmMeasure;
import gov.cms.madie.models.dto.TranslatedLibrary;
import gov.cms.madie.models.measure.QdmMeasure;
import lombok.AllArgsConstructor;
@@ -16,6 +17,7 @@
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -46,10 +48,11 @@ public QrdaExportResponseDto generateQrda(QrdaRequestDTO request, String accessT
QrdaDTO dto = null;
try {
+ CqmMeasure cqmMeasure = mapper.measureToCqmMeasure(measure, elms, null);
+ cqmMeasure.setPopulation_sets(new ArrayList<>());
dto =
QrdaDTO.builder()
- .measure(
- objectMapper.writeValueAsString(mapper.measureToCqmMeasure(measure, elms, null)))
+ .measure(objectMapper.writeValueAsString(cqmMeasure))
.testCases(measure.getTestCases())
.sourceDataCriteria(dataCriteria)
.options(buildOptions(measure))
diff --git a/src/main/java/gov/cms/madie/util/ElmDependencyUtil.java b/src/main/java/gov/cms/madie/util/ElmDependencyUtil.java
index c3bdae0..b9a26a3 100644
--- a/src/main/java/gov/cms/madie/util/ElmDependencyUtil.java
+++ b/src/main/java/gov/cms/madie/util/ElmDependencyUtil.java
@@ -12,7 +12,7 @@
@Slf4j
public class ElmDependencyUtil {
- public static List findDependencies(
+ public static Map> findDependencies(
List cqlLibraryElms, String mainCqlLibraryName) {
Map> neededElmDepsMap = new HashMap<>();
Map> allElmsDepMap = new HashMap<>();
@@ -28,7 +28,7 @@ public static List findDependencies(
throw new QrdaServiceException("library or identifier missing");
}
String elmId = elmJson.getJSONObject("library").getJSONObject("identifier").getString("id");
- neededElmDepsMap.put(elmId, List.of(buildStatementDependency(elmId, null, null)));
+ neededElmDepsMap.put(elmId, new ArrayList());
allElmsDepMap.put(elmId, makeStatementDepsForElm(elmJson));
}
neededElmDepsMap.put(mainCqlLibraryName, allElmsDepMap.get(mainCqlLibraryName));
@@ -42,7 +42,7 @@ public static List findDependencies(
reference ->
deepAddExternalLibraryDeps(reference, neededElmDepsMap, allElmsDepMap));
});
- return neededElmDepsMap.get(mainCqlLibraryName);
+ return neededElmDepsMap;
}
private static List makeStatementDepsForElm(JSONObject elmJson) {
@@ -74,34 +74,38 @@ private static void generateStatementDepsForElmHelper(
String parentName,
List elmDeps,
Map includedLibrariesMap) {
+ String parent = parentName;
if (obj instanceof JSONArray) {
((JSONArray) obj)
.forEach(
el ->
generateStatementDepsForElmHelper(
- el, libraryId, parentName, elmDeps, includedLibrariesMap));
+ el, libraryId, parent, elmDeps, includedLibrariesMap));
} else if (obj instanceof JSONObject jsonObj) {
List ref = List.of("ExpressionRef", "FunctionRef");
if (jsonObj.has("type")
&& ref.contains(jsonObj.getString("type"))
- && !"Patient".equals(parentName)
- && null != parentName) {
+ && !"Patient".equals(parentName)) {
if (elmDeps.isEmpty()) {
elmDeps.add(buildStatementDependency(parentName, libraryId, jsonObj));
} else {
elmDeps.stream()
- .map(
+ .filter(statementDependency -> parent.equals(statementDependency.getStatement_name()))
+ .forEach(
statementDependency -> {
- if (parentName.equals(statementDependency.getStatement_name())) {
+ if (jsonObj.has("libraryName")
+ && includedLibrariesMap.containsKey(jsonObj.get("libraryName"))) {
statementDependency
.getStatement_references()
.add(
buildStatementReference(
- includedLibrariesMap.getOrDefault(
- jsonObj.getString("libraryName"), libraryId),
+ includedLibrariesMap.get(jsonObj.getString("libraryName")),
jsonObj));
+ } else {
+ statementDependency
+ .getStatement_references()
+ .add(buildStatementReference(libraryId, jsonObj));
}
- return statementDependency;
});
}
} else if (jsonObj.has("name") && jsonObj.has("expression")) {
@@ -114,6 +118,7 @@ private static void generateStatementDepsForElmHelper(
.findAny()
.orElseGet(() -> buildStatementDependency(newParentName, null, null));
elmDeps.add(dep);
+ parentName = newParentName;
}
for (String key : jsonObj.keySet()) {
if (!key.equals("annotation")) {
diff --git a/src/test/java/gov/cms/madie/util/ElmDependencyUtilTest.java b/src/test/java/gov/cms/madie/util/ElmDependencyUtilTest.java
index 5fecdcb..e506a11 100644
--- a/src/test/java/gov/cms/madie/util/ElmDependencyUtilTest.java
+++ b/src/test/java/gov/cms/madie/util/ElmDependencyUtilTest.java
@@ -7,6 +7,7 @@
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -18,11 +19,23 @@ class ElmDependencyUtilTest implements ResourceFileUtil {
void findDependencies() throws Exception {
String elms = getStringFromTestResource("/elm/libraryElm.json");
String elms2 = getStringFromTestResource("/elm/libraryElm2.json");
- List dependencies =
+ Map> libraries =
ElmDependencyUtil.findDependencies(
List.of(elms, elms2), "UrinarySymptomScoreChangeAfterBenignProstaticHyperplasia");
- assertEquals(25, dependencies.size());
+ assertEquals(2, libraries.size());
+ assertEquals(
+ 25, libraries.get("UrinarySymptomScoreChangeAfterBenignProstaticHyperplasia").size());
+ assertEquals(4, libraries.get("MATGlobalCommonFunctionsQDM").size());
+ assertEquals(
+ 2,
+ libraries.get("MATGlobalCommonFunctionsQDM").stream()
+ .filter(
+ statementDependency -> statementDependency.getStatement_name().equals("EarliestOf"))
+ .findFirst()
+ .get()
+ .getStatement_references()
+ .size());
}
@Test