-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
LNK-2950: Ability to run measure eval using a command line interface (#…
…460) Adding FileSystemInvocation class that allows evaluating a measure using a data from the locally running file system Format: `java -jar measureeval-cli.jar "<measure-bundle-path>" "<patient-bundle-path>" "<start>" "<end>"` Example: `java -jar measureeval-cli.jar "C:/path/to/measure-bundle.json" "C:/path/to/patient-bundle.json" "2021-01-01" "2021-12-31"` Or directly via IntelliJ with a debug configuration. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a command-line interface for evaluating healthcare measures using files from the filesystem. - Users can specify paths for measure and patient bundles, along with evaluation date ranges. - Comprehensive documentation added to guide users on building and running the module. - Enhanced flexibility with multiple execution profiles for different use cases. - New logging configuration for improved monitoring and debugging. - **Bug Fixes** - Improved error handling during file loading and evaluation processes to provide clearer feedback. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
- Loading branch information
Showing
5 changed files
with
279 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# measureeval | ||
|
||
## Building for use as CLI | ||
|
||
Perform the following from the `/Java` directory, which builds the measureeval JAR file _and_ the dependent shared | ||
module: | ||
|
||
```bash | ||
mvn -P cli -pl measureeval -am clean package | ||
``` | ||
|
||
We use the `cli` Maven profile to ensure that `FileSystemInvocation` is used as the main class. | ||
|
||
### Parameters | ||
|
||
| Parameter | Description | | ||
|---------------------|--------------------------------------------------------------------------------------| | ||
| measure-bundle-path | The path to the measure bundle JSON file or a directory of resource JSON/XML files. | | ||
| patient-bundle-path | The path to the patient bundle JSON file or a directory of resources JSON/XML files. | | ||
| start | The start date for the measurement period in FHIR Date or DateTime format. | | ||
| end | The end date for the measurement period in FHIR Date or DateTime format. | | ||
|
||
### Format/Example | ||
|
||
Format: | ||
|
||
```bash | ||
java -jar measureeval-cli.jar "<measure-bundle-path>" "<patient-bundle-path>" "<start>" "<end>" | ||
``` | ||
|
||
Example: | ||
|
||
```bash | ||
java -jar measureeval-cli.jar "C:/path/to/measure-bundle.json" "C:/path/to/patient-bundle.json" "2021-01-01" "2021-12-31" | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
185 changes: 185 additions & 0 deletions
185
Java/measureeval/src/main/java/com/lantanagroup/link/measureeval/FileSystemInvocation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
package com.lantanagroup.link.measureeval; | ||
|
||
import ca.uhn.fhir.context.FhirContext; | ||
import com.lantanagroup.link.measureeval.services.MeasureEvaluator; | ||
import com.lantanagroup.link.measureeval.utils.StreamUtils; | ||
import org.apache.commons.io.FileUtils; | ||
import org.apache.commons.io.FilenameUtils; | ||
import org.hl7.fhir.r4.model.*; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
|
||
/** | ||
* This class is used to invoke measure evaluation using arbitrary artifacts on the file system for ease of testing and debugging new measures. | ||
* See the main measureeval project's README for more details about how to execute the JAR from the command line. | ||
* Careful when specifying file paths to use forward-slash instead of windows' backslash, as that will cause java to interpret the backslash as an escape character and not as a path separator, leading to potentially incorrect parameter interpretation. | ||
* The `start` and `end` parameters must be in a valid DateTime format from the FHIR specification. | ||
* The `measure-bundle-path` can be a path to a single measure bundle file or a directory containing each of the resources needed for the measure. | ||
* The `patient-bundle-path` must be a path to a single (JSON or XML) Bundle file or a directory of files containing the patient data to be used in the evaluation. | ||
* The response from the operation is the MeasureReport resource being printed to the console in JSON format. | ||
*/ | ||
public class FileSystemInvocation { | ||
private static final FhirContext fhirContext = FhirContext.forR4Cached(); | ||
private static final Logger logger = LoggerFactory.getLogger(FileSystemInvocation.class); | ||
|
||
private static Bundle getBundle(String measureBundlePath) throws IOException { | ||
logger.info("Loading measure bundle from: {}", measureBundlePath); | ||
|
||
try { | ||
File measureBundleFile = new File(measureBundlePath); | ||
|
||
if (!measureBundleFile.exists()) { | ||
throw new IllegalArgumentException("Measure bundle file does not exist: " + measureBundlePath); | ||
} | ||
|
||
if (measureBundleFile.isFile()) { | ||
String measureBundleContent = FileUtils.readFileToString(measureBundleFile, "UTF-8"); | ||
if (measureBundlePath.toLowerCase().endsWith(".json")) { | ||
return fhirContext.newJsonParser().parseResource(Bundle.class, measureBundleContent); | ||
} else if (measureBundlePath.toLowerCase().endsWith(".xml")) { | ||
return fhirContext.newXmlParser().parseResource(Bundle.class, measureBundleContent); | ||
} else { | ||
throw new IllegalArgumentException("Unsupported measure bundle file format: " + measureBundlePath); | ||
} | ||
} else { | ||
// Parse and load each file in the directory | ||
Bundle bundle = new Bundle(); | ||
bundle.setType(Bundle.BundleType.COLLECTION); | ||
|
||
File[] files = measureBundleFile.listFiles(); | ||
|
||
if (files != null) { | ||
HashSet<String> loaded = new HashSet<>(); | ||
|
||
for (File file : files) { | ||
String filePath = file.getAbsolutePath(); | ||
String fileExtension = file.isFile() ? FilenameUtils.getExtension(filePath) : null; | ||
|
||
if (file.isFile() && (fileExtension.equalsIgnoreCase("json") || fileExtension.equalsIgnoreCase("xml"))) { | ||
String fileName = FilenameUtils.getBaseName(filePath); | ||
|
||
if (loaded.contains(fileName)) { | ||
logger.warn("Skipping duplicate file: {}", filePath); | ||
} | ||
|
||
Resource resource; | ||
|
||
if (filePath.endsWith(".json")) { | ||
resource = (Resource) fhirContext.newJsonParser().parseResource(FileUtils.readFileToString(file, "UTF-8")); | ||
} else { | ||
resource = (Resource) fhirContext.newXmlParser().parseResource(FileUtils.readFileToString(file, "UTF-8")); | ||
} | ||
|
||
bundle.addEntry(new Bundle.BundleEntryComponent().setResource(resource)); | ||
loaded.add(fileName); | ||
} else { | ||
logger.warn("Skipping file: {}", filePath); | ||
} | ||
} | ||
} | ||
|
||
logger.info("Loaded " + bundle.getEntry().size() + " resources from directory: " + measureBundlePath); | ||
|
||
return bundle; | ||
} | ||
} catch (IOException ex) { | ||
logger.error("Error occurred while loading measure bundle: {}", ex.getMessage()); | ||
throw ex; | ||
} | ||
} | ||
|
||
|
||
public static List<Bundle> getBundlesFromDirectoryAndSubDirectories(String directory) { | ||
List<Bundle> bundles = new ArrayList<>(); | ||
File[] files = new File(directory).listFiles(); | ||
if (files != null) { | ||
for (File file : files) { | ||
if (file.isDirectory()) { | ||
bundles.addAll(getBundlesFromDirectoryAndSubDirectories(file.getAbsolutePath())); | ||
} else { | ||
try { | ||
if (!file.getAbsolutePath().toLowerCase().endsWith(".json") && !file.getAbsolutePath().toLowerCase().endsWith(".xml")) { | ||
continue; | ||
} | ||
Bundle bundle = getBundle(file.getAbsolutePath()); | ||
bundles.add(bundle); | ||
} catch (IOException e) { | ||
System.err.println("Error occurred while loading bundle: " + e.getMessage()); | ||
continue; | ||
} | ||
} | ||
} | ||
} | ||
return bundles; | ||
} | ||
|
||
public static String getGroupPopulations(MeasureReport measureReport) { | ||
StringBuilder populations = new StringBuilder(); | ||
for (MeasureReport.MeasureReportGroupComponent group : measureReport.getGroup()) { | ||
populations.append("Group: ").append(group.getId()).append("\n"); | ||
for (MeasureReport.MeasureReportGroupPopulationComponent population : group.getPopulation()) { | ||
populations.append("Population: ").append(population.getCode().getCodingFirstRep().getDisplay()).append(" - ").append(population.getCount()).append("\n"); | ||
} | ||
} | ||
return populations.toString(); | ||
} | ||
|
||
private static Patient findPatient(Bundle bundle) { | ||
return bundle.getEntry().stream() | ||
.filter(e -> e.getResource() instanceof Patient) | ||
.map(e -> (Patient) e.getResource()) | ||
.reduce(StreamUtils::toOnlyElement) | ||
.orElseThrow(() -> new IllegalArgumentException("Patient resource not found in bundle")); | ||
} | ||
|
||
private static void evaluatePatientBundle(String patientBundlePath, Bundle patientBundle, String start, String end, MeasureEvaluator evaluator) { | ||
Patient patient = findPatient(patientBundle); | ||
var report = evaluator.evaluate( | ||
new DateTimeType(start), | ||
new DateTimeType(end), | ||
new StringType("Patient/" + patient.getIdElement().getIdPart()), | ||
patientBundle); | ||
String json = fhirContext.newJsonParser().encodeResourceToString(report); | ||
logger.info("Summary of evaluate for patient/groups/populations:\nPatient: {}\n{}\nJSON: {}", patient.getIdElement().getIdPart(), getGroupPopulations(report), json); | ||
} | ||
|
||
public static void main(String[] args) { | ||
if (args.length != 4) { | ||
System.err.println("Invalid number of arguments. Expected 4 arguments: <measure-bundle-path> <patient-bundle-path> <start> <end>"); | ||
System.exit(1); | ||
} | ||
|
||
String measureBundlePath = args[0]; | ||
String patientBundlePath = args[1]; | ||
String start = args[2]; | ||
String end = args[3]; | ||
|
||
try { | ||
Bundle measureBundle = getBundle(measureBundlePath); | ||
MeasureEvaluator evaluator = MeasureEvaluator.compile(fhirContext, measureBundle, true); | ||
|
||
File patientBundleFile = new File(patientBundlePath); | ||
|
||
if (patientBundleFile.isDirectory()) { | ||
List<Bundle> patientBundles = getBundlesFromDirectoryAndSubDirectories(patientBundlePath); | ||
|
||
for (Bundle patientBundle : patientBundles) { | ||
logger.info("\n==================================================="); | ||
evaluatePatientBundle(patientBundlePath, patientBundle, start, end, evaluator); | ||
} | ||
} else { | ||
Bundle patientBundle = getBundle(patientBundlePath); | ||
evaluatePatientBundle(patientBundlePath, patientBundle, start, end, evaluator); | ||
} | ||
} catch (Exception e) { | ||
System.err.println("Error occurred while evaluating measure: " + e.getMessage()); | ||
e.printStackTrace(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<configuration> | ||
|
||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> | ||
<layout class="ch.qos.logback.classic.PatternLayout"> | ||
<Pattern> | ||
%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n | ||
</Pattern> | ||
</layout> | ||
</appender> | ||
|
||
<logger name="com.lantanagroup.link" level="info" additivity="false"> | ||
<appender-ref ref="CONSOLE"/> | ||
</logger> | ||
|
||
<logger name="org.opencds.cqf" level="debug" additivity="false"> | ||
<appender-ref ref="CONSOLE"/> | ||
</logger> | ||
|
||
<root level="error"> | ||
<appender-ref ref="CONSOLE"/> | ||
</root> | ||
|
||
</configuration> |