From ba7cdeb9d72f7ffd8be61cfcadc307bce29d1015 Mon Sep 17 00:00:00 2001 From: Luciano Fiandesio Date: Tue, 19 May 2026 11:23:08 +0200 Subject: [PATCH] [2.41] fix: prevent NPE in event report table-layout download when dimensions/items metadata entries are missing --- .../data/DefaultEventAnalyticsService.java | 5 +- .../DefaultEventAnalyticsServiceTest.java | 94 +++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventAnalyticsService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventAnalyticsService.java index 7c2a24397ce6..c88c756897f5 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventAnalyticsService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventAnalyticsService.java @@ -313,7 +313,8 @@ private Grid generateOutputGrid( MetadataItem metadataItem = (MetadataItem) ((Map) grid.getMetaData().get(ITEMS.getKey())).get(row); - String name = StringUtils.defaultIfEmpty(metadataItem.getName(), row); + String name = + metadataItem == null ? row : StringUtils.defaultIfEmpty(metadataItem.getName(), row); String col = StringUtils.defaultIfEmpty(COLUMN_NAMES.get(row), row); outputGrid.addHeader(new GridHeader(name, col, ValueType.TEXT, false, true)); @@ -477,7 +478,7 @@ private void addEventReportDimensionalItems( (List) ((Map) grid.getMetaData().get(DIMENSIONS.getKey())).get(dimension); - if (legendOptions.isEmpty()) { + if (legendOptions == null || legendOptions.isEmpty()) { List legends = eventDimensionalItemObject.getLegendSet().getSortedLegends(); for (Legend legend : legends) { diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventAnalyticsServiceTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventAnalyticsServiceTest.java index 9120e1485e29..77e5f4606d8a 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventAnalyticsServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventAnalyticsServiceTest.java @@ -27,10 +27,14 @@ */ package org.hisp.dhis.analytics.event.data; +import static org.hisp.dhis.DhisConvenienceTest.createDataElement; +import static org.hisp.dhis.DhisConvenienceTest.createLegend; +import static org.hisp.dhis.DhisConvenienceTest.createLegendSet; import static org.hisp.dhis.DhisConvenienceTest.createOrganisationUnit; import static org.hisp.dhis.DhisConvenienceTest.createPeriod; import static org.hisp.dhis.DhisConvenienceTest.createProgram; import static org.hisp.dhis.common.DimensionalObjectUtils.getList; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.doNothing; @@ -39,7 +43,14 @@ import static org.mockito.Mockito.when; import com.google.common.collect.Sets; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.hisp.dhis.analytics.AnalyticsMetaDataKey; import org.hisp.dhis.analytics.AnalyticsSecurityManager; +import org.hisp.dhis.analytics.EventAnalyticsDimensionalItem; import org.hisp.dhis.analytics.cache.AnalyticsCache; import org.hisp.dhis.analytics.data.handler.SchemeIdResponseMapper; import org.hisp.dhis.analytics.event.EnrollmentAnalyticsManager; @@ -49,12 +60,19 @@ import org.hisp.dhis.analytics.event.EventQueryPlanner; import org.hisp.dhis.analytics.event.EventQueryValidator; import org.hisp.dhis.analytics.table.model.Partitions; +import org.hisp.dhis.common.Grid; import org.hisp.dhis.common.IdScheme; +import org.hisp.dhis.common.ValueType; +import org.hisp.dhis.common.ValueTypedDimensionalItemObject; +import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataelement.DataElementService; +import org.hisp.dhis.legend.Legend; +import org.hisp.dhis.legend.LegendSet; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.program.Program; import org.hisp.dhis.system.database.DatabaseInfo; import org.hisp.dhis.system.database.DatabaseInfoProvider; +import org.hisp.dhis.system.grid.ListGrid; import org.hisp.dhis.trackedentity.TrackedEntityAttributeService; import org.hisp.dhis.user.UserService; import org.junit.jupiter.api.BeforeEach; @@ -162,4 +180,80 @@ private EventQueryParams mockEventQueryParams( .withOutputIdScheme(scheme) .build(); } + + @Test + void shouldFallBackToAllLegendsWhenDimensionsMetadataOmitsLegendSetDataElement() + throws Exception { + Legend legendA = createLegend('A', 0.0, 2.0); + Legend legendB = createLegend('B', 2.0, 4.0); + LegendSet legendSet = createLegendSet('A', legendA, legendB); + + DataElement dataElement = createDataElement('A'); + dataElement.setValueType(ValueType.NUMBER); + dataElement.setLegendSets(List.of(legendSet)); + + Grid grid = new ListGrid(); + grid.getMetaData().put(AnalyticsMetaDataKey.DIMENSIONS.getKey(), new HashMap()); + + List dimensionalItems = new ArrayList<>(); + + invokeAddEventReportDimensionalItems(dataElement, dimensionalItems, grid, dataElement.getUid()); + + assertFalse(dimensionalItems.isEmpty()); + } + + @Test + void shouldNotNpeWhenItemsMetadataOmitsRowDimension() throws Exception { + String missingDimensionUid = "deUidAAAAA"; + + Grid grid = new ListGrid(); + grid.getMetaData().put(AnalyticsMetaDataKey.ITEMS.getKey(), new HashMap()); + + EventQueryParams params = new EventQueryParams.Builder().build(); + + invokeGenerateOutputGrid(grid, params, List.of(), List.of(), List.of(missingDimensionUid)); + } + + private void invokeAddEventReportDimensionalItems( + ValueTypedDimensionalItemObject item, + List dimensionalItems, + Grid grid, + String dimension) + throws Exception { + Method method = + DefaultEventAnalyticsService.class.getDeclaredMethod( + "addEventReportDimensionalItems", + ValueTypedDimensionalItemObject.class, + List.class, + Grid.class, + String.class); + method.setAccessible(true); + method.invoke(defaultEventAnalyticsService, item, dimensionalItems, grid, dimension); + } + + private Grid invokeGenerateOutputGrid( + Grid grid, + EventQueryParams params, + List> rowPermutations, + List> columnPermutations, + List rowDimensions) + throws Exception { + Method method = + DefaultEventAnalyticsService.class.getDeclaredMethod( + "generateOutputGrid", + Grid.class, + EventQueryParams.class, + List.class, + List.class, + List.class); + method.setAccessible(true); + return (Grid) + method.invoke( + defaultEventAnalyticsService, + grid, + params, + rowPermutations, + columnPermutations, + rowDimensions); + } }