Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/LNK-3102' into LNK-3102
Browse files Browse the repository at this point in the history
  • Loading branch information
seanmcilvenna committed Dec 31, 2024
2 parents 726172c + 604e8da commit ea52c8f
Show file tree
Hide file tree
Showing 21 changed files with 496 additions and 68 deletions.
15 changes: 14 additions & 1 deletion Azure_Pipelines/azure-pipelines.measureeval.cd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ steps:
Write-Host "Set MyTag to: $myTag1"
- task: Docker@2
displayName: "Build & Push Audit Docker Image"
displayName: "Build & Push MeasureEval Docker Image"
condition: always()
inputs:
containerRegistry: $(containerRegistry)
Expand All @@ -89,3 +89,16 @@ steps:
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)'
artifact: 'manifest'

- task: Maven@2
displayName: Package CLI
inputs:
mavenPomFile: 'Java/pom.xml'
goals: 'clean package'
options: '-P cli -pl measureeval -am'

- task: PublishPipelineArtifact@1
displayName: 'Publish MeasureEval CLI Jar'
inputs:
targetPath: 'Java/measureeval/target/measureeval-cli.jar'
artifact: 'measureeval-cli-jar'
1 change: 0 additions & 1 deletion Java/measureeval/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,6 @@
<id>cli</id>
<properties>
<mainClass>com.lantanagroup.link.measureeval.FileSystemInvocation</mainClass>
<logback.configurationFile>src/main/resources/logback-cli.xml</logback.configurationFile>
</properties>
<build>
<finalName>measureeval-cli</finalName>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.lantanagroup.link.measureeval;

import ca.uhn.fhir.context.FhirContext;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import com.lantanagroup.link.measureeval.services.MeasureEvaluator;
import com.lantanagroup.link.measureeval.utils.CqlLogAppender;
import com.lantanagroup.link.measureeval.utils.CqlUtils;
import com.lantanagroup.link.measureeval.utils.StreamUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
Expand All @@ -28,6 +33,15 @@ public class FileSystemInvocation {
private static final FhirContext fhirContext = FhirContext.forR4Cached();
private static final Logger logger = LoggerFactory.getLogger(FileSystemInvocation.class);

private static void configureLogging(Bundle bundle) throws JoranException {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
context.reset();
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
configurator.doConfigure(ClassLoader.getSystemResource("logback-cli.xml"));
CqlLogAppender.start(context, libraryId -> CqlUtils.getLibrary(bundle, libraryId));
}

private static Bundle getBundle(String measureBundlePath) throws IOException {
logger.info("Loading measure bundle from: {}", measureBundlePath);

Expand Down Expand Up @@ -138,7 +152,7 @@ private static Patient findPatient(Bundle bundle) {
.orElseThrow(() -> new IllegalArgumentException("Patient resource not found in bundle"));
}

private static void evaluatePatientBundle(String patientBundlePath, Bundle patientBundle, String start, String end, MeasureEvaluator evaluator) {
private static void evaluatePatientBundle(Bundle patientBundle, String start, String end, MeasureEvaluator evaluator, boolean isDebug) {
Patient patient = findPatient(patientBundle);
var report = evaluator.evaluate(
new DateTimeType(start),
Expand All @@ -162,6 +176,7 @@ public static void main(String[] args) {

try {
Bundle measureBundle = getBundle(measureBundlePath);
configureLogging(measureBundle);
MeasureEvaluator evaluator = MeasureEvaluator.compile(fhirContext, measureBundle, true);

File patientBundleFile = new File(patientBundlePath);
Expand All @@ -171,11 +186,11 @@ public static void main(String[] args) {

for (Bundle patientBundle : patientBundles) {
logger.info("\n===================================================");
evaluatePatientBundle(patientBundlePath, patientBundle, start, end, evaluator);
evaluatePatientBundle(patientBundle, start, end, evaluator, true);
}
} else {
Bundle patientBundle = getBundle(patientBundlePath);
evaluatePatientBundle(patientBundlePath, patientBundle, start, end, evaluator);
evaluatePatientBundle(patientBundle, start, end, evaluator, true);
}
} catch (Exception e) {
System.err.println("Error occurred while evaluating measure: " + e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.lantanagroup.link.measureeval;

import ch.qos.logback.classic.LoggerContext;
import com.lantanagroup.link.measureeval.services.MeasureEvaluatorCache;
import com.lantanagroup.link.measureeval.utils.CqlLogAppender;
import org.slf4j.LoggerFactory;
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.mongodb.config.EnableMongoAuditing;

Expand All @@ -20,4 +25,10 @@ public static void main(String[] args) {
application.setBannerMode(Banner.Mode.OFF);
application.run(args);
}

@Bean
public CqlLogAppender cqlLogAppender(MeasureEvaluatorCache measureEvaluatorCache) {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
return CqlLogAppender.start(loggerContext, measureEvaluatorCache);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
public class LinkConfig {
private String reportabilityPredicate;

private boolean cqlDebug = false;

@Bean
@SuppressWarnings("unchecked")
public Predicate<MeasureReport> reportabilityPredicate() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package com.lantanagroup.link.measureeval.controllers;

import ca.uhn.fhir.context.FhirContext;
import com.fasterxml.jackson.annotation.JsonView;
import com.lantanagroup.link.measureeval.entities.MeasureDefinition;
import com.lantanagroup.link.measureeval.repositories.MeasureDefinitionRepository;
import com.lantanagroup.link.measureeval.serdes.Views;
import com.lantanagroup.link.measureeval.services.MeasureDefinitionBundleValidator;
import com.lantanagroup.link.measureeval.services.MeasureEvaluator;
import com.lantanagroup.link.measureeval.services.MeasureEvaluatorCache;
import com.lantanagroup.link.measureeval.utils.CqlUtils;
import com.lantanagroup.link.shared.auth.PrincipalUser;
import io.opentelemetry.api.trace.Span;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import javassist.NotFoundException;
import org.apache.commons.text.StringEscapeUtils;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.MeasureReport;
Expand Down Expand Up @@ -98,21 +102,50 @@ public MeasureDefinition put(@AuthenticationPrincipal PrincipalUser user, @PathV
return entity;
}

@GetMapping("/{id}/{library-id}/$cql")
@PreAuthorize("hasAuthority('IsLinkAdmin')")
@Operation(summary = "Get the CQL for a measure definition's library", tags = {"Measure Definitions"})
@Parameter(name = "id", description = "The ID of the measure definition", required = true)
@Parameter(name = "library-id", description = "The ID of the library in the measure definition", required = true)
@Parameter(name = "range", description = "The range of the CQL to return (e.g. 37:1-38:22)", required = false)
public String getMeasureLibraryCQL(
@PathVariable("id") String measureId,
@PathVariable("library-id") String libraryId,
@RequestParam(value = "range", required = false) String range) {

// Test that the range format is correct (i.e. "37:1-38:22")
if (range != null && !range.matches("\\d+:\\d+-\\d+:\\d+")) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid range format");
}

// Get the measure definition from the repo by ID
MeasureDefinition measureDefinition = repository.findById(measureId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

try {
return CqlUtils.getCql(measureDefinition.getBundle(), libraryId, range);
} catch (NotFoundException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, e.getMessage(), e);
}
}

@PostMapping("/{id}/$evaluate")
@PreAuthorize("hasAuthority('IsLinkAdmin')")
@Operation(summary = "Evaluate a measure against data in request body", tags = {"Measure Definitions"})
public MeasureReport evaluate(@AuthenticationPrincipal PrincipalUser user, @PathVariable String id, @RequestBody Parameters parameters) {
@Parameter(name = "id", description = "The ID of the measure definition", required = true)
@Parameter(name = "parameters", description = "The parameters to use in the evaluation", required = true)
@Parameter(name = "debug", description = "Whether to log CQL debugging information during evaluation", required = false)
public MeasureReport evaluate(@AuthenticationPrincipal PrincipalUser user, @PathVariable String id, @RequestBody Parameters parameters, @RequestParam(required = false, defaultValue = "false") boolean debug) {

if (user != null){
Span currentSpan = Span.current();
currentSpan.setAttribute("user", user.getEmailAddress());
}
MeasureEvaluator evaluator = evaluatorCache.get(id);
if (evaluator == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}

try {
return evaluator.evaluate(parameters);
// Ensure that a measure evaluator is cached (so that CQL logging can use it)
MeasureEvaluator evaluator = evaluatorCache.get(id);
// But recompile the bundle every time because the debug flag may not match what's in the cache
return MeasureEvaluator.compileAndEvaluate(FhirContext.forR4(), evaluator.getBundle(), parameters, debug);
} catch (Exception e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage(), e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.lantanagroup.link.measureeval.repositories;

import ca.uhn.fhir.context.FhirContext;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.r4.model.Bundle;
import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

/**
* This class extends the InMemoryFhirRepository to provide a transaction method that will update the resources in the repository.
* This implementation is primarily used to avoid the exception stack trace that is thrown when the InMemoryFhirRepository.transaction method is called by measure eval, but is not implemented
* in the default InMemoryFhirRepository.
*/
public class LinkInMemoryFhirRepository extends InMemoryFhirRepository {
private static final Logger logger = LoggerFactory.getLogger(LinkInMemoryFhirRepository.class);

public LinkInMemoryFhirRepository(FhirContext context) {
super(context);
}

public LinkInMemoryFhirRepository(FhirContext context, IBaseBundle bundle) {
super(context, bundle);
}

@Override
public <B extends IBaseBundle> B transaction(B transaction, Map<String, String> headers) {
if (!(transaction instanceof Bundle)) {
return transaction;
}

Bundle bundle = (Bundle) transaction;

if (bundle.getEntry() == null || bundle.getEntry().size() == 0) {
return transaction;
}

for (Bundle.BundleEntryComponent entry : bundle.getEntry()) {
if (entry != null && entry.hasResource()) {
try {
// Ensure each resource has an ID, or create a GUID for them
if (!entry.getResource().hasId()) {
entry.getResource().setId(java.util.UUID.randomUUID().toString());
}

this.update(entry.getResource());
} catch (Exception ex) {
logger.warn("Failed to process resource in transaction", ex);
}
}
}

return transaction;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.lantanagroup.link.measureeval.services;

import org.hl7.fhir.r4.model.Library;

public interface LibraryResolver {
Library resolve(String libraryId);
}
Loading

0 comments on commit ea52c8f

Please sign in to comment.