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