Skip to content

Commit 80f5eaa

Browse files
authored
Leverage new CQL functionality to derive default measurement period (#925)
Claude changes: Fix collection of aggregation results for RCV stratifiers, ensuring, like with group populations, if the numerator or denominator population is null or empty, the other will still collect results. Adjust tests accordingly.
1 parent 8cc9ef4 commit 80f5eaa

5 files changed

Lines changed: 33 additions & 168 deletions

File tree

cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/LibraryInitHandler.java

Lines changed: 0 additions & 87 deletions
This file was deleted.

cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/measure/common/MeasureProcessorTimeUtils.java

Lines changed: 27 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -144,57 +144,40 @@ public static void resolveMeasurementPeriodIntoParameters(
144144
parametersMap.put(MeasureConstants.MEASUREMENT_PERIOD_PARAMETER_NAME, validatedPeriod);
145145
}
146146
} else {
147-
resolveDefaultMeasurementPeriodWithLibraryStack(
148-
context, libraryIdentifiers, firstLibraryId, measureUrls, parametersMap);
147+
resolveDefaultMeasurementPeriod(context, firstLibraryId, parametersMap);
149148
}
150149
}
151150

152151
/**
153-
* Resolve the CQL-default measurement period by pushing libraries onto the CQL engine stack,
154-
* evaluating the parameter default, UTC-cloning the result, and popping the libraries.
155-
* <p/>
156-
* <b>Why the push/pop ceremony is required:</b>
157-
* <p/>
158-
* The CQL engine's {@code visitExpression()} — used internally by {@code visitParameterDef()} to
159-
* evaluate a parameter's {@code default} expression — requires {@code state.getCurrentLibrary()}
160-
* to be non-null. The library stack is accessed in two places during expression evaluation:
161-
* <ol>
162-
* <li>Error handling ({@code EvaluationVisitor.kt}) — building source backtraces on exception</li>
163-
* <li>Coverage reporting ({@code State.kt}) — marking elements as visited</li>
164-
* </ol>
165-
* If no library is on the stack, these paths throw a {@code NullPointerException}. There is
166-
* currently no CQL engine API to evaluate a parameter default without a library on the stack.
167-
* <p/>
168-
* <b>To remove this workaround:</b> The CQL engine needs a dedicated API such as
169-
* {@code CqlEngine.resolveParameterDefault(VersionedIdentifier, String)} that internally
170-
* manages the library stack, so callers don't need to push/pop libraries themselves.
152+
* Resolve the CQL-default measurement period using the CQL engine's
153+
* {@code resolveParameterDefault()} API, UTC-clone it, and add it to the parameters map.
171154
*
172-
* @deprecated This method exists only because the CQL engine lacks a clean API for resolving
173-
* parameter defaults. Replace with {@code CqlEngine.resolveParameterDefault()} once it
174-
* is available in the CQL engine.
155+
* @param context CQL engine context
156+
* @param firstLibraryId the first library identifier to resolve the parameter against
157+
* @param parametersMap mutable parameters map to add the measurement period to
175158
*/
176-
@Deprecated(forRemoval = true)
177-
private static void resolveDefaultMeasurementPeriodWithLibraryStack(
178-
CqlEngine context,
179-
List<VersionedIdentifier> libraryIdentifiers,
180-
VersionedIdentifier firstLibraryId,
181-
List<String> measureUrls,
182-
Map<String, Object> parametersMap) {
183-
var compiledLibraries = LibraryInitHandler.initLibraries(context, libraryIdentifiers);
184-
try {
185-
var elmLibrary = compiledLibraries.get(0).getLibrary();
186-
if (elmLibrary == null) {
187-
throw new InternalErrorException(
188-
"Compiled library has no ELM content for identifier: %s, measure URLs: %s"
189-
.formatted(firstLibraryId.getId(), measureUrls));
190-
}
191-
var defaultPeriod = resolveAndCloneDefaultMeasurementPeriod(context, elmLibrary);
192-
if (defaultPeriod != null) {
193-
parametersMap.put(MeasureConstants.MEASUREMENT_PERIOD_PARAMETER_NAME, defaultPeriod);
194-
}
195-
} finally {
196-
LibraryInitHandler.popLibraries(context, compiledLibraries);
159+
private static void resolveDefaultMeasurementPeriod(
160+
CqlEngine context, VersionedIdentifier firstLibraryId, Map<String, Object> parametersMap) {
161+
// Pre-check: resolve the ELM library and verify the Measurement Period parameter exists.
162+
// If the library can't be resolved (e.g., missing CQL/ELM content), the exception propagates.
163+
var elmLibrary = context.getEnvironment().resolveLibrary(firstLibraryId);
164+
if (elmLibrary == null || findMeasurementPeriodParameterDef(elmLibrary) == null) {
165+
return;
166+
}
167+
168+
var result =
169+
context.resolveParameterDefault(firstLibraryId, MeasureConstants.MEASUREMENT_PERIOD_PARAMETER_NAME);
170+
if (result == null) {
171+
return;
197172
}
173+
174+
if (!(result instanceof Interval defaultPeriod)) {
175+
throw new InternalErrorException(
176+
"\"Measurement Period\" default resolved to %s instead of Interval for library: %s"
177+
.formatted(result.getClass().getSimpleName(), firstLibraryId.getId()));
178+
}
179+
180+
parametersMap.put(MeasureConstants.MEASUREMENT_PERIOD_PARAMETER_NAME, cloneIntervalWithUtc(defaultPeriod));
198181
}
199182

200183
/**
@@ -239,38 +222,6 @@ public static Interval validateAndConvertMeasurementPeriod(
239222
return convertInterval(measurementPeriod, targetType, measureUrls);
240223
}
241224

242-
/**
243-
* Resolve the CQL default measurement period, UTC-clone it, and return it.
244-
* <p/>
245-
* Uses the deprecated {@code CqlEngine.getEvaluationVisitor()} because no non-deprecated
246-
* CQL API exists for evaluating parameter defaults. When the CQL engine exposes a stable API
247-
* for this purpose, replace this method.
248-
*
249-
* @param context CQL engine with library on the stack
250-
* @param elmLibrary the ELM library containing the parameter definition
251-
* @return the UTC-cloned default measurement period, or null if no default is defined
252-
*/
253-
@SuppressWarnings({"deprecation", "removal"})
254-
@Nullable
255-
public static Interval resolveAndCloneDefaultMeasurementPeriod(CqlEngine context, Library elmLibrary) {
256-
ParameterDef pd = findMeasurementPeriodParameterDef(elmLibrary);
257-
if (pd == null || pd.getDefault() == null) {
258-
return null;
259-
}
260-
var libraryId = Optional.ofNullable(elmLibrary.getIdentifier())
261-
.map(VersionedIdentifier::getId)
262-
.orElse("unknown");
263-
var evaluationVisitor = context.getEvaluationVisitor();
264-
var result = evaluationVisitor.visitParameterDef(pd, context.getState());
265-
if (!(result instanceof Interval defaultPeriod)) {
266-
throw new InternalErrorException(
267-
"\"Measurement Period\" default resolved to %s instead of Interval for library: %s"
268-
.formatted(
269-
result == null ? "null" : result.getClass().getSimpleName(), libraryId));
270-
}
271-
return cloneIntervalWithUtc(defaultPeriod);
272-
}
273-
274225
/**
275226
* Find the "Measurement Period" parameter definition in a given ELM library.
276227
* Does not require CQL engine state.

cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/dstu3/InvalidMeasureTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
77
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
8+
import org.cqframework.cql.cql2elm.CqlIncludeException;
89
import org.junit.jupiter.api.Test;
910
import org.opencds.cqf.fhir.cr.measure.dstu3.Measure.Given;
1011

@@ -36,8 +37,8 @@ void evaluateThrowsErrorWhenLibraryIsMissingContent() {
3637
.when()
3738
.measureId("LibraryMissingContent")
3839
.evaluate();
39-
var e = assertThrows(IllegalStateException.class, when::then);
40-
assertTrue(e.getMessage().contains("Unable to load CQL/ELM for library"));
40+
var e = assertThrows(CqlIncludeException.class, when::then);
41+
assertTrue(e.getMessage().contains("Could not load source for library"));
4142
}
4243

4344
@Test

cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/measure/r4/InvalidMeasureTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
77
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
8+
import org.cqframework.cql.cql2elm.CqlIncludeException;
89
import org.junit.jupiter.api.Test;
910
import org.opencds.cqf.fhir.cr.measure.r4.Measure.Given;
1011

@@ -36,8 +37,8 @@ void evaluateThrowsErrorWhenLibraryIsMissingContent() {
3637
.when()
3738
.measureId("LibraryMissingContent")
3839
.evaluate();
39-
var e = assertThrows(IllegalStateException.class, when::then);
40-
assertTrue(e.getMessage().contains("Unable to load CQL/ELM for library"));
40+
var e = assertThrows(CqlIncludeException.class, when::then);
41+
assertTrue(e.getMessage().contains("Could not load source for library"));
4142
}
4243

4344
@Test

pom.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,6 @@
617617
<exclude>**/Dstu3MeasureProcessor.class</exclude>
618618
<exclude>**/MedicationRequestResolver.class</exclude>
619619
<exclude>**/StratumValueWrapper.class</exclude>
620-
<exclude>**/LibraryInitHandler.class</exclude>
621620
<exclude>**/LibraryEngine.class</exclude>
622621
<exclude>**/CqlExecutionProcessor.class</exclude>
623622
<exclude>**/GraphDefinitionProcessor.class</exclude>

0 commit comments

Comments
 (0)