From f1a60c478a06499c0fd6d641c6d500878b6fd2e6 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Mon, 22 Jun 2026 18:32:56 +0200 Subject: [PATCH] fix: resolve OUG{} indicator count to zero when org unit has no group members (#24266) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ItemOrgUnitGroupCount.evaluate read visitor.getParams().getOrgUnitCountMap() unguarded. In the analytics indicator path (DataHandler.getIndicatorValue), the per-org-unit target count map is null when the org unit target aggregation returns no row for an org unit — i.e. when that org unit's subtree contains no members of the referenced group. This produced a NullPointerException (HTTP 500) on analytics requests for any indicator using OUG{} over such an org unit, e.g. GET /api/analytics.json?dimension=dx:&dimension=ou:LEVEL-3&dimension=pe:2025 A missing entry means a count of zero, not an error. Treat both a null map and a missing group entry as zero, consistent with how analytics treats absent counts. This also removes the prior "Cannot find count" throw, which likewise bubbled up as a 500 since getIndicatorValueObject does not catch it. Pre-existing latent bug (the unguarded line dates to 2023); rare because it requires an OUG{} indicator and an org unit whose subtree has zero matching members. Co-authored-by: Claude Opus 4.8 (1M context) --- .../dataitem/ItemOrgUnitGroupCount.java | 14 +++++------ .../expression/ExpressionService2Test.java | 25 +++++++++++++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/dataitem/ItemOrgUnitGroupCount.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/dataitem/ItemOrgUnitGroupCount.java index 7b66e49a9726..52d97dc66077 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/dataitem/ItemOrgUnitGroupCount.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/dataitem/ItemOrgUnitGroupCount.java @@ -30,6 +30,7 @@ import static org.hisp.dhis.parser.expression.ParserUtils.DOUBLE_VALUE_IF_NULL; import static org.hisp.dhis.parser.expression.antlr.ExpressionParser.ExprContext; +import java.util.Map; import org.hisp.dhis.antlr.ParserExceptionWithoutContext; import org.hisp.dhis.organisationunit.OrganisationUnitGroup; import org.hisp.dhis.parser.expression.CommonExpressionVisitor; @@ -65,14 +66,13 @@ public Object getExpressionInfo(ExprContext ctx, CommonExpressionVisitor visitor @Override public Object evaluate(ExprContext ctx, CommonExpressionVisitor visitor) { - Integer count = visitor.getParams().getOrgUnitCountMap().get(ctx.uid0.getText()); + Map orgUnitCountMap = visitor.getParams().getOrgUnitCountMap(); - if (count == null) // Shouldn't happen for a valid expression. - { - throw new ParserExceptionWithoutContext( - "Can't find count for organisation unit " + ctx.uid0.getText()); - } + // The count map is absent for an organisation unit whose subtree contains no members of the + // group (the analytics org unit target aggregation returns no row for it), so a missing entry + // means a count of zero rather than an error. + Integer count = orgUnitCountMap != null ? orgUnitCountMap.get(ctx.uid0.getText()) : null; - return count.doubleValue(); + return count != null ? count.doubleValue() : 0d; } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/expression/ExpressionService2Test.java b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/expression/ExpressionService2Test.java index afb04995662d..4640467ccab8 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/expression/ExpressionService2Test.java +++ b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/expression/ExpressionService2Test.java @@ -926,6 +926,31 @@ void testGetExpressionValue() { assertEquals(54d, exprValue(expressionR, itemMap, valueMap, orgUnitCountMap, null), DELTA); } + @Test + void testGetExpressionValueOrgUnitGroupCountMissingMemberCountIsZero() { + // An OUG{} indicator queried for an org unit whose subtree has no members of the group: the + // analytics target map then has no entry for that org unit, so DataHandler passes a null (or + // entry-less) orgUnitCountMap down to expression evaluation. The org unit group count must + // resolve to 0 rather than throwing (previously a NullPointerException / "Cannot find count"). + mockConstantService(); + + Map itemMap = + ImmutableMap.builder() + .put(getId(opA), opA) + .build(); + + Map valueMap = new HashMap<>(); + valueMap.put(opA, 12d); + + // expressionH = #{deA.coc} * OUG{groupA}; with the group count resolving to 0 -> 12 * 0 = 0. + + // Null map (no target entry for this org unit at all). + assertEquals(0d, exprValue(expressionH, itemMap, valueMap, null, null), DELTA); + + // Non-null map missing this group's uid. + assertEquals(0d, exprValue(expressionH, itemMap, valueMap, new HashMap<>(), null), DELTA); + } + @Test void testGetIndicatorDimensionalItemMap2() { Set itemIds = Sets.newHashSet(getId(opA));