Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.

Commit f66e823

Browse files
authored
Merge pull request #21 from num-codex/allow-other-query-types
Allow other query types
2 parents 5c1065f + 1792d08 commit f66e823

36 files changed

Lines changed: 83652 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package de.netzwerk_universitaetsmedizin.codex.processes.feasibility;
2+
3+
import java.util.Arrays;
4+
import java.util.Objects;
5+
6+
/**
7+
* Determines which evaluation strategy to be used within the process.
8+
*/
9+
public enum EvaluationStrategy implements EvaluationStrategyProvider {
10+
11+
/**
12+
* Measures will be evaluated using CQL.
13+
* Requires a FHIR server capable of handling CQL queries.
14+
*/
15+
CQL("cql"),
16+
17+
/**
18+
* Measures will be evaluated using a structured query.
19+
*/
20+
STRUCTURED_QUERY("structured-query");
21+
22+
private final String strategyRepresentation;
23+
24+
EvaluationStrategy(String strategyRepresentation) {
25+
this.strategyRepresentation = Objects.requireNonNull(strategyRepresentation);
26+
}
27+
28+
/**
29+
* Gets the string representation of this evaluation strategy.
30+
*
31+
* @return The evaluation strategy's string representation.
32+
*/
33+
public String getStrategyRepresentation() {
34+
return strategyRepresentation;
35+
}
36+
37+
/**
38+
* Gets the appropriate evaluation strategy instance for a given representation.
39+
*
40+
* @param strategyRepresentation An evaluation strategy's string representation.
41+
* @return The appropriate evaluation strategy instance.
42+
* @throws IllegalArgumentException If the given strategy representation is not supported.
43+
*/
44+
public static EvaluationStrategy fromStrategyRepresentation(String strategyRepresentation) {
45+
return Arrays.stream(EvaluationStrategy.values())
46+
.filter(es -> es.strategyRepresentation.equalsIgnoreCase(strategyRepresentation))
47+
.findFirst()
48+
.orElseThrow(() -> new IllegalArgumentException("No known evaluation strategy with the representation: " + strategyRepresentation));
49+
}
50+
51+
@Override
52+
public EvaluationStrategy provideEvaluationStrategy() {
53+
return this;
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package de.netzwerk_universitaetsmedizin.codex.processes.feasibility;
2+
3+
public interface EvaluationStrategyProvider {
4+
/**
5+
* Provides an evaluation strategy.
6+
*
7+
* @return An evaluation strategy.
8+
*/
9+
EvaluationStrategy provideEvaluationStrategy();
10+
}

codex-process-feasibility/src/main/java/de/netzwerk_universitaetsmedizin/codex/processes/feasibility/FeasibilityProcessPluginDefinition.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import ca.uhn.fhir.context.FhirContext;
44
import de.netzwerk_universitaetsmedizin.codex.processes.feasibility.spring.config.EnhancedFhirWebserviceClientProviderConfig;
5+
import de.netzwerk_universitaetsmedizin.codex.processes.feasibility.spring.config.EvaluationConfig;
56
import de.netzwerk_universitaetsmedizin.codex.processes.feasibility.spring.config.FeasibilityConfig;
7+
import de.netzwerk_universitaetsmedizin.codex.processes.feasibility.spring.config.FlareWebserviceClientConfig;
68
import de.netzwerk_universitaetsmedizin.codex.processes.feasibility.spring.config.StoreConfig;
79
import org.highmed.dsf.bpe.ProcessPluginDefinition;
810
import org.highmed.dsf.fhir.resources.AbstractResource;
@@ -38,7 +40,8 @@ public Stream<String> getBpmnFiles() {
3840

3941
@Override
4042
public Stream<Class<?>> getSpringConfigClasses() {
41-
return Stream.of(StoreConfig.class, FeasibilityConfig.class, EnhancedFhirWebserviceClientProviderConfig.class);
43+
return Stream.of(StoreConfig.class, FeasibilityConfig.class, EnhancedFhirWebserviceClientProviderConfig.class,
44+
EvaluationConfig.class, FlareWebserviceClientConfig.class);
4245
}
4346

4447
@Override
@@ -51,6 +54,13 @@ public ResourceProvider getResourceProvider(FhirContext fhirContext, ClassLoader
5154
var sExtDic = StructureDefinitionResource
5255
.file("fhir/StructureDefinition/codex-extension-dic.xml");
5356

57+
var sMeasure = StructureDefinitionResource
58+
.file("fhir/StructureDefinition/codex-measure.xml");
59+
var sMeasureReport = StructureDefinitionResource
60+
.file("fhir/StructureDefinition/codex-measure-report.xml");
61+
var sLibrary = StructureDefinitionResource
62+
.file("fhir/StructureDefinition/codex-library.xml");
63+
5464
var sTExe = StructureDefinitionResource
5565
.file("fhir/StructureDefinition/codex-task-execute-simple-feasibility.xml");
5666
var sTReq = StructureDefinitionResource
@@ -62,9 +72,9 @@ public ResourceProvider getResourceProvider(FhirContext fhirContext, ClassLoader
6272

6373
Map<String, List<AbstractResource>> resourcesByProcessKeyAndVersion = Map.of(
6474
"executeSimpleFeasibility/" + VERSION,
65-
Arrays.asList(aExe, sTExe, sTResS, vF, cF),
75+
Arrays.asList(aExe, sTExe, sTResS, vF, cF, sMeasure, sMeasureReport, sLibrary),
6676
"requestSimpleFeasibility/" + VERSION,
67-
Arrays.asList(aReq, sTReq, sExtDic, vF, cF));
77+
Arrays.asList(aReq, sTReq, sExtDic, vF, cF, sMeasure, sMeasureReport, sLibrary));
6878

6979
return ResourceProvider.read(VERSION,
7080
() -> fhirContext.newXmlParser().setStripVersionsFromReferences(false),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package de.netzwerk_universitaetsmedizin.codex.processes.feasibility;
2+
3+
import java.io.IOException;
4+
5+
/**
6+
* Describes a client for communicating with a Flare instance.
7+
*/
8+
public interface FlareWebserviceClient {
9+
10+
/**
11+
* Given a structured query sends this query to a Flare instance in order to request the corresponding
12+
* feasibility (population count).
13+
*
14+
* @param structuredQuery The query that shall be evaluated.
15+
* @return Feasibility (population count) corresponding to the evaluated query.
16+
* @throws IOException If an I/O error occurs when sending or receiving.
17+
* @throws InterruptedException If the operation is interrupted.
18+
*/
19+
int requestFeasibility(byte[] structuredQuery) throws IOException, InterruptedException;
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package de.netzwerk_universitaetsmedizin.codex.processes.feasibility;
2+
3+
import java.io.IOException;
4+
import java.net.URI;
5+
import java.net.http.HttpClient;
6+
import java.net.http.HttpRequest;
7+
8+
import static java.net.http.HttpRequest.BodyPublishers.ofByteArray;
9+
import static java.net.http.HttpResponse.BodyHandlers.ofString;
10+
11+
/**
12+
* Client for communicating with a Flare instance.
13+
*/
14+
public class FlareWebserviceClientImpl implements FlareWebserviceClient {
15+
16+
private final HttpClient httpClient;
17+
private final URI flareBaseUrl;
18+
19+
public FlareWebserviceClientImpl(HttpClient httpClient, URI flareBaseUrl) {
20+
this.httpClient = httpClient;
21+
this.flareBaseUrl = flareBaseUrl;
22+
}
23+
24+
@Override
25+
public int requestFeasibility(byte[] structuredQuery) throws IOException, InterruptedException {
26+
var req = HttpRequest.newBuilder()
27+
.POST(ofByteArray(structuredQuery))
28+
.setHeader("Content-Type", "codex/json")
29+
.setHeader("Accept", "internal/json")
30+
.uri(flareBaseUrl.resolve("query-sync"))
31+
.build();
32+
33+
var res = httpClient.send(req, ofString());
34+
return Integer.parseInt(res.body());
35+
}
36+
}

codex-process-feasibility/src/main/java/de/netzwerk_universitaetsmedizin/codex/processes/feasibility/service/EvaluateMeasure.java renamed to codex-process-feasibility/src/main/java/de/netzwerk_universitaetsmedizin/codex/processes/feasibility/service/EvaluateCqlMeasure.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@
1717
import static de.netzwerk_universitaetsmedizin.codex.processes.feasibility.variables.ConstantsFeasibility.VARIABLE_MEASURE_ID;
1818
import static de.netzwerk_universitaetsmedizin.codex.processes.feasibility.variables.ConstantsFeasibility.VARIABLE_MEASURE_REPORT;
1919

20-
public class EvaluateMeasure extends AbstractServiceDelegate implements InitializingBean {
20+
public class EvaluateCqlMeasure extends AbstractServiceDelegate implements InitializingBean {
2121

22-
private static final Logger logger = LoggerFactory.getLogger(EvaluateMeasure.class);
22+
private static final Logger logger = LoggerFactory.getLogger(EvaluateCqlMeasure.class);
2323

2424
private static final String CODE_SYSTEM_MEASURE_POPULATION = "http://terminology.hl7.org/CodeSystem/measure-population";
2525
private static final String CODE_INITIAL_POPULATION = "initial-population";
2626

2727
private final IGenericClient storeClient;
2828

29-
public EvaluateMeasure(FhirWebserviceClientProvider clientProvider, TaskHelper taskHelper,
30-
IGenericClient storeClient) {
29+
public EvaluateCqlMeasure(FhirWebserviceClientProvider clientProvider, TaskHelper taskHelper,
30+
IGenericClient storeClient) {
3131
super(clientProvider, taskHelper);
3232

3333
this.storeClient = storeClient;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package de.netzwerk_universitaetsmedizin.codex.processes.feasibility.service;
2+
3+
import de.netzwerk_universitaetsmedizin.codex.processes.feasibility.FlareWebserviceClient;
4+
import org.camunda.bpm.engine.delegate.DelegateExecution;
5+
import org.highmed.dsf.bpe.delegate.AbstractServiceDelegate;
6+
import org.highmed.dsf.fhir.client.FhirWebserviceClientProvider;
7+
import org.highmed.dsf.fhir.task.TaskHelper;
8+
import org.hl7.fhir.r4.model.CodeableConcept;
9+
import org.hl7.fhir.r4.model.Coding;
10+
import org.hl7.fhir.r4.model.Library;
11+
import org.hl7.fhir.r4.model.Measure;
12+
import org.hl7.fhir.r4.model.MeasureReport;
13+
import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupComponent;
14+
import org.hl7.fhir.r4.model.MeasureReport.MeasureReportGroupPopulationComponent;
15+
import org.hl7.fhir.r4.model.Period;
16+
import org.joda.time.LocalDate;
17+
import org.springframework.beans.factory.InitializingBean;
18+
19+
import java.io.IOException;
20+
import java.util.Date;
21+
import java.util.List;
22+
import java.util.Objects;
23+
24+
import static de.netzwerk_universitaetsmedizin.codex.processes.feasibility.variables.ConstantsFeasibility.CODESYSTEM_MEASURE_POPULATION;
25+
import static de.netzwerk_universitaetsmedizin.codex.processes.feasibility.variables.ConstantsFeasibility.CODESYSTEM_MEASURE_POPULATION_VALUE_INITIAL_POPULATION;
26+
import static de.netzwerk_universitaetsmedizin.codex.processes.feasibility.variables.ConstantsFeasibility.VARIABLE_LIBRARY;
27+
import static de.netzwerk_universitaetsmedizin.codex.processes.feasibility.variables.ConstantsFeasibility.VARIABLE_MEASURE;
28+
import static de.netzwerk_universitaetsmedizin.codex.processes.feasibility.variables.ConstantsFeasibility.VARIABLE_MEASURE_REPORT;
29+
import static org.hl7.fhir.r4.model.MeasureReport.MeasureReportStatus.COMPLETE;
30+
import static org.hl7.fhir.r4.model.MeasureReport.MeasureReportType.SUMMARY;
31+
32+
public class EvaluateStructuredQueryMeasure extends AbstractServiceDelegate implements InitializingBean {
33+
34+
private static final String STRUCTURED_QUERY_CONTENT_TYPE = "application/json";
35+
36+
private final FlareWebserviceClient flareClient;
37+
38+
public EvaluateStructuredQueryMeasure(FhirWebserviceClientProvider clientProvider, TaskHelper taskHelper,
39+
FlareWebserviceClient flareClient) {
40+
super(clientProvider, taskHelper);
41+
this.flareClient = flareClient;
42+
}
43+
44+
@Override
45+
public void afterPropertiesSet() throws Exception {
46+
super.afterPropertiesSet();
47+
Objects.requireNonNull(flareClient, "flareClient");
48+
}
49+
50+
@Override
51+
protected void doExecute(DelegateExecution execution) throws IOException, InterruptedException {
52+
var library = (Library) execution.getVariable(VARIABLE_LIBRARY);
53+
var measure = (Measure) execution.getVariable(VARIABLE_MEASURE);
54+
55+
var structuredQuery = getStructuredQuery(library);
56+
var feasibility = getFeasibility(structuredQuery);
57+
var measureReport = buildMeasureReport(measure.getUrl(), feasibility);
58+
59+
execution.setVariable(VARIABLE_MEASURE_REPORT, measureReport);
60+
}
61+
62+
private byte[] getStructuredQuery(Library library) {
63+
return library.getContent().stream()
64+
.filter(c -> c.getContentType().equalsIgnoreCase(STRUCTURED_QUERY_CONTENT_TYPE))
65+
.findFirst()
66+
.orElseThrow(() -> new IllegalStateException("query is missing content of type " + STRUCTURED_QUERY_CONTENT_TYPE))
67+
.getData();
68+
}
69+
70+
private int getFeasibility(byte[] structuredQuery) throws IOException, InterruptedException {
71+
return flareClient.requestFeasibility(structuredQuery);
72+
}
73+
74+
private MeasureReport buildMeasureReport(String measureRef, int feasibility) {
75+
var measureReport = new MeasureReport()
76+
.setStatus(COMPLETE)
77+
.setType(SUMMARY)
78+
.setDate(new Date())
79+
.setMeasure(measureRef)
80+
.setPeriod(new Period()
81+
.setStart(new LocalDate(1900, 1, 1).toDate())
82+
.setEnd(new LocalDate(2100, 1, 1).toDate()));
83+
84+
var populationGroup = new MeasureReportGroupPopulationComponent()
85+
.setCount(feasibility)
86+
.setCode(new CodeableConcept()
87+
.addCoding(new Coding()
88+
.setSystem(CODESYSTEM_MEASURE_POPULATION)
89+
.setCode(CODESYSTEM_MEASURE_POPULATION_VALUE_INITIAL_POPULATION)));
90+
91+
measureReport.getGroup()
92+
.add(new MeasureReportGroupComponent()
93+
.setPopulation(List.of(populationGroup)));
94+
95+
return measureReport;
96+
}
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package de.netzwerk_universitaetsmedizin.codex.processes.feasibility.service;
2+
3+
import de.netzwerk_universitaetsmedizin.codex.processes.feasibility.EvaluationStrategyProvider;
4+
import org.camunda.bpm.engine.delegate.DelegateExecution;
5+
import org.highmed.dsf.bpe.delegate.AbstractServiceDelegate;
6+
import org.highmed.dsf.fhir.client.FhirWebserviceClientProvider;
7+
import org.highmed.dsf.fhir.task.TaskHelper;
8+
import org.springframework.beans.factory.InitializingBean;
9+
10+
import java.util.Objects;
11+
12+
import static de.netzwerk_universitaetsmedizin.codex.processes.feasibility.variables.ConstantsFeasibility.VARIABLE_EVALUATION_STRATEGY;
13+
14+
public class PrepareEvaluationStrategy extends AbstractServiceDelegate implements InitializingBean {
15+
16+
private final EvaluationStrategyProvider evaluationStrategyProvider;
17+
18+
public PrepareEvaluationStrategy(FhirWebserviceClientProvider clientProvider, TaskHelper taskHelper,
19+
EvaluationStrategyProvider evaluationStrategyProvider) {
20+
super(clientProvider, taskHelper);
21+
this.evaluationStrategyProvider = evaluationStrategyProvider;
22+
}
23+
24+
@Override
25+
public void afterPropertiesSet() throws Exception {
26+
super.afterPropertiesSet();
27+
Objects.requireNonNull(evaluationStrategyProvider, "evaluationStrategyProvider");
28+
}
29+
30+
@Override
31+
protected void doExecute(DelegateExecution execution) {
32+
execution.setVariable(VARIABLE_EVALUATION_STRATEGY,
33+
evaluationStrategyProvider.provideEvaluationStrategy().getStrategyRepresentation().toLowerCase());
34+
}
35+
}

codex-process-feasibility/src/main/java/de/netzwerk_universitaetsmedizin/codex/processes/feasibility/service/StoreFeasibilityResources.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
import org.slf4j.LoggerFactory;
1515
import org.springframework.beans.factory.InitializingBean;
1616

17+
import java.util.List;
1718
import java.util.Objects;
1819

1920
public class StoreFeasibilityResources extends AbstractServiceDelegate implements InitializingBean {
2021

2122
private static final Logger logger = LoggerFactory.getLogger(StoreFeasibilityResources.class);
23+
private static final String CQL_QUERY_CONTENT_TYPE = "text/cql";
2224

2325
private final IGenericClient storeClient;
2426

@@ -41,11 +43,21 @@ protected void doExecute(DelegateExecution execution) {
4143
Measure measure = (Measure) execution.getVariable(ConstantsFeasibility.VARIABLE_MEASURE);
4244
Library library = (Library) execution.getVariable(ConstantsFeasibility.VARIABLE_LIBRARY);
4345

44-
Bundle transactionResponse = storeResources(measure, library);
46+
Bundle transactionResponse = storeResources(measure, stripNonCqlAttachments(library));
4547

4648
execution.setVariable(ConstantsFeasibility.VARIABLE_MEASURE_ID, extractMeasureId(transactionResponse));
4749
}
4850

51+
private Library stripNonCqlAttachments(Library library) {
52+
var cqlAttachment = library.getContent()
53+
.stream()
54+
.filter(a -> a.getContentType().equalsIgnoreCase(CQL_QUERY_CONTENT_TYPE))
55+
.findFirst()
56+
.orElseThrow(() -> new IllegalStateException("query is missing content of type " + CQL_QUERY_CONTENT_TYPE));
57+
58+
return library.setContent(List.of(cqlAttachment));
59+
}
60+
4961
private Bundle storeResources(Measure measure, Library library) {
5062
logger.info("Store Measure `{}` and Library `{}`", measure.getId(), library.getUrl());
5163

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package de.netzwerk_universitaetsmedizin.codex.processes.feasibility.spring.config;
2+
3+
import de.netzwerk_universitaetsmedizin.codex.processes.feasibility.EvaluationStrategy;
4+
import de.netzwerk_universitaetsmedizin.codex.processes.feasibility.EvaluationStrategyProvider;
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
9+
@Configuration
10+
public class EvaluationConfig {
11+
12+
@Value("${de.netzwerk_universitaetsmedizin.codex.processes.feasibility.evaluation.strategy:cql}")
13+
private String evaluationStrategy;
14+
15+
@Bean
16+
public EvaluationStrategyProvider evaluationStrategy() {
17+
return EvaluationStrategy.fromStrategyRepresentation(evaluationStrategy);
18+
}
19+
}

0 commit comments

Comments
 (0)