Skip to content

Commit

Permalink
Added support for population-level data requirement processing to IG …
Browse files Browse the repository at this point in the history
…refresh measure processor (#536)

- Updated Refresh IG params to include flag for population level data requirement processing
- Added the population level data requirement processing logic to the MeasureRefreshProcessor
- Tested locally with the NHSN measure repository measures
  • Loading branch information
c-schuler authored Oct 30, 2024
1 parent eb2ec44 commit 955f07e
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,25 @@
import java.util.concurrent.CopyOnWriteArrayList;

public class MeasureProcessor extends BaseProcessor {
public static String ResourcePrefix = "measure-";
public static final String RESOURCE_PREFIX = "measure-";
protected List<Object> identifiers;

public static String getId(String baseId) {
return ResourcePrefix + baseId;
return RESOURCE_PREFIX + baseId;
}

public List<String> refreshIgMeasureContent(BaseProcessor parentContext, Encoding outputEncoding, Boolean versioned, FhirContext fhirContext,
String measureToRefreshPath, Boolean shouldApplySoftwareSystemStamp) {
String measureToRefreshPath, Boolean shouldApplySoftwareSystemStamp, Boolean shouldIncludePopDataRequirements) {

return refreshIgMeasureContent(parentContext, outputEncoding, null, versioned, fhirContext, measureToRefreshPath,
shouldApplySoftwareSystemStamp);
shouldApplySoftwareSystemStamp, shouldIncludePopDataRequirements);
}



public List<String> refreshIgMeasureContent(BaseProcessor parentContext, Encoding outputEncoding, String measureOutputDirectory,
Boolean versioned, FhirContext fhirContext, String measureToRefreshPath,
Boolean shouldApplySoftwareSystemStamp) {
Boolean shouldApplySoftwareSystemStamp, Boolean shouldIncludePopDataRequirements) {

logger.info("[Refreshing Measures]");

Expand All @@ -61,6 +63,8 @@ public List<String> refreshIgMeasureContent(BaseProcessor parentContext, Encodin
params.encoding = outputEncoding;
params.versioned = versioned;
params.measureOutputDirectory = measureOutputDirectory;
params.shouldApplySoftwareSystemStamp = shouldApplySoftwareSystemStamp;
params.includePopulationDataRequirements = shouldIncludePopDataRequirements;
List<String> contentList = measureProcessor.refreshMeasureContent(params);

if (!measureProcessor.getIdentifiers().isEmpty()) {
Expand Down Expand Up @@ -111,15 +115,15 @@ private Measure refreshGeneratedContent(Measure measure, MeasureRefreshProcessor
VersionedIdentifier primaryLibraryIdentifier = CanonicalUtils.toVersionedIdentifier(libraryUrl);

List<CqlCompilerException> errors = new CopyOnWriteArrayList<>();
CompiledLibrary CompiledLibrary = libraryManager.resolveLibrary(primaryLibraryIdentifier, errors);
CompiledLibrary compiledLibrary = libraryManager.resolveLibrary(primaryLibraryIdentifier, errors);

logger.info(CqlProcessor.buildStatusMessage(errors, measure.getName(), verboseMessaging));

boolean hasSevereErrors = CqlProcessor.hasSevereErrors(errors);

//refresh measures without severe errors:
if (!hasSevereErrors) {
return processor.refreshMeasure(measure, libraryManager, CompiledLibrary, cqlTranslatorOptions.getCqlCompilerOptions());
return processor.refreshMeasure(measure, libraryManager, compiledLibrary, cqlTranslatorOptions.getCqlCompilerOptions());
}

return measure;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Collections;

import org.cqframework.cql.cql2elm.CqlCompilerOptions;
import org.cqframework.cql.cql2elm.LibraryManager;
Expand All @@ -17,19 +18,13 @@
import org.hl7.fhir.r5.model.Reference;
import org.hl7.fhir.r5.model.RelatedArtifact;
import org.hl7.fhir.r5.model.Resource;
import org.hl7.fhir.r5.model.StringType;

public class MeasureRefreshProcessor {
public Measure refreshMeasure(Measure measureToUse, LibraryManager libraryManager, CompiledLibrary CompiledLibrary, CqlCompilerOptions options) {

Library moduleDefinitionLibrary = getModuleDefinitionLibrary(measureToUse, libraryManager, CompiledLibrary, options);
public Boolean includePopulationDataRequirements = false;

measureToUse.setDate(new Date());
// http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/measure-cqfm
setMeta(measureToUse, moduleDefinitionLibrary);
// Don't need to do this... it is required information to perform this processing in the first place, should just be left alone
//setLibrary(measureToUse, CompiledLibrary);
// Don't need to do this... type isn't a computable attribute, it's just metadata and will come from the source measure
//setType(measureToUse);
public Measure refreshMeasure(Measure measureToUse, LibraryManager libraryManager, CompiledLibrary compiledLibrary, CqlCompilerOptions options) {

// Computable measure http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/computable-measure-cqfm
clearMeasureExtensions(measureToUse, "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-parameter");
Expand All @@ -39,29 +34,46 @@ public Measure refreshMeasure(Measure measureToUse, LibraryManager libraryManage
clearMeasureExtensions(measureToUse, "http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-effectiveDataRequirements");
clearRelatedArtifacts(measureToUse);

Library moduleDefinitionLibrary = getModuleDefinitionLibrary(measureToUse, libraryManager, compiledLibrary, options);
measureToUse.setDate(new Date());
// http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/measure-cqfm
setMeta(measureToUse, moduleDefinitionLibrary);
moduleDefinitionLibrary.setId("effective-data-requirements");
setEffectiveDataRequirements(measureToUse, moduleDefinitionLibrary);
setEffectiveDataRequirementsReference(measureToUse);
if (Boolean.TRUE.equals(includePopulationDataRequirements)) {
setPopulationDataRequirements(measureToUse, libraryManager, compiledLibrary, options);
}

return measureToUse;
}

private Library getModuleDefinitionLibrary(Measure measureToUse, LibraryManager libraryManager, CompiledLibrary CompiledLibrary, CqlCompilerOptions options){
private Library getModuleDefinitionLibrary(Measure measureToUse, LibraryManager libraryManager, CompiledLibrary compiledLibrary, CqlCompilerOptions options){
Set<String> expressionList = getExpressions(measureToUse);
DataRequirementsProcessor dqReqTrans = new DataRequirementsProcessor();
return dqReqTrans.gatherDataRequirements(libraryManager, CompiledLibrary, options, expressionList, true);
return dqReqTrans.gatherDataRequirements(libraryManager, compiledLibrary, options, expressionList, true);
}

private void setPopulationDataRequirements(Measure measureToUse, LibraryManager libraryManager, CompiledLibrary compiledLibrary, CqlCompilerOptions options) {
DataRequirementsProcessor dqReqTrans = new DataRequirementsProcessor();
measureToUse.getGroup().forEach(groupMember -> groupMember.getPopulation().forEach(population -> {
if (population.hasId()) { // Requirement for computable measures
var popMDL = dqReqTrans.gatherDataRequirements(libraryManager, compiledLibrary, options, Collections.singleton(population.getCriteria().getExpression()), false);
var mdlID = population.getId() + "-effectiveDataRequirements";
popMDL.setId(mdlID);
setEffectiveDataRequirements(measureToUse, popMDL);
population.getExtension().removeAll(population.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/artifact-reference"));
population.addExtension("http://hl7.org/fhir/StructureDefinition/artifact-reference", new StringType("#" + mdlID));
}
}));
}

private Set<String> getExpressions(Measure measureToUse) {
Set<String> expressionSet = new HashSet<>();
measureToUse.getSupplementalData().forEach(supData->{
expressionSet.add(supData.getCriteria().getExpression());
});
measureToUse.getSupplementalData().forEach(supData-> expressionSet.add(supData.getCriteria().getExpression()));
measureToUse.getGroup().forEach(groupMember->{
groupMember.getPopulation().forEach(population->{
expressionSet.add(population.getCriteria().getExpression());
});
groupMember.getStratifier().forEach(stratifier->{
expressionSet.add(stratifier.getCriteria().getExpression());
});
groupMember.getPopulation().forEach(population-> expressionSet.add(population.getCriteria().getExpression()));
groupMember.getStratifier().forEach(stratifier-> expressionSet.add(stratifier.getCriteria().getExpression()));
});
return expressionSet;
}
Expand All @@ -76,12 +88,9 @@ private void clearRelatedArtifacts(Measure measure) {
}

private void setEffectiveDataRequirements(Measure measureToUse, Library moduleDefinitionLibrary) {

moduleDefinitionLibrary.setId("effective-data-requirements");

int delIndex = -1;
for (Resource res : measureToUse.getContained()) {
if (res instanceof Library && ((Library)res).getId().equalsIgnoreCase("effective-data-requirements")) {
if (res instanceof Library && res.getId().equalsIgnoreCase(moduleDefinitionLibrary.getId())) {
delIndex = measureToUse.getContained().indexOf(res);
break;
}
Expand All @@ -92,9 +101,11 @@ private void setEffectiveDataRequirements(Measure measureToUse, Library moduleDe
}

measureToUse.getContained().add(moduleDefinitionLibrary);
}

Extension effDataReqExtension = new Extension();
effDataReqExtension.setUrl("http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-effectiveDataRequirements");
private void setEffectiveDataRequirementsReference(Measure measureToUse) {
Extension effDataReqExtension = new Extension();
effDataReqExtension.setUrl("http://hl7.org/fhir/us/cqfmeasures/StructureDefinition/cqfm-effectiveDataRequirements");
effDataReqExtension.setId("effective-data-requirements");
effDataReqExtension.setValue(new Reference().setReference("#effective-data-requirements"));
measureToUse.addExtension(effDataReqExtension);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
package org.opencds.cqf.tooling.measure.r4;

import org.cqframework.cql.cql2elm.CqlCompilerException;
import org.cqframework.cql.cql2elm.CqlTranslatorOptions;
import org.cqframework.cql.cql2elm.LibraryManager;
import org.cqframework.cql.cql2elm.model.CompiledLibrary;
import org.hl7.elm.r1.VersionedIdentifier;
import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_40_50;
import org.hl7.fhir.convertors.conv40_50.VersionConvertor_40_50;
import org.hl7.fhir.r4.formats.FormatUtilities;
import org.hl7.fhir.r5.model.Measure;
import org.opencds.cqf.tooling.common.r4.CqfmSoftwareSystemHelper;
import org.opencds.cqf.tooling.measure.MeasureProcessor;
import org.opencds.cqf.tooling.measure.MeasureRefreshProcessor;
import org.opencds.cqf.tooling.parameter.RefreshMeasureParameters;
import org.opencds.cqf.tooling.processor.CqlProcessor;
import org.opencds.cqf.tooling.utilities.CanonicalUtils;
import org.opencds.cqf.tooling.utilities.IOUtils;
import org.opencds.cqf.tooling.utilities.ResourceUtils;

import java.io.File;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

public class R4MeasureProcessor extends MeasureProcessor {

private String measurePath;
private String measureOutputDirectory;
private IOUtils.Encoding encoding;
private Boolean shouldApplySoftwareSystemStamp;
private Boolean includePopulationDataRequirements;
private static CqfmSoftwareSystemHelper cqfmHelper;

private String getMeasurePath(String measurePath) {
Expand Down Expand Up @@ -60,11 +73,11 @@ else if (file.isDirectory()) {
loadMeasure(fileMap, measures, file);
}

List<String> refreshedMeasureNames = new ArrayList<String>();
List<org.hl7.fhir.r5.model.Measure> refreshedMeasures = super.refreshGeneratedContent(measures);
VersionConvertor_40_50 versionConvertor_40_50 = new VersionConvertor_40_50(new BaseAdvisor_40_50());
List<String> refreshedMeasureNames = new ArrayList<>();
List<org.hl7.fhir.r5.model.Measure> refreshedMeasures = refreshGeneratedContent(measures);
VersionConvertor_40_50 versionConvertor = new VersionConvertor_40_50(new BaseAdvisor_40_50());
for (org.hl7.fhir.r5.model.Measure refreshedMeasure : refreshedMeasures) {
org.hl7.fhir.r4.model.Measure measure = (org.hl7.fhir.r4.model.Measure) versionConvertor_40_50.convertResource(refreshedMeasure);
org.hl7.fhir.r4.model.Measure measure = (org.hl7.fhir.r4.model.Measure) versionConvertor.convertResource(refreshedMeasure);
if (measure.hasIdentifier() && !measure.getIdentifier().isEmpty()) {
this.getIdentifiers().addAll(measure.getIdentifier());
}
Expand Down Expand Up @@ -111,15 +124,61 @@ else if (file.isDirectory()) {
private void loadMeasure(Map<String, String> fileMap, List<org.hl7.fhir.r5.model.Measure> measures, File measureFile) {
try {
org.hl7.fhir.r4.model.Resource resource = FormatUtilities.loadFile(measureFile.getAbsolutePath());
VersionConvertor_40_50 versionConvertor_40_50 = new VersionConvertor_40_50(new BaseAdvisor_40_50());
org.hl7.fhir.r5.model.Measure measure = (org.hl7.fhir.r5.model.Measure) versionConvertor_40_50.convertResource(resource);
VersionConvertor_40_50 versionConvertor = new VersionConvertor_40_50(new BaseAdvisor_40_50());
org.hl7.fhir.r5.model.Measure measure = (org.hl7.fhir.r5.model.Measure) versionConvertor.convertResource(resource);
fileMap.put(measure.getId(), measureFile.getAbsolutePath());
measures.add(measure);
} catch (Exception ex) {
logMessage(String.format("Error reading measure: %s. Error: %s", measureFile.getAbsolutePath(), ex.getMessage()));
}
}

@Override
protected List<Measure> refreshGeneratedContent(List<Measure> sourceMeasures) {
return internalRefreshGeneratedContent(sourceMeasures);
}

private List<Measure> internalRefreshGeneratedContent(List<Measure> sourceMeasures) {
// for each Measure, refresh the measure based on the primary measure library
List<Measure> resources = new ArrayList<>();
MeasureRefreshProcessor processor = new MeasureRefreshProcessor();
LibraryManager libraryManager = getCqlProcessor().getLibraryManager();
CqlTranslatorOptions cqlTranslatorOptions = getCqlProcessor().getCqlTranslatorOptions();
for (Measure measure : sourceMeasures) {
// Do not attempt to refresh if the measure does not have a library
if (measure.hasLibrary()) {
resources.add(refreshGeneratedContent(measure, processor, libraryManager, cqlTranslatorOptions));
} else {
resources.add(measure);
}
}

return resources;
}

private Measure refreshGeneratedContent(Measure measure, MeasureRefreshProcessor processor, LibraryManager libraryManager, CqlTranslatorOptions cqlTranslatorOptions) {

String libraryUrl = ResourceUtils.getPrimaryLibraryUrl(measure, fhirContext);
VersionedIdentifier primaryLibraryIdentifier = CanonicalUtils.toVersionedIdentifier(libraryUrl);

List<CqlCompilerException> errors = new CopyOnWriteArrayList<>();
CompiledLibrary compiledLibrary = libraryManager.resolveLibrary(primaryLibraryIdentifier, errors);

logger.info(CqlProcessor.buildStatusMessage(errors, measure.getName(), verboseMessaging));

boolean hasSevereErrors = CqlProcessor.hasSevereErrors(errors);

//refresh measures without severe errors:
if (!hasSevereErrors) {
if (includePopulationDataRequirements != null) {
processor.includePopulationDataRequirements = includePopulationDataRequirements;
}
return processor.refreshMeasure(measure, libraryManager, compiledLibrary, cqlTranslatorOptions.getCqlCompilerOptions());
}

return measure;
}

@Override
public List<String> refreshMeasureContent(RefreshMeasureParameters params) {
if (params.parentContext != null) {
Expand All @@ -134,6 +193,8 @@ public List<String> refreshMeasureContent(RefreshMeasureParameters params) {
fhirContext = params.fhirContext;
encoding = params.encoding;
versioned = params.versioned;
shouldApplySoftwareSystemStamp = params.shouldApplySoftwareSystemStamp;
includePopulationDataRequirements = params.includePopulationDataRequirements;

R4MeasureProcessor.cqfmHelper = new CqfmSoftwareSystemHelper(rootDir);

Expand Down
Loading

0 comments on commit 955f07e

Please sign in to comment.