From a2a199373f4967e91c1b6dd51ba2cdf05f3c41fa Mon Sep 17 00:00:00 2001 From: "chris.vukin" Date: Thu, 26 Feb 2026 09:59:04 -0500 Subject: [PATCH 1/2] fix: replace CompositeParam with separate date params in populateDateSearchParams CompositeParam causes a ClassCastException when the HAPI JPA search engine attempts to cast each element to DateParam during date-range query execution: java.lang.ClassCastException: class ca.uhn.fhir.rest.param.CompositeParam cannot be cast to class ca.uhn.fhir.rest.param.DateParam The Multimap already supports AND semantics via multiple put() calls on the same key, so wrapping in CompositeParam is unnecessary and incorrect. This replaces the single CompositeParam entry with two separate entries (one for >= start, one for <= end), which the JPA layer handles correctly. Affects all FHIR versions (DSTU3, R4, R5) since BaseRetrieveProvider is shared. Reproducer: 1. Enable clinical-reasoning on HAPI FHIR 8.x 2. POST an eCQM measure bundle (e.g. CMS125 Breast Cancer Screening) 3. GET [base]/Measure/BreastCancerScreeningFHIR/$evaluate-measure ?periodStart=2025-01-01&periodEnd=2025-12-31 4. Response returns status: error with CompositeParam ClassCastException in server logs After fix: status: complete with correct population counts. --- .../cqf/fhir/cql/engine/retrieve/BaseRetrieveProvider.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/engine/retrieve/BaseRetrieveProvider.java b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/engine/retrieve/BaseRetrieveProvider.java index cab74975b..beff8d92d 100644 --- a/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/engine/retrieve/BaseRetrieveProvider.java +++ b/cqf-fhir-cql/src/main/java/org/opencds/cqf/fhir/cql/engine/retrieve/BaseRetrieveProvider.java @@ -8,7 +8,6 @@ import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; -import ca.uhn.fhir.rest.param.CompositeParam; import ca.uhn.fhir.rest.param.DateParam; import ca.uhn.fhir.rest.param.InternalCodingDt; import ca.uhn.fhir.rest.param.ParamPrefixEnum; @@ -442,11 +441,13 @@ public void populateDateSearchParams( throw new InternalErrorException("resolved search parameter definition is null"); } - // a date range is a search && condition - so we'll use a composite + // a date range is a search AND condition — each put() on the Multimap + // adds a separate AND clause (one for >= start, one for <= end) DateParam gte = new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, start); DateParam lte = new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, end); - searchParams.put(sp.getName(), List.of(new CompositeParam<>(gte, lte))); + searchParams.put(sp.getName(), makeMutableSingleElementList(gte)); + searchParams.put(sp.getName(), makeMutableSingleElementList(lte)); } else if (StringUtils.isNotBlank(dateLowPath)) { List dateRangeParam = new ArrayList<>(); DateParam dateParam = new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, start); From 8148d48b3e4bff2dcbf1aa70b0af3b713ba7c73f Mon Sep 17 00:00:00 2001 From: "chris.vukin" Date: Mon, 2 Mar 2026 12:52:30 -0500 Subject: [PATCH 2/2] Handle DateParam on Period in ResourceMatcher --- .../fhir/utility/matcher/ResourceMatcher.java | 41 +++++++++++++++++++ .../utility/matcher/ResourceMatcherTest.java | 32 +++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcher.java b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcher.java index 6976fab84..b5df4d92d 100644 --- a/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcher.java +++ b/cqf-fhir-utility/src/main/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcher.java @@ -274,6 +274,47 @@ default boolean isMatchDate(DateParam param, IBase pathResult) { throw new UnsupportedOperationException( "Expected date, found " + pathResult.getClass().getSimpleName()); } + } else if (pathResult instanceof ICompositeType type) { + // For Period/Timing paths, a single DateParam represents one side of an overlap check. + // This allows repositories that AND repeated date params to evaluate each bound safely. + dateRange = getDateRange(type); + DateParam lowerBound = dateRange.getLowerBound(); + DateParam upperBound = dateRange.getUpperBound(); + + Date resourceStart = lowerBound == null ? null : lowerBound.getValue(); + Date resourceEnd = upperBound == null ? null : upperBound.getValue(); + + if (param.getValue() == null) { + return false; + } + + switch (param.getPrefix()) { + case GREATERTHAN: + case GREATERTHAN_OR_EQUALS: + return resourceEnd != null && isDateMatch(param, resourceEnd); + case LESSTHAN: + case LESSTHAN_OR_EQUALS: + return resourceStart != null && isDateMatch(param, resourceStart); + case EQUAL: + if (resourceStart == null || resourceEnd == null) { + return false; + } + + Date compareDate = param.getValue(); + return !compareDate.before(resourceStart) && !compareDate.after(resourceEnd); + case NOT_EQUAL: + if (resourceStart == null || resourceEnd == null) { + return true; + } + + Date notEqualCompare = param.getValue(); + return notEqualCompare.before(resourceStart) || notEqualCompare.after(resourceEnd); + default: + String msg = String.format( + "Unsupported DateTime comparison operation %s", + param.getPrefix().getValue()); + throw new UnsupportedOperationException(msg); + } } else { throw new UnsupportedOperationException( "Expected element of type date, dateTime, instant, Timing or Period, found " diff --git a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherTest.java b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherTest.java index 624d8ceac..fbdb48466 100644 --- a/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherTest.java +++ b/cqf-fhir-utility/src/test/java/org/opencds/cqf/fhir/utility/matcher/ResourceMatcherTest.java @@ -57,6 +57,38 @@ void before() { resourceMatcher = new ResourceMatcherR4(); } + @Test + void matches_locationPeriodWithSingleDateParam_doesNotThrowAndRespectsBounds() { + var encounter = new Encounter(); + encounter + .addLocation() + .setPeriod(new Period() + .setStart(createDate("2000-01-01 00:00:00")) + .setEnd(createDate("2000-12-31 23:59:59"))); + + assertTrue(resourceMatcher.matches( + "location-period", + List.of(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, createDate("2000-01-01"))), + encounter)); + assertTrue(resourceMatcher.matches( + "location-period", + List.of(new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, createDate("2000-12-31"))), + encounter)); + + assertEquals( + false, + resourceMatcher.matches( + "location-period", + List.of(new DateParam(ParamPrefixEnum.GREATERTHAN_OR_EQUALS, createDate("2001-01-01"))), + encounter)); + assertEquals( + false, + resourceMatcher.matches( + "location-period", + List.of(new DateParam(ParamPrefixEnum.LESSTHAN_OR_EQUALS, createDate("1999-12-31"))), + encounter)); + } + // NB: the list of parameters are always OR'd // internal compositeparams are always AND'd static List coverageParameters() {