diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/handler/DataHandler.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/handler/DataHandler.java index 91973503f301..a8a6eb5c7f19 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/handler/DataHandler.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/handler/DataHandler.java @@ -129,8 +129,8 @@ import org.hisp.dhis.analytics.QueryPlannerParams; import org.hisp.dhis.analytics.RawAnalyticsManager; import org.hisp.dhis.analytics.analyze.ExecutionPlanStore; -import org.hisp.dhis.analytics.event.EventAnalyticsService; import org.hisp.dhis.analytics.event.EventQueryParams; +import org.hisp.dhis.analytics.event.data.EventAggregateService; import org.hisp.dhis.analytics.resolver.ExpressionResolver; import org.hisp.dhis.analytics.resolver.ExpressionResolvers; import org.hisp.dhis.analytics.util.PeriodOffsetUtils; @@ -171,7 +171,7 @@ public class DataHandler { private static final int PERCENT = 100; - private final EventAnalyticsService eventAnalyticsService; + private final EventAggregateService eventAggregatedService; private final RawAnalyticsManager rawAnalyticsManager; @@ -428,7 +428,7 @@ public void addProgramDataElementAttributeIndicatorValues(DataQueryParams params .withSkipMeta(true) .build(); - Grid eventGrid = eventAnalyticsService.getAggregatedEventData(eventQueryParams); + Grid eventGrid = eventAggregatedService.getAggregatedData(eventQueryParams); grid.addRows(eventGrid); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/EnrollmentAnalyticsService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/EnrollmentAnalyticsService.java deleted file mode 100644 index 0190f1eb97be..000000000000 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/EnrollmentAnalyticsService.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2004-2022, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package org.hisp.dhis.analytics.event; - -import org.hisp.dhis.common.Grid; - -/** - * This interface is responsible for retrieving aggregated event data. Data will be returned in a - * grid object or as a dimensional key-value mapping. - * - * @author Markus Bekken - */ -public interface EnrollmentAnalyticsService { - - /** - * Returns a list of enrollments matching the given query. - * - * @param params the envent query parameters. - * @return enrollments with event data as a Grid object. - */ - Grid getEnrollments(EventQueryParams params); -} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/EventAnalyticsService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/EventAnalyticsService.java deleted file mode 100644 index 3f4794718a15..000000000000 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/EventAnalyticsService.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2004-2022, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package org.hisp.dhis.analytics.event; - -import java.util.List; -import org.hisp.dhis.analytics.AnalyticsMetaDataKey; -import org.hisp.dhis.analytics.Rectangle; -import org.hisp.dhis.common.AnalyticalObject; -import org.hisp.dhis.common.Grid; - -/** - * This interface is responsible for retrieving aggregated event data. Data will be returned in a - * grid object or as a dimensional key-value mapping. - * - * @author Lars Helge Overland - */ -public interface EventAnalyticsService { - - /** - * Generates aggregated event data for the given query. - * - * @param params the event query parameters. - * @return aggregated event data as a Grid object. - */ - Grid getAggregatedEventData(EventQueryParams params); - - /** - * Generates an aggregated value grid for the given query. The grid will represent a table with - * dimensions used as columns and rows as specified in columns and rows dimension arguments. If - * columns and rows are null or empty, the normalized table will be returned. - * - *

If meta data is included in the query, the meta data map of the grid will contain keys - * described in {@link AnalyticsMetaDataKey}. - * - * @param params the event query parameters. - * @param columns the identifiers of the dimensions to use as columns. - * @param rows the identifiers of the dimensions to use as rows. - * @return aggregated data as a Grid object. - */ - Grid getAggregatedEventData(EventQueryParams params, List columns, List rows) - throws Exception; - - /** - * Generates aggregated event data for the given analytical object. - * - * @param params the event query parameters. - * @return aggregated event data as a Grid object. - */ - Grid getAggregatedEventData(AnalyticalObject params); - - /** - * Returns a list of events matching the given query. - * - * @param params the event query parameters. - * @return events as a Grid object. - */ - Grid getEvents(EventQueryParams params); - - /** - * Returns a list of event clusters matching the given query. - * - * @param params the event query parameters. - * @return event clusters as a Grid object. - */ - Grid getEventClusters(EventQueryParams params); - - /** - * Returns a Rectangle with information about event count and extent of the spatial rectangle for - * the given query. - * - * @param params the event query parameters. - * @return event clusters as a Grid object. - */ - Rectangle getRectangle(EventQueryParams params); -} 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 deleted file mode 100644 index 0f1588971b61..000000000000 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventAnalyticsService.java +++ /dev/null @@ -1,824 +0,0 @@ -/* - * Copyright (c) 2004-2022, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package org.hisp.dhis.analytics.event.data; - -import static com.google.common.base.Preconditions.checkNotNull; -import static org.hisp.dhis.analytics.AnalyticsMetaDataKey.DIMENSIONS; -import static org.hisp.dhis.analytics.AnalyticsMetaDataKey.ITEMS; -import static org.hisp.dhis.analytics.DataQueryParams.DENOMINATOR_HEADER_NAME; -import static org.hisp.dhis.analytics.DataQueryParams.DENOMINATOR_ID; -import static org.hisp.dhis.analytics.DataQueryParams.DIVISOR_HEADER_NAME; -import static org.hisp.dhis.analytics.DataQueryParams.DIVISOR_ID; -import static org.hisp.dhis.analytics.DataQueryParams.FACTOR_HEADER_NAME; -import static org.hisp.dhis.analytics.DataQueryParams.FACTOR_ID; -import static org.hisp.dhis.analytics.DataQueryParams.MULTIPLIER_HEADER_NAME; -import static org.hisp.dhis.analytics.DataQueryParams.MULTIPLIER_ID; -import static org.hisp.dhis.analytics.DataQueryParams.NUMERATOR_HEADER_NAME; -import static org.hisp.dhis.analytics.DataQueryParams.NUMERATOR_ID; -import static org.hisp.dhis.analytics.DataQueryParams.VALUE_HEADER_NAME; -import static org.hisp.dhis.analytics.DataQueryParams.VALUE_ID; -import static org.hisp.dhis.analytics.common.ColumnHeader.CREATED_BY_DISPLAY_NAME; -import static org.hisp.dhis.analytics.common.ColumnHeader.ENROLLMENT_DATE; -import static org.hisp.dhis.analytics.common.ColumnHeader.EVENT; -import static org.hisp.dhis.analytics.common.ColumnHeader.EVENT_DATE; -import static org.hisp.dhis.analytics.common.ColumnHeader.EVENT_STATUS; -import static org.hisp.dhis.analytics.common.ColumnHeader.GEOMETRY; -import static org.hisp.dhis.analytics.common.ColumnHeader.INCIDENT_DATE; -import static org.hisp.dhis.analytics.common.ColumnHeader.LAST_UPDATED; -import static org.hisp.dhis.analytics.common.ColumnHeader.LAST_UPDATED_BY_DISPLAY_NAME; -import static org.hisp.dhis.analytics.common.ColumnHeader.LATITUDE; -import static org.hisp.dhis.analytics.common.ColumnHeader.LONGITUDE; -import static org.hisp.dhis.analytics.common.ColumnHeader.ORG_UNIT_CODE; -import static org.hisp.dhis.analytics.common.ColumnHeader.ORG_UNIT_NAME; -import static org.hisp.dhis.analytics.common.ColumnHeader.ORG_UNIT_NAME_HIERARCHY; -import static org.hisp.dhis.analytics.common.ColumnHeader.PROGRAM_INSTANCE; -import static org.hisp.dhis.analytics.common.ColumnHeader.PROGRAM_STAGE; -import static org.hisp.dhis.analytics.common.ColumnHeader.PROGRAM_STATUS; -import static org.hisp.dhis.analytics.common.ColumnHeader.SCHEDULED_DATE; -import static org.hisp.dhis.analytics.common.ColumnHeader.STORED_BY; -import static org.hisp.dhis.analytics.common.ColumnHeader.TRACKED_ENTITY; -import static org.hisp.dhis.analytics.event.LabelMapper.getEnrollmentDateLabel; -import static org.hisp.dhis.analytics.event.LabelMapper.getEventDateLabel; -import static org.hisp.dhis.analytics.event.LabelMapper.getEventLabel; -import static org.hisp.dhis.analytics.event.LabelMapper.getIncidentDateLabel; -import static org.hisp.dhis.analytics.event.LabelMapper.getOrgUnitLabel; -import static org.hisp.dhis.analytics.event.LabelMapper.getProgramStageLabel; -import static org.hisp.dhis.analytics.event.LabelMapper.getScheduledDateLabel; -import static org.hisp.dhis.analytics.util.AnalyticsUtils.throwIllegalQueryEx; -import static org.hisp.dhis.common.DimensionalObject.CATEGORYOPTIONCOMBO_DIM_ID; -import static org.hisp.dhis.common.DimensionalObject.DATA_X_DIM_ID; -import static org.hisp.dhis.common.DimensionalObject.ORGUNIT_DIM_ID; -import static org.hisp.dhis.common.DimensionalObject.PERIOD_DIM_ID; -import static org.hisp.dhis.common.ValueType.BOOLEAN; -import static org.hisp.dhis.common.ValueType.DATETIME; -import static org.hisp.dhis.common.ValueType.NUMBER; -import static org.hisp.dhis.common.ValueType.TEXT; - -import com.google.common.base.Preconditions; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import org.apache.commons.lang3.StringUtils; -import org.hisp.dhis.analytics.AnalyticsSecurityManager; -import org.hisp.dhis.analytics.DataQueryParams; -import org.hisp.dhis.analytics.EventAnalyticsDimensionalItem; -import org.hisp.dhis.analytics.Rectangle; -import org.hisp.dhis.analytics.cache.AnalyticsCache; -import org.hisp.dhis.analytics.common.ColumnHeader; -import org.hisp.dhis.analytics.common.scheme.SchemeInfo; -import org.hisp.dhis.analytics.data.handler.SchemeIdResponseMapper; -import org.hisp.dhis.analytics.event.EnrollmentAnalyticsManager; -import org.hisp.dhis.analytics.event.EventAnalyticsManager; -import org.hisp.dhis.analytics.event.EventAnalyticsService; -import org.hisp.dhis.analytics.event.EventAnalyticsUtils; -import org.hisp.dhis.analytics.event.EventDataQueryService; -import org.hisp.dhis.analytics.event.EventQueryParams; -import org.hisp.dhis.analytics.event.EventQueryPlanner; -import org.hisp.dhis.analytics.event.EventQueryValidator; -import org.hisp.dhis.analytics.util.AnalyticsUtils; -import org.hisp.dhis.common.AnalyticalObject; -import org.hisp.dhis.common.DimensionalObject; -import org.hisp.dhis.common.EventAnalyticalObject; -import org.hisp.dhis.common.Grid; -import org.hisp.dhis.common.GridHeader; -import org.hisp.dhis.common.IdentifiableObjectUtils; -import org.hisp.dhis.common.MetadataItem; -import org.hisp.dhis.common.QueryItem; -import org.hisp.dhis.common.ValueType; -import org.hisp.dhis.common.ValueTypedDimensionalItemObject; -import org.hisp.dhis.commons.collection.ListUtils; -import org.hisp.dhis.dataelement.DataElementService; -import org.hisp.dhis.feedback.ErrorCode; -import org.hisp.dhis.legend.Legend; -import org.hisp.dhis.option.Option; -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.hisp.dhis.util.Timer; -import org.springframework.stereotype.Service; - -/** - * @author Lars Helge Overland - */ -@Service("org.hisp.dhis.analytics.event.EventAnalyticsService") -public class DefaultEventAnalyticsService extends AbstractAnalyticsService - implements EventAnalyticsService { - private static final String DASH_PRETTY_SEPARATOR = " - "; - - private static final String SPACE = " "; - - private static final String TOTAL_COLUMN_PRETTY_NAME = "Total"; - - private static final Map COLUMN_NAMES = - Map.of( - DATA_X_DIM_ID, "data", - CATEGORYOPTIONCOMBO_DIM_ID, "categoryoptioncombo", - PERIOD_DIM_ID, "period", - ORGUNIT_DIM_ID, "organisationunit"); - - private static final Option OPT_TRUE = new Option("Yes", "1"); - - private static final Option OPT_FALSE = new Option("No", "0"); - - private final DataElementService dataElementService; - - private final TrackedEntityAttributeService trackedEntityAttributeService; - - private final EventAnalyticsManager eventAnalyticsManager; - - private final EnrollmentAnalyticsManager enrollmentAnalyticsManager; - - private final EventDataQueryService eventDataQueryService; - - private final EventQueryPlanner queryPlanner; - - private final boolean spatialSupport; - - private final AnalyticsCache analyticsCache; - - public DefaultEventAnalyticsService( - DataElementService dataElementService, - TrackedEntityAttributeService trackedEntityAttributeService, - EventAnalyticsManager eventAnalyticsManager, - EventDataQueryService eventDataQueryService, - AnalyticsSecurityManager securityManager, - EventQueryPlanner queryPlanner, - EventQueryValidator queryValidator, - DatabaseInfoProvider databaseInfoProvider, - AnalyticsCache analyticsCache, - EnrollmentAnalyticsManager enrollmentAnalyticsManager, - SchemeIdResponseMapper schemeIdResponseMapper, - UserService userService) { - super(securityManager, queryValidator, schemeIdResponseMapper, userService); - - checkNotNull(dataElementService); - checkNotNull(trackedEntityAttributeService); - checkNotNull(eventAnalyticsManager); - checkNotNull(eventDataQueryService); - checkNotNull(queryPlanner); - checkNotNull(databaseInfoProvider); - checkNotNull(analyticsCache); - checkNotNull(schemeIdResponseMapper); - - this.dataElementService = dataElementService; - this.trackedEntityAttributeService = trackedEntityAttributeService; - this.eventAnalyticsManager = eventAnalyticsManager; - this.eventDataQueryService = eventDataQueryService; - this.queryPlanner = queryPlanner; - this.spatialSupport = databaseInfoProvider.getDatabaseInfo().isSpatialSupport(); - this.analyticsCache = analyticsCache; - this.enrollmentAnalyticsManager = enrollmentAnalyticsManager; - } - - // ------------------------------------------------------------------------- - // EventAnalyticsService implementation - // ------------------------------------------------------------------------- - - // TODO Use [longitude/latitude] format for event points - // TODO Order event analytics tables on execution date to avoid default sort - // TODO Sorting in queries - - @Override - public Grid getAggregatedEventData( - EventQueryParams params, List columns, List rows) { - return AnalyticsUtils.isTableLayout(columns, rows) - ? getAggregatedEventDataTableLayout(params, columns, rows) - : getAggregatedEventData(params); - } - - @Override - public Grid getAggregatedEventData(AnalyticalObject object) { - EventQueryParams params = - eventDataQueryService.getFromAnalyticalObject((EventAnalyticalObject) object); - - return getAggregatedEventData(params); - } - - @Override - public Grid getAggregatedEventData(EventQueryParams params) { - securityManager.decideAccessEventQuery(params); - params = securityManager.withUserConstraints(params); - - queryValidator.validate(params); - - if (analyticsCache.isEnabled() && !params.analyzeOnly()) { - EventQueryParams immutableParams = new EventQueryParams.Builder(params).build(); - return analyticsCache.getOrFetch(params, p -> getAggregatedEventDataGrid(immutableParams)); - } - - return getAggregatedEventDataGrid(params); - } - - /** - * Creates a grid with table layout for downloading event reports. The grid is dynamically made - * from rows and columns input, which refers to the dimensions requested. - * - *

For event reports each option for a dimension will be an {@link - * EventAnalyticsDimensionalItem} and all permutations will be added to the grid. - * - * @param params the {@link EventQueryParams}. - * @param columns the identifiers of the dimensions to use as columns. - * @param rows the identifiers of the dimensions to use as rows. - * @return aggregated data as a Grid object. - */ - private Grid getAggregatedEventDataTableLayout( - EventQueryParams params, List columns, List rows) { - params.removeProgramIndicatorItems(); - - Grid grid = getAggregatedEventData(params); - - ListUtils.removeEmptys(columns); - ListUtils.removeEmptys(rows); - - Map> tableColumns = new LinkedHashMap<>(); - - if (columns != null) { - for (String dimension : columns) { - addEventDataObjects(grid, params, tableColumns, dimension); - } - } - - Map> tableRows = new LinkedHashMap<>(); - List rowDimensions = new ArrayList<>(); - - if (rows != null) { - for (String dimension : rows) { - rowDimensions.add(dimension); - addEventDataObjects(grid, params, tableRows, dimension); - } - } - - List> rowPermutations = - EventAnalyticsUtils.generateEventDataPermutations(tableRows); - - List> columnPermutations = - EventAnalyticsUtils.generateEventDataPermutations(tableColumns); - - return generateOutputGrid(grid, params, rowPermutations, columnPermutations, rowDimensions); - } - - /** - * Generates an output grid for event analytics download based on input parameters. - * - * @param grid the result grid. - * @param params the {@link EventQueryParams}. - * @param rowPermutations the row permutations - * @param columnPermutations the column permutations. - * @param rowDimensions the row dimensions. - * @return grid with table layout. - */ - @SuppressWarnings("unchecked") - private Grid generateOutputGrid( - Grid grid, - EventQueryParams params, - List> rowPermutations, - List> columnPermutations, - List rowDimensions) { - Grid outputGrid = new ListGrid(); - outputGrid.setTitle(IdentifiableObjectUtils.join(params.getFilterItems())); - - for (String row : rowDimensions) { - MetadataItem metadataItem = - (MetadataItem) ((Map) grid.getMetaData().get(ITEMS.getKey())).get(row); - - String name = StringUtils.defaultIfEmpty(metadataItem.getName(), row); - String col = StringUtils.defaultIfEmpty(COLUMN_NAMES.get(row), row); - - outputGrid.addHeader(new GridHeader(name, col, ValueType.TEXT, false, true)); - } - - columnPermutations.forEach( - permutation -> { - StringBuilder builder = new StringBuilder(); - - permutation.forEach( - (key, value) -> { - if (!key.equals(ORGUNIT_DIM_ID) && !key.equals(PERIOD_DIM_ID)) { - builder.append(key).append(SPACE); - } - builder - .append(value.getDisplayProperty(params.getDisplayProperty())) - .append(DASH_PRETTY_SEPARATOR); - }); - - String display = - builder.length() > 0 - ? builder.substring(0, builder.lastIndexOf(DASH_PRETTY_SEPARATOR)) - : TOTAL_COLUMN_PRETTY_NAME; - - outputGrid.addHeader(new GridHeader(display, display, ValueType.NUMBER, false, false)); - }); - - for (Map rowCombination : rowPermutations) { - outputGrid.addRow(); - List> ids = new ArrayList<>(); - Map displayObjects = new HashMap<>(); - - boolean fillDisplayList = true; - - for (Map columnCombination : columnPermutations) { - List idList = new ArrayList<>(); - - boolean finalFillDisplayList = fillDisplayList; - rowCombination.forEach( - (key, value) -> { - idList.add(value.toString()); - - if (finalFillDisplayList) { - displayObjects.put(value.getParentUid(), value); - } - }); - - columnCombination.forEach((key, value) -> idList.add(value.toString())); - - ids.add(idList); - fillDisplayList = false; - } - - addValuesInOutputGrid(rowDimensions, outputGrid, displayObjects, params); - - EventAnalyticsUtils.addValues(ids, grid, outputGrid); - } - - return getGridWithRows(grid, outputGrid); - } - - /** - * Returns a valid grid. - * - * @param grid the {@link Grid}. - * @param outputGrid the output {@link Grid}. - */ - private static Grid getGridWithRows(Grid grid, Grid outputGrid) { - return outputGrid.getRows().isEmpty() ? grid : outputGrid; - } - - /** - * Adds values to the given output grid. Display objects are not empty if columns and rows are not - * empty. - * - * @param rowDimensions the list of row dimensions. - * @param grid the {@link Grid}. - * @param displayObjects the map of display objects. - * @param params the {@link EventQueryParams}. - */ - private static void addValuesInOutputGrid( - List rowDimensions, - Grid grid, - Map displayObjects, - EventQueryParams params) { - if (!displayObjects.isEmpty()) { - rowDimensions.forEach( - dimension -> - grid.addValue( - displayObjects.get(dimension).getDisplayProperty(params.getDisplayProperty()))); - } - } - - /** - * Puts elements into the mapping table. The elements are fetched from the query parameters. - * - * @param grid the {@link Grid}. - * @param params the {@link EventQueryParams}. - * @param table the map to add elements to. - * @param dimension the dimension identifier. - */ - private void addEventDataObjects( - Grid grid, - EventQueryParams params, - Map> table, - String dimension) { - List objects = - params.getEventReportDimensionalItemArrayExploded(dimension); - - if (objects.isEmpty()) { - ValueTypedDimensionalItemObject eventDimensionalItemObject = - dataElementService.getDataElement(dimension); - - if (eventDimensionalItemObject == null) { - eventDimensionalItemObject = - trackedEntityAttributeService.getTrackedEntityAttribute(dimension); - } - - addEventReportDimensionalItems(eventDimensionalItemObject, objects, grid, dimension); - - table.put( - eventDimensionalItemObject.getDisplayProperty(params.getDisplayProperty()), objects); - } else { - table.put(dimension, objects); - } - } - - /** - * Adds dimensional items to the given list of objects. Send in a list of {@link - * EventAnalyticsDimensionalItem} and add properties from {@link ValueTypedDimensionalItemObject} - * parameter. - * - * @param eventDimensionalItemObject the {@link ValueTypedDimensionalItemObject} object to get - * properties from. - * @param objects the list of {@link EventAnalyticsDimensionalItem} objects. - * @param grid the {@link Grid} from the event analytics request. - * @param dimension the dimension identifier. - */ - @SuppressWarnings("unchecked") - private void addEventReportDimensionalItems( - ValueTypedDimensionalItemObject eventDimensionalItemObject, - List objects, - Grid grid, - String dimension) { - Preconditions.checkNotNull( - eventDimensionalItemObject, String.format("Data dimension '%s' is invalid", dimension)); - - String parentUid = eventDimensionalItemObject.getUid(); - - if (eventDimensionalItemObject.getValueType() == BOOLEAN) { - objects.add(new EventAnalyticsDimensionalItem(OPT_TRUE, parentUid)); - objects.add(new EventAnalyticsDimensionalItem(OPT_FALSE, parentUid)); - } - - if (eventDimensionalItemObject.hasOptionSet()) { - for (Option option : eventDimensionalItemObject.getOptionSet().getOptions()) { - objects.add(new EventAnalyticsDimensionalItem(option, parentUid)); - } - } else if (eventDimensionalItemObject.hasLegendSet()) { - List legendOptions = - (List) - ((Map) grid.getMetaData().get(DIMENSIONS.getKey())).get(dimension); - - if (legendOptions.isEmpty()) { - List legends = eventDimensionalItemObject.getLegendSet().getSortedLegends(); - - for (Legend legend : legends) { - for (int i = legend.getStartValue().intValue(); - i < legend.getEndValue().intValue(); - i++) { - objects.add( - new EventAnalyticsDimensionalItem( - new Option(String.valueOf(i), String.valueOf(i)), parentUid)); - } - } - } else { - for (String legend : legendOptions) { - MetadataItem metadataItem = - (MetadataItem) - ((Map) grid.getMetaData().get(ITEMS.getKey())).get(legend); - - objects.add( - new EventAnalyticsDimensionalItem( - new Option(metadataItem.getName(), legend), parentUid)); - } - } - } - } - - private Grid getAggregatedEventDataGrid(EventQueryParams params) { - params.removeProgramIndicatorItems(); - - Grid grid = new ListGrid(); - - int maxLimit = queryValidator.getMaxLimit(); - - // --------------------------------------------------------------------- - // Headers and data - // --------------------------------------------------------------------- - - if (!params.isSkipData() || params.analyzeOnly()) { - // ----------------------------------------------------------------- - // Headers - // ----------------------------------------------------------------- - - if (params.isCollapseDataDimensions() || params.isAggregateData()) { - grid.addHeader( - new GridHeader( - DimensionalObject.DATA_COLLAPSED_DIM_ID, - DataQueryParams.DISPLAY_NAME_DATA_X, - ValueType.TEXT, - false, - true)); - } else { - for (QueryItem item : params.getItems()) { - String displayProperty = item.getItem().getDisplayProperty(params.getDisplayProperty()); - - grid.addHeader( - new GridHeader( - item.getItem().getUid(), - displayProperty, - item.getValueType(), - false, - true, - item.getOptionSet(), - item.getLegendSet())); - } - } - - for (DimensionalObject dimension : params.getDimensions()) { - String displayProperty = dimension.getDisplayProperty(params.getDisplayProperty()); - - grid.addHeader( - new GridHeader(dimension.getDimension(), displayProperty, TEXT, false, true)); - } - - grid.addHeader(new GridHeader(VALUE_ID, VALUE_HEADER_NAME, NUMBER, false, false)); - - if (params.isIncludeNumDen()) { - grid.addHeader(new GridHeader(NUMERATOR_ID, NUMERATOR_HEADER_NAME, NUMBER, false, false)) - .addHeader( - new GridHeader(DENOMINATOR_ID, DENOMINATOR_HEADER_NAME, NUMBER, false, false)) - .addHeader(new GridHeader(FACTOR_ID, FACTOR_HEADER_NAME, NUMBER, false, false)) - .addHeader(new GridHeader(MULTIPLIER_ID, MULTIPLIER_HEADER_NAME, NUMBER, false, false)) - .addHeader(new GridHeader(DIVISOR_ID, DIVISOR_HEADER_NAME, NUMBER, false, false)); - } - - // ----------------------------------------------------------------- - // Data - // ----------------------------------------------------------------- - - Timer timer = new Timer().start().disablePrint(); - - List queries = queryPlanner.planAggregateQuery(params); - - timer.getSplitTime("Planned event query, got partitions: " + params.getPartitions()); - - for (EventQueryParams query : queries) { - // Query might be either an enrollment or event indicator - - if (query.hasEnrollmentProgramIndicatorDimension()) { - enrollmentAnalyticsManager.getAggregatedEventData(query, grid, maxLimit); - } else { - eventAnalyticsManager.getAggregatedEventData(query, grid, maxLimit); - } - } - - timer.getTime("Got aggregated events"); - - if (maxLimit > 0 && grid.getHeight() > maxLimit) { - throwIllegalQueryEx(ErrorCode.E7128, maxLimit); - } - - // ----------------------------------------------------------------- - // Limit and sort, done again due to potential multiple partitions - // ----------------------------------------------------------------- - - if (params.hasSortOrder() && grid.getHeight() > 0) { - grid.sortGrid(1, params.getSortOrderAsInt()); - } - - if (params.hasLimit() && grid.getHeight() > params.getLimit()) { - grid.limitGrid(params.getLimit()); - } - } - - if (!params.isSkipMeta()) { - SchemeInfo schemeInfo = new SchemeInfo(schemeSettings(params), schemeData(params)); - schemeIdResponseMapper.applyCustomIdScheme(schemeInfo, grid); - } - - // --------------------------------------------------------------------- - // Meta-ata - // --------------------------------------------------------------------- - - addMetadata(params, grid); - - return grid; - } - - // ------------------------------------------------------------------------- - // Query - // ------------------------------------------------------------------------- - - @Override - public Grid getEvents(EventQueryParams params) { - return getGrid(params); - } - - @Override - public Grid getEventClusters(EventQueryParams params) { - if (!spatialSupport) { - throwIllegalQueryEx(ErrorCode.E7218); - } - - params = - new EventQueryParams.Builder(params) - .withGeometryOnly(true) - .withStartEndDatesForPeriods() - .build(); - - securityManager.decideAccessEventQuery(params); - - queryValidator.validate(params); - - Grid grid = new ListGrid(); - - // --------------------------------------------------------------------- - // Headers - // --------------------------------------------------------------------- - - grid.addHeader( - new GridHeader( - ColumnHeader.COUNT.getItem(), ColumnHeader.COUNT.getName(), NUMBER, false, false)) - .addHeader( - new GridHeader( - ColumnHeader.CENTER.getItem(), ColumnHeader.CENTER.getName(), TEXT, false, false)) - .addHeader( - new GridHeader( - ColumnHeader.EXTENT.getItem(), ColumnHeader.EXTENT.getName(), TEXT, false, false)) - .addHeader( - new GridHeader( - ColumnHeader.POINTS.getItem(), ColumnHeader.POINTS.getName(), TEXT, false, false)); - - // --------------------------------------------------------------------- - // Data - // --------------------------------------------------------------------- - - params = queryPlanner.planEventQuery(params); - - eventAnalyticsManager.getEventClusters(params, grid, queryValidator.getMaxLimit()); - - return grid; - } - - @Override - public Rectangle getRectangle(EventQueryParams params) { - if (!spatialSupport) { - throwIllegalQueryEx(ErrorCode.E7218); - } - - params = - new EventQueryParams.Builder(params) - .withGeometryOnly(true) - .withStartEndDatesForPeriods() - .build(); - - securityManager.decideAccessEventQuery(params); - - queryValidator.validate(params); - - params = queryPlanner.planEventQuery(params); - - return eventAnalyticsManager.getRectangle(params); - } - - /** - * Creates a grid with headers. - * - * @param params the {@link EventQueryParams}. - */ - @Override - protected Grid createGridWithHeaders(EventQueryParams params) { - Grid grid = new ListGrid(); - - grid.addHeader( - new GridHeader( - EVENT.getItem(), - getEventLabel(params.getProgramStage(), EVENT.getName()), - TEXT, - false, - true)) - .addHeader( - new GridHeader( - PROGRAM_STAGE.getItem(), - getProgramStageLabel(params.getProgramStage(), PROGRAM_STAGE.getName()), - TEXT, - false, - true)) - .addHeader( - new GridHeader( - EVENT_DATE.getItem(), - getEventDateLabel(params.getProgramStage(), EVENT_DATE.getName()), - DATETIME, - false, - true)) - .addHeader(new GridHeader(STORED_BY.getItem(), STORED_BY.getName(), TEXT, false, true)) - .addHeader( - new GridHeader( - CREATED_BY_DISPLAY_NAME.getItem(), - CREATED_BY_DISPLAY_NAME.getName(), - TEXT, - false, - true)) - .addHeader( - new GridHeader( - LAST_UPDATED_BY_DISPLAY_NAME.getItem(), - LAST_UPDATED_BY_DISPLAY_NAME.getName(), - TEXT, - false, - true)) - .addHeader( - new GridHeader(LAST_UPDATED.getItem(), LAST_UPDATED.getName(), DATETIME, false, true)) - .addHeader( - new GridHeader( - SCHEDULED_DATE.getItem(), - getScheduledDateLabel(params.getProgramStage(), SCHEDULED_DATE.getName()), - DATETIME, - false, - true)); - - if (params.getProgram().isRegistration()) { - grid.addHeader( - new GridHeader( - ENROLLMENT_DATE.getItem(), - getEnrollmentDateLabel(params.getProgram(), ENROLLMENT_DATE.getName()), - DATETIME, - false, - true)) - .addHeader( - new GridHeader( - INCIDENT_DATE.getItem(), - getIncidentDateLabel(params.getProgram(), INCIDENT_DATE.getName()), - DATETIME, - false, - true)) - .addHeader( - new GridHeader(TRACKED_ENTITY.getItem(), TRACKED_ENTITY.getName(), TEXT, false, true)) - .addHeader( - new GridHeader( - PROGRAM_INSTANCE.getItem(), PROGRAM_INSTANCE.getName(), TEXT, false, true)); - } - - grid.addHeader(new GridHeader(GEOMETRY.getItem(), GEOMETRY.getName(), TEXT, false, true)) - .addHeader(new GridHeader(LONGITUDE.getItem(), LONGITUDE.getName(), NUMBER, false, true)) - .addHeader(new GridHeader(LATITUDE.getItem(), LATITUDE.getName(), NUMBER, false, true)) - .addHeader( - new GridHeader( - ORG_UNIT_NAME.getItem(), - getOrgUnitLabel(params.getProgram(), ORG_UNIT_NAME.getName()), - TEXT, - false, - true)) - .addHeader( - new GridHeader( - ORG_UNIT_NAME_HIERARCHY.getItem(), - ORG_UNIT_NAME_HIERARCHY.getName(), - TEXT, - false, - true)) - .addHeader( - new GridHeader(ORG_UNIT_CODE.getItem(), ORG_UNIT_CODE.getName(), TEXT, false, true)) - .addHeader( - new GridHeader(PROGRAM_STATUS.getItem(), PROGRAM_STATUS.getName(), TEXT, false, true)) - .addHeader( - new GridHeader(EVENT_STATUS.getItem(), EVENT_STATUS.getName(), TEXT, false, true)); - - return grid; - } - - /** - * Adds event data to the given grid. Returns the number of events matching the given event query. - * - * @param grid the {@link Grid}. - * @param params the {@link EventQueryParams}. - * @return the count of events. - */ - @Override - protected long addData(Grid grid, EventQueryParams params) { - Timer timer = new Timer().start().disablePrint(); - - params = queryPlanner.planEventQuery(params); - - timer.getSplitTime("Planned event query, got partitions: " + params.getPartitions()); - - long count = 0; - EventQueryParams immutableParams = new EventQueryParams.Builder(params).build(); - - if (params.getPartitions().hasAny() || params.isSkipPartitioning()) { - - eventAnalyticsManager.getEvents(immutableParams, grid, queryValidator.getMaxLimit()); - - if (params.isPaging() && params.isTotalPages()) { - count = eventAnalyticsManager.getEventCount(immutableParams); - } - - timer.getTime("Got events " + grid.getHeight()); - } - - return count; - } -} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EnrollmentAggregateService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EnrollmentAggregateService.java new file mode 100644 index 000000000000..cf08987e7753 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EnrollmentAggregateService.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.analytics.event.data; + +import static org.hisp.dhis.analytics.DataQueryParams.VALUE_HEADER_NAME; +import static org.hisp.dhis.analytics.DataQueryParams.VALUE_ID; +import static org.hisp.dhis.analytics.tracker.HeaderHelper.addCommonHeaders; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.UNLIMITED_PAGING; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.addPaging; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.applyHeaders; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.getDimensionsKeywords; +import static org.hisp.dhis.common.DimensionType.PERIOD; +import static org.hisp.dhis.common.ValueType.NUMBER; +import static org.hisp.dhis.commons.util.TextUtils.EMPTY; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.hisp.dhis.analytics.AnalyticsSecurityManager; +import org.hisp.dhis.analytics.event.EnrollmentAnalyticsManager; +import org.hisp.dhis.analytics.event.EventQueryParams; +import org.hisp.dhis.analytics.event.EventQueryPlanner; +import org.hisp.dhis.analytics.event.EventQueryValidator; +import org.hisp.dhis.analytics.tracker.MetadataItemsHandler; +import org.hisp.dhis.analytics.tracker.SchemeIdHandler; +import org.hisp.dhis.common.DimensionItemKeywords.Keyword; +import org.hisp.dhis.common.DimensionalObject; +import org.hisp.dhis.common.Grid; +import org.hisp.dhis.common.GridHeader; +import org.hisp.dhis.system.grid.ListGrid; +import org.hisp.dhis.util.Timer; +import org.springframework.stereotype.Service; + +/** This service is responsible for retrieving aggregated enrollments data. */ +@Service +@RequiredArgsConstructor +public class EnrollmentAggregateService { + + private final EnrollmentAnalyticsManager enrollmentAnalyticsManager; + + private final EventQueryPlanner queryPlanner; + + private final AnalyticsSecurityManager securityManager; + + private final EventQueryValidator queryValidator; + + private final MetadataItemsHandler metadataHandler; + + private final SchemeIdHandler schemeIdHandler; + + /** + * Returns the aggregated data related to enrollments, that matches the given query. + * + * @param params the {@link EventQueryParams} parameters. + * @return enrollments data as a {@link Grid} object. + */ + public Grid getEnrollments(EventQueryParams params) { + // Check access/constraints. + securityManager.decideAccessEventQuery(params); + params = securityManager.withUserConstraints(params); + + // Validate request. + queryValidator.validate(params); + + List keywords = getDimensionsKeywords(params); + List periods = getPeriods(params); + + // Set periods. + params = new EventQueryParams.Builder(params).withStartEndDatesForPeriods().build(); + + // Populate headers. + Grid grid = createGridWithHeaders(); + addCommonHeaders(grid, params, periods); + + // Add data. + if (!params.isSkipData() || params.analyzeOnly()) { + if (!periods.isEmpty()) { + params = + new EventQueryParams.Builder(params) + .withPeriods(periods.stream().flatMap(p -> p.getItems().stream()).toList(), EMPTY) + .build(); + } + + addData(grid, params); + } + + // Set response info. + metadataHandler.addMetadata(grid, params, keywords); + schemeIdHandler.applyScheme(grid, params); + + addPaging(params, UNLIMITED_PAGING, grid); + applyHeaders(grid, params); + + return grid; + } + + /** + * Creates a {@link Grid} object with default headers. + * + * @return the {@link Grid} with initial headers. + */ + private Grid createGridWithHeaders() { + return new ListGrid() + .addHeader(new GridHeader(VALUE_ID, VALUE_HEADER_NAME, NUMBER, false, false)); + } + + /** + * Adds data into the given grid, based on the given params. + * + * @param grid {@link Grid}. + * @param params the {@link EventQueryParams}. @@param maxLimit the max number of records to + * retrieve. + */ + private void addData(Grid grid, EventQueryParams params) { + Timer timer = new Timer().start().disablePrint(); + + List paramsList = queryPlanner.planAggregateQuery(params); + + for (EventQueryParams queryParams : paramsList) { + timer.getSplitTime( + "Planned enrollment query, got partitions: " + queryParams.getPartitions()); + + int maxLimit = + params.isAggregatedEnrollments() ? UNLIMITED_PAGING : queryValidator.getMaxLimit(); + + enrollmentAnalyticsManager.getEnrollments(queryParams, grid, maxLimit); + + timer.getTime("Got enrollments " + grid.getHeight()); + } + } + + private List getPeriods(EventQueryParams params) { + return params.getDimensions().stream().filter(d -> d.getDimensionType() == PERIOD).toList(); + } +} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEnrollmentAnalyticsService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EnrollmentQueryService.java similarity index 67% rename from dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEnrollmentAnalyticsService.java rename to dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EnrollmentQueryService.java index 6c786f54268c..376ff813652c 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEnrollmentAnalyticsService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EnrollmentQueryService.java @@ -27,9 +27,6 @@ */ package org.hisp.dhis.analytics.event.data; -import static com.google.common.base.Preconditions.checkNotNull; -import static org.hisp.dhis.analytics.DataQueryParams.VALUE_HEADER_NAME; -import static org.hisp.dhis.analytics.DataQueryParams.VALUE_ID; import static org.hisp.dhis.analytics.common.ColumnHeader.CREATED_BY_DISPLAY_NAME; import static org.hisp.dhis.analytics.common.ColumnHeader.ENROLLMENT; import static org.hisp.dhis.analytics.common.ColumnHeader.ENROLLMENT_DATE; @@ -49,72 +46,97 @@ import static org.hisp.dhis.analytics.event.LabelMapper.getEnrollmentLabel; import static org.hisp.dhis.analytics.event.LabelMapper.getIncidentDateLabel; import static org.hisp.dhis.analytics.event.LabelMapper.getOrgUnitLabel; +import static org.hisp.dhis.analytics.tracker.HeaderHelper.addCommonHeaders; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.UNLIMITED_PAGING; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.addPaging; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.applyHeaders; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.getDimensionsKeywords; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.setRowContextColumns; import static org.hisp.dhis.common.ValueType.DATETIME; import static org.hisp.dhis.common.ValueType.NUMBER; import static org.hisp.dhis.common.ValueType.TEXT; import java.util.List; +import lombok.RequiredArgsConstructor; import org.hisp.dhis.analytics.AnalyticsSecurityManager; -import org.hisp.dhis.analytics.data.handler.SchemeIdResponseMapper; import org.hisp.dhis.analytics.event.EnrollmentAnalyticsManager; -import org.hisp.dhis.analytics.event.EnrollmentAnalyticsService; import org.hisp.dhis.analytics.event.EventQueryParams; import org.hisp.dhis.analytics.event.EventQueryPlanner; import org.hisp.dhis.analytics.event.EventQueryValidator; -import org.hisp.dhis.common.DimensionType; -import org.hisp.dhis.common.DimensionalObject; +import org.hisp.dhis.analytics.tracker.MetadataItemsHandler; +import org.hisp.dhis.analytics.tracker.SchemeIdHandler; +import org.hisp.dhis.common.DimensionItemKeywords.Keyword; import org.hisp.dhis.common.Grid; import org.hisp.dhis.common.GridHeader; -import org.hisp.dhis.common.RequestTypeAware; import org.hisp.dhis.system.grid.ListGrid; -import org.hisp.dhis.user.UserService; import org.hisp.dhis.util.Timer; import org.springframework.stereotype.Service; -/** - * @author Markus Bekken - */ -@Service("org.hisp.dhis.analytics.event.EnrollmentAnalyticsService") -public class DefaultEnrollmentAnalyticsService extends AbstractAnalyticsService - implements EnrollmentAnalyticsService { +/** This service is responsible for querying enrollments. */ +@Service +@RequiredArgsConstructor +public class EnrollmentQueryService { private final EnrollmentAnalyticsManager enrollmentAnalyticsManager; private final EventQueryPlanner queryPlanner; - public DefaultEnrollmentAnalyticsService( - EnrollmentAnalyticsManager enrollmentAnalyticsManager, - AnalyticsSecurityManager securityManager, - EventQueryPlanner queryPlanner, - EventQueryValidator queryValidator, - SchemeIdResponseMapper schemeIdResponseMapper, - UserService userService) { - super(securityManager, queryValidator, schemeIdResponseMapper, userService); - - checkNotNull(enrollmentAnalyticsManager); - checkNotNull(queryPlanner); - checkNotNull(schemeIdResponseMapper); - - this.enrollmentAnalyticsManager = enrollmentAnalyticsManager; - this.queryPlanner = queryPlanner; - } + private final AnalyticsSecurityManager securityManager; + + private final EventQueryValidator queryValidator; - // ------------------------------------------------------------------------- - // EventAnalyticsService implementation - // ------------------------------------------------------------------------- + private final MetadataItemsHandler metadataHandler; - @Override + private final SchemeIdHandler schemeIdHandler; + + /** + * Returns a list of enrollments matching the given query. + * + * @param params the {@link EventQueryParams} parameters. + * @return enrollments data as a {@link Grid} object. + */ public Grid getEnrollments(EventQueryParams params) { - return getGrid(params); - } + // Check access/constraints. + securityManager.decideAccessEventQuery(params); + params = securityManager.withUserConstraints(params); + + // Validate request. + queryValidator.validate(params); - @Override - protected Grid createGridWithHeaders(EventQueryParams params) { - if (params.getEndpointAction() == RequestTypeAware.EndpointAction.AGGREGATE) { - return new ListGrid() - .addHeader(new GridHeader(VALUE_ID, VALUE_HEADER_NAME, NUMBER, false, false)); + List keywords = getDimensionsKeywords(params); + + // Set periods. + params = new EventQueryParams.Builder(params).withStartEndDatesForPeriods().build(); + + // Populate headers. + Grid grid = createGridWithHeaders(params); + addCommonHeaders(grid, params, List.of()); + + // Add data. + long count = 0; + + if (!params.isSkipData() || params.analyzeOnly()) { + count = addData(grid, params); } + // Set response info. + metadataHandler.addMetadata(grid, params, keywords); + schemeIdHandler.applyScheme(grid, params); + + addPaging(params, count, grid); + applyHeaders(grid, params); + setRowContextColumns(grid); + + return grid; + } + + /** + * Creates a {@link Grid} object with default headers. + * + * @param params the {@link EventQueryParams}. + * @return the {@link Grid} with initial headers. + */ + private Grid createGridWithHeaders(EventQueryParams params) { return new ListGrid() .addHeader( new GridHeader( @@ -179,45 +201,33 @@ protected Grid createGridWithHeaders(EventQueryParams params) { new GridHeader(PROGRAM_STATUS.getItem(), PROGRAM_STATUS.getName(), TEXT, false, true)); } - @Override - protected long addData(Grid grid, EventQueryParams params) { + /** + * Adds data into the given grid, based on the given params. + * + * @param grid {@link Grid}. + * @param params the {@link EventQueryParams}. @@param maxLimit the max number of records to + * retrieve. + */ + private long addData(Grid grid, EventQueryParams params) { Timer timer = new Timer().start().disablePrint(); - List paramsList; - - if (params.getEndpointAction() == RequestTypeAware.EndpointAction.AGGREGATE) { - paramsList = queryPlanner.planAggregateQuery(params); - } else { - paramsList = List.of(queryPlanner.planEnrollmentQuery(params)); - } + EventQueryParams queryParams = queryPlanner.planEnrollmentQuery(params); long count = 0; - for (EventQueryParams queryParams : paramsList) { - timer.getSplitTime("Planned event query, got partitions: " + queryParams.getPartitions()); - if (queryParams.isTotalPages() && !params.isAggregatedEnrollments()) { - count += enrollmentAnalyticsManager.getEnrollmentCount(queryParams); - } - - // maxLimit == 0 means unlimited paging - int maxLimit = params.isAggregatedEnrollments() ? 0 : queryValidator.getMaxLimit(); - enrollmentAnalyticsManager.getEnrollments(queryParams, grid, maxLimit); + timer.getSplitTime("Planned enrollment query, got partitions: " + queryParams.getPartitions()); - timer.getTime("Got enrollments " + grid.getHeight()); + if (queryParams.isTotalPages()) { + count += enrollmentAnalyticsManager.getEnrollmentCount(queryParams); } - return count; - } + int maxLimit = + params.isAggregatedEnrollments() ? UNLIMITED_PAGING : queryValidator.getMaxLimit(); - @Override - protected List getPeriods(EventQueryParams params) { - // for aggregated enrollments only - if (!params.isAggregatedEnrollments()) { - return List.of(); - } + enrollmentAnalyticsManager.getEnrollments(queryParams, grid, maxLimit); - return params.getDimensions().stream() - .filter(d -> d.getDimensionType() == DimensionType.PERIOD) - .toList(); + timer.getTime("Got enrollments " + grid.getHeight()); + + return count; } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventAggregateService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventAggregateService.java new file mode 100644 index 000000000000..39ba417dfbaf --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventAggregateService.java @@ -0,0 +1,586 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.analytics.event.data; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.commons.lang3.StringUtils.defaultIfEmpty; +import static org.hisp.dhis.analytics.AnalyticsMetaDataKey.DIMENSIONS; +import static org.hisp.dhis.analytics.AnalyticsMetaDataKey.ITEMS; +import static org.hisp.dhis.analytics.DataQueryParams.DENOMINATOR_HEADER_NAME; +import static org.hisp.dhis.analytics.DataQueryParams.DENOMINATOR_ID; +import static org.hisp.dhis.analytics.DataQueryParams.DISPLAY_NAME_DATA_X; +import static org.hisp.dhis.analytics.DataQueryParams.DIVISOR_HEADER_NAME; +import static org.hisp.dhis.analytics.DataQueryParams.DIVISOR_ID; +import static org.hisp.dhis.analytics.DataQueryParams.FACTOR_HEADER_NAME; +import static org.hisp.dhis.analytics.DataQueryParams.FACTOR_ID; +import static org.hisp.dhis.analytics.DataQueryParams.MULTIPLIER_HEADER_NAME; +import static org.hisp.dhis.analytics.DataQueryParams.MULTIPLIER_ID; +import static org.hisp.dhis.analytics.DataQueryParams.NUMERATOR_HEADER_NAME; +import static org.hisp.dhis.analytics.DataQueryParams.NUMERATOR_ID; +import static org.hisp.dhis.analytics.DataQueryParams.VALUE_HEADER_NAME; +import static org.hisp.dhis.analytics.DataQueryParams.VALUE_ID; +import static org.hisp.dhis.analytics.event.EventAnalyticsUtils.addValues; +import static org.hisp.dhis.analytics.event.EventAnalyticsUtils.generateEventDataPermutations; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.UNLIMITED_PAGING; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.addPaging; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.getDimensionsKeywords; +import static org.hisp.dhis.analytics.util.AnalyticsUtils.isTableLayout; +import static org.hisp.dhis.analytics.util.AnalyticsUtils.throwIllegalQueryEx; +import static org.hisp.dhis.common.DimensionalObject.CATEGORYOPTIONCOMBO_DIM_ID; +import static org.hisp.dhis.common.DimensionalObject.DATA_COLLAPSED_DIM_ID; +import static org.hisp.dhis.common.DimensionalObject.DATA_X_DIM_ID; +import static org.hisp.dhis.common.DimensionalObject.ORGUNIT_DIM_ID; +import static org.hisp.dhis.common.DimensionalObject.PERIOD_DIM_ID; +import static org.hisp.dhis.common.IdentifiableObjectUtils.join; +import static org.hisp.dhis.common.ValueType.BOOLEAN; +import static org.hisp.dhis.common.ValueType.NUMBER; +import static org.hisp.dhis.common.ValueType.TEXT; +import static org.hisp.dhis.commons.collection.ListUtils.removeEmptys; +import static org.hisp.dhis.feedback.ErrorCode.E7128; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.hisp.dhis.analytics.AnalyticsSecurityManager; +import org.hisp.dhis.analytics.EventAnalyticsDimensionalItem; +import org.hisp.dhis.analytics.cache.AnalyticsCache; +import org.hisp.dhis.analytics.event.EnrollmentAnalyticsManager; +import org.hisp.dhis.analytics.event.EventAnalyticsManager; +import org.hisp.dhis.analytics.event.EventDataQueryService; +import org.hisp.dhis.analytics.event.EventQueryParams; +import org.hisp.dhis.analytics.event.EventQueryPlanner; +import org.hisp.dhis.analytics.event.EventQueryValidator; +import org.hisp.dhis.analytics.tracker.MetadataItemsHandler; +import org.hisp.dhis.analytics.tracker.SchemeIdHandler; +import org.hisp.dhis.common.DimensionItemKeywords.Keyword; +import org.hisp.dhis.common.DimensionalObject; +import org.hisp.dhis.common.EventAnalyticalObject; +import org.hisp.dhis.common.Grid; +import org.hisp.dhis.common.GridHeader; +import org.hisp.dhis.common.MetadataItem; +import org.hisp.dhis.common.QueryItem; +import org.hisp.dhis.common.ValueTypedDimensionalItemObject; +import org.hisp.dhis.dataelement.DataElementService; +import org.hisp.dhis.legend.Legend; +import org.hisp.dhis.option.Option; +import org.hisp.dhis.system.grid.ListGrid; +import org.hisp.dhis.trackedentity.TrackedEntityAttributeService; +import org.hisp.dhis.util.Timer; +import org.springframework.stereotype.Service; + +/** This service is responsible for retrieving aggregated event data. */ +@Service +@RequiredArgsConstructor +public class EventAggregateService { + + private static final String DASH_PRETTY_SEPARATOR = " - "; + + private static final String SPACE = " "; + + private static final String TOTAL_COLUMN_PRETTY_NAME = "Total"; + + private static final Map COLUMN_NAMES = + Map.of( + DATA_X_DIM_ID, "data", + CATEGORYOPTIONCOMBO_DIM_ID, "categoryoptioncombo", + PERIOD_DIM_ID, "period", + ORGUNIT_DIM_ID, "organisationunit"); + + private static final Option OPT_TRUE = new Option("Yes", "1"); + + private static final Option OPT_FALSE = new Option("No", "0"); + + private final DataElementService dataElementService; + + private final TrackedEntityAttributeService trackedEntityAttributeService; + + private final EventAnalyticsManager eventAnalyticsManager; + + private final EnrollmentAnalyticsManager enrollmentAnalyticsManager; + + private final EventDataQueryService eventDataQueryService; + + private final EventQueryPlanner queryPlanner; + + private final AnalyticsCache analyticsCache; + + private final AnalyticsSecurityManager securityManager; + + private final EventQueryValidator queryValidator; + + private final MetadataItemsHandler metadataHandler; + + private final SchemeIdHandler schemeIdHandler; + + /** + * Generates an aggregated for the given query. The grid will represent a table with dimensions + * used as columns and rows as specified in columns and rows dimension arguments. If columns and + * rows are null or empty, the normalized table will be returned. + * + *

If metadata is included in the query, the metadata map of the grid will contain keys + * described in {@link AnalyticsMetaDataKey}. + * + * @param params the {@link EventQueryParams}. + * @param columns the identifiers of the dimensions to use as columns. + * @param rows the identifiers of the dimensions to use as rows. + * @return aggregated data as a {@link Grid} object. + */ + public Grid getAggregatedData(EventQueryParams params, List columns, List rows) { + return isTableLayout(columns, rows) + ? getAggregatedDataTableLayout(params, columns, rows) + : getAggregatedData(params); + } + + /** + * Generates aggregated event data for the given analytical object. + * + * @param object the {@link EventAnalyticalObject}. + * @return aggregated event data as a {@link Grid} object. + */ + public Grid getAggregatedData(EventAnalyticalObject object) { + EventQueryParams params = eventDataQueryService.getFromAnalyticalObject(object); + + return getAggregatedData(params); + } + + /** + * Generates aggregated event data for the given query, along with required validation. + * + * @param params the {@link EventQueryParams}. + * @return aggregated event data as a {@link Grid} object. + */ + public Grid getAggregatedData(EventQueryParams params) { + securityManager.decideAccessEventQuery(params); + params = securityManager.withUserConstraints(params); + + queryValidator.validate(params); + + if (analyticsCache.isEnabled() && !params.analyzeOnly()) { + EventQueryParams immutableParams = new EventQueryParams.Builder(params).build(); + return analyticsCache.getOrFetch(params, p -> getAggregatedDataGrid(immutableParams)); + } + + return getAggregatedDataGrid(params); + } + + /** + * Fetches aggregated event data for the given query and creates a grid containing headers and + * metadata if applicable. + * + * @param params the {@link EventQueryParams}. + * @return aggregated event data as a {@link Grid} object. + */ + private Grid getAggregatedDataGrid(EventQueryParams params) { + params.removeProgramIndicatorItems(); + + Grid grid = new ListGrid(); + int maxLimit = queryValidator.getMaxLimit(); + List keywords = getDimensionsKeywords(params); + + if (!params.isSkipData() || params.analyzeOnly()) { + addHeaders(params, grid); + addData(grid, params, maxLimit); + + // Sort, done again due to potential multiple partitions. + if (params.hasSortOrder() && grid.getHeight() > 0) { + grid.sortGrid(1, params.getSortOrderAsInt()); + } + + // Limit the grid, if asked for. + if (params.hasLimit() && grid.getHeight() > params.getLimit()) { + grid.limitGrid(params.getLimit()); + } + } + + addPaging(params, UNLIMITED_PAGING, grid); + schemeIdHandler.applyScheme(grid, params); + metadataHandler.addMetadata(grid, params, keywords); + + return grid; + } + + /** + * Adds data into the given grid, based on the given params. + * + * @param grid {@link Grid}. + * @param params the {@link EventQueryParams}. @@param maxLimit the max number of records to + * retrieve. + */ + private void addData(Grid grid, EventQueryParams params, int maxLimit) { + Timer timer = new Timer().start().disablePrint(); + + List queries = queryPlanner.planAggregateQuery(params); + + timer.getSplitTime("Planned event query, got partitions: " + params.getPartitions()); + + for (EventQueryParams query : queries) { + if (query.hasEnrollmentProgramIndicatorDimension()) { + enrollmentAnalyticsManager.getAggregatedEventData(query, grid, maxLimit); + } else { + eventAnalyticsManager.getAggregatedEventData(query, grid, maxLimit); + } + } + + timer.getTime("Got aggregated events"); + + if (maxLimit > 0 && grid.getHeight() > maxLimit) { + throwIllegalQueryEx(E7128, maxLimit); + } + } + + /** + * Add headers into the given {@link Grid}. + * + * @param params the {@link EventQueryParams}. + * @return the {@link Grid} with initial headers. + */ + private void addHeaders(EventQueryParams params, Grid grid) { + if (params.isCollapseDataDimensions() || params.isAggregateData()) { + grid.addHeader(new GridHeader(DATA_COLLAPSED_DIM_ID, DISPLAY_NAME_DATA_X, TEXT, false, true)); + } else { + for (QueryItem item : params.getItems()) { + String displayProperty = item.getItem().getDisplayProperty(params.getDisplayProperty()); + + grid.addHeader( + new GridHeader( + item.getItem().getUid(), + displayProperty, + item.getValueType(), + false, + true, + item.getOptionSet(), + item.getLegendSet())); + } + } + + for (DimensionalObject dimension : params.getDimensions()) { + String displayProperty = dimension.getDisplayProperty(params.getDisplayProperty()); + + grid.addHeader(new GridHeader(dimension.getDimension(), displayProperty, TEXT, false, true)); + } + + grid.addHeader(new GridHeader(VALUE_ID, VALUE_HEADER_NAME, NUMBER, false, false)); + + if (params.isIncludeNumDen()) { + grid.addHeader(new GridHeader(NUMERATOR_ID, NUMERATOR_HEADER_NAME, NUMBER, false, false)) + .addHeader(new GridHeader(DENOMINATOR_ID, DENOMINATOR_HEADER_NAME, NUMBER, false, false)) + .addHeader(new GridHeader(FACTOR_ID, FACTOR_HEADER_NAME, NUMBER, false, false)) + .addHeader(new GridHeader(MULTIPLIER_ID, MULTIPLIER_HEADER_NAME, NUMBER, false, false)) + .addHeader(new GridHeader(DIVISOR_ID, DIVISOR_HEADER_NAME, NUMBER, false, false)); + } + } + + /** + * Puts elements into the mapping table. The elements are fetched from the query parameters. + * + * @param grid the {@link Grid}. + * @param params the {@link EventQueryParams}. + * @param table the map to add elements to. + * @param dimension the dimension identifier. + */ + private void addEventDataObjects( + Grid grid, + EventQueryParams params, + Map> table, + String dimension) { + List objects = + params.getEventReportDimensionalItemArrayExploded(dimension); + + if (objects.isEmpty()) { + ValueTypedDimensionalItemObject eventDimensionalItemObject = + dataElementService.getDataElement(dimension); + + if (eventDimensionalItemObject == null) { + eventDimensionalItemObject = + trackedEntityAttributeService.getTrackedEntityAttribute(dimension); + } + + addEventReportDimensionalItems(eventDimensionalItemObject, objects, grid, dimension); + + table.put( + eventDimensionalItemObject.getDisplayProperty(params.getDisplayProperty()), objects); + } else { + table.put(dimension, objects); + } + } + + /** + * Adds dimensional items to the given list of objects. Send in a list of {@link + * EventAnalyticsDimensionalItem} and add properties from {@link ValueTypedDimensionalItemObject} + * parameter. + * + * @param eventDimensionalItemObject the {@link ValueTypedDimensionalItemObject} object to get + * properties from. + * @param dimensionalItems the list of {@link EventAnalyticsDimensionalItem} objects. + * @param grid the {@link Grid} from the event analytics request. + * @param dimension the dimension identifier. + */ + @SuppressWarnings("unchecked") + private void addEventReportDimensionalItems( + ValueTypedDimensionalItemObject eventDimensionalItemObject, + List dimensionalItems, + Grid grid, + String dimension) { + checkNotNull( + eventDimensionalItemObject, String.format("Data dimension '%s' is invalid", dimension)); + + String parentUid = eventDimensionalItemObject.getUid(); + + if (eventDimensionalItemObject.getValueType() == BOOLEAN) { + dimensionalItems.add(new EventAnalyticsDimensionalItem(OPT_TRUE, parentUid)); + dimensionalItems.add(new EventAnalyticsDimensionalItem(OPT_FALSE, parentUid)); + } + + if (eventDimensionalItemObject.hasOptionSet()) { + for (Option option : eventDimensionalItemObject.getOptionSet().getOptions()) { + dimensionalItems.add(new EventAnalyticsDimensionalItem(option, parentUid)); + } + } else if (eventDimensionalItemObject.hasLegendSet()) { + List legendOptions = + (List) + ((Map) grid.getMetaData().get(DIMENSIONS.getKey())).get(dimension); + + if (legendOptions.isEmpty()) { + List legends = eventDimensionalItemObject.getLegendSet().getSortedLegends(); + addLegends(dimensionalItems, parentUid, legends); + } else { + addLegendOptions(dimensionalItems, grid, parentUid, legendOptions); + } + } + } + + /** + * Adds the given legends into the list of dimensionalItems. + * + * @param dimensionalItems + * @param parentUid + * @param legends + */ + private static void addLegends( + List dimensionalItems, + String parentUid, + List legends) { + for (Legend legend : legends) { + for (int i = legend.getStartValue().intValue(); i < legend.getEndValue().intValue(); i++) { + dimensionalItems.add( + new EventAnalyticsDimensionalItem( + new Option(String.valueOf(i), String.valueOf(i)), parentUid)); + } + } + } + + /** + * Adds the given legendOptions into the list of dimensionalItems. + * + * @param dimensionalItems + * @param grid + * @param parentUid + * @param legendOptions + */ + private static void addLegendOptions( + List dimensionalItems, + Grid grid, + String parentUid, + List legendOptions) { + for (String legend : legendOptions) { + MetadataItem metadataItem = + (MetadataItem) ((Map) grid.getMetaData().get(ITEMS.getKey())).get(legend); + + dimensionalItems.add( + new EventAnalyticsDimensionalItem(new Option(metadataItem.getName(), legend), parentUid)); + } + } + + /** + * Creates a grid with table layout for downloading event reports. The grid is dynamically made + * from rows and columns input, which refers to the dimensions requested. + * + *

For event reports each option for a dimension will be an {@link + * EventAnalyticsDimensionalItem} and all permutations will be added to the grid. + * + * @param params the {@link EventQueryParams}. + * @param columns the identifiers of the dimensions to use as columns. + * @param rows the identifiers of the dimensions to use as rows. + * @return aggregated data as a Grid object. + */ + private Grid getAggregatedDataTableLayout( + EventQueryParams params, List columns, List rows) { + params.removeProgramIndicatorItems(); + + Grid grid = getAggregatedData(params); + + removeEmptys(columns); + removeEmptys(rows); + + Map> tableColumns = new LinkedHashMap<>(); + + if (columns != null) { + for (String dimension : columns) { + addEventDataObjects(grid, params, tableColumns, dimension); + } + } + + Map> tableRows = new LinkedHashMap<>(); + List rowDimensions = new ArrayList<>(); + + if (rows != null) { + for (String dimension : rows) { + rowDimensions.add(dimension); + addEventDataObjects(grid, params, tableRows, dimension); + } + } + + List> rowPermutations = + generateEventDataPermutations(tableRows); + + List> columnPermutations = + generateEventDataPermutations(tableColumns); + + return generateOutputGrid(grid, params, rowPermutations, columnPermutations, rowDimensions); + } + + /** + * Generates an output grid for event analytics download based on input parameters. + * + * @param grid the result grid. + * @param params the {@link EventQueryParams}. + * @param rowPermutations the row permutations + * @param columnPermutations the column permutations. + * @param rowDimensions the row dimensions. + * @return grid with table layout. + */ + @SuppressWarnings("unchecked") + private Grid generateOutputGrid( + Grid grid, + EventQueryParams params, + List> rowPermutations, + List> columnPermutations, + List rowDimensions) { + Grid outputGrid = new ListGrid(); + outputGrid.setTitle(join(params.getFilterItems())); + + for (String row : rowDimensions) { + MetadataItem metadataItem = + (MetadataItem) ((Map) grid.getMetaData().get(ITEMS.getKey())).get(row); + + String name = defaultIfEmpty(metadataItem.getName(), row); + String col = defaultIfEmpty(COLUMN_NAMES.get(row), row); + + outputGrid.addHeader(new GridHeader(name, col, TEXT, false, true)); + } + + columnPermutations.forEach( + permutation -> { + StringBuilder builder = new StringBuilder(); + + permutation.forEach( + (key, value) -> { + if (!key.equals(ORGUNIT_DIM_ID) && !key.equals(PERIOD_DIM_ID)) { + builder.append(key).append(SPACE); + } + builder + .append(value.getDisplayProperty(params.getDisplayProperty())) + .append(DASH_PRETTY_SEPARATOR); + }); + + String display = + builder.length() > 0 + ? builder.substring(0, builder.lastIndexOf(DASH_PRETTY_SEPARATOR)) + : TOTAL_COLUMN_PRETTY_NAME; + + outputGrid.addHeader(new GridHeader(display, display, NUMBER, false, false)); + }); + + for (Map rowCombination : rowPermutations) { + outputGrid.addRow(); + List> ids = new ArrayList<>(); + Map displayObjects = new HashMap<>(); + + boolean fillDisplayList = true; + + for (Map columnCombination : columnPermutations) { + List idList = new ArrayList<>(); + + boolean finalFillDisplayList = fillDisplayList; + rowCombination.forEach( + (key, value) -> { + idList.add(value.toString()); + + if (finalFillDisplayList) { + displayObjects.put(value.getParentUid(), value); + } + }); + + columnCombination.forEach((key, value) -> idList.add(value.toString())); + + ids.add(idList); + fillDisplayList = false; + } + + addValuesInOutputGrid(rowDimensions, outputGrid, displayObjects, params); + addValues(ids, grid, outputGrid); + } + + return getGridWithRows(grid, outputGrid); + } + + /** + * Returns a valid grid. + * + * @param grid the {@link Grid}. + * @param outputGrid the output {@link Grid}. + */ + private static Grid getGridWithRows(Grid grid, Grid outputGrid) { + return outputGrid.getRows().isEmpty() ? grid : outputGrid; + } + + /** + * Adds values to the given output grid. Display objects are not empty if columns and rows are not + * empty. + * + * @param rowDimensions the list of row dimensions. + * @param grid the {@link Grid}. + * @param displayObjects the map of display objects. + * @param params the {@link EventQueryParams}. + */ + private static void addValuesInOutputGrid( + List rowDimensions, + Grid grid, + Map displayObjects, + EventQueryParams params) { + if (!displayObjects.isEmpty()) { + rowDimensions.forEach( + dimension -> + grid.addValue( + displayObjects.get(dimension).getDisplayProperty(params.getDisplayProperty()))); + } + } +} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventQueryService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventQueryService.java new file mode 100644 index 000000000000..24f4741250d9 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventQueryService.java @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.analytics.event.data; + +import static org.hisp.dhis.analytics.common.ColumnHeader.CENTER; +import static org.hisp.dhis.analytics.common.ColumnHeader.COUNT; +import static org.hisp.dhis.analytics.common.ColumnHeader.CREATED_BY_DISPLAY_NAME; +import static org.hisp.dhis.analytics.common.ColumnHeader.ENROLLMENT_DATE; +import static org.hisp.dhis.analytics.common.ColumnHeader.EVENT; +import static org.hisp.dhis.analytics.common.ColumnHeader.EVENT_DATE; +import static org.hisp.dhis.analytics.common.ColumnHeader.EVENT_STATUS; +import static org.hisp.dhis.analytics.common.ColumnHeader.EXTENT; +import static org.hisp.dhis.analytics.common.ColumnHeader.GEOMETRY; +import static org.hisp.dhis.analytics.common.ColumnHeader.INCIDENT_DATE; +import static org.hisp.dhis.analytics.common.ColumnHeader.LAST_UPDATED; +import static org.hisp.dhis.analytics.common.ColumnHeader.LAST_UPDATED_BY_DISPLAY_NAME; +import static org.hisp.dhis.analytics.common.ColumnHeader.LATITUDE; +import static org.hisp.dhis.analytics.common.ColumnHeader.LONGITUDE; +import static org.hisp.dhis.analytics.common.ColumnHeader.ORG_UNIT_CODE; +import static org.hisp.dhis.analytics.common.ColumnHeader.ORG_UNIT_NAME; +import static org.hisp.dhis.analytics.common.ColumnHeader.ORG_UNIT_NAME_HIERARCHY; +import static org.hisp.dhis.analytics.common.ColumnHeader.POINTS; +import static org.hisp.dhis.analytics.common.ColumnHeader.PROGRAM_INSTANCE; +import static org.hisp.dhis.analytics.common.ColumnHeader.PROGRAM_STAGE; +import static org.hisp.dhis.analytics.common.ColumnHeader.PROGRAM_STATUS; +import static org.hisp.dhis.analytics.common.ColumnHeader.SCHEDULED_DATE; +import static org.hisp.dhis.analytics.common.ColumnHeader.STORED_BY; +import static org.hisp.dhis.analytics.common.ColumnHeader.TRACKED_ENTITY; +import static org.hisp.dhis.analytics.event.LabelMapper.getEnrollmentDateLabel; +import static org.hisp.dhis.analytics.event.LabelMapper.getEventDateLabel; +import static org.hisp.dhis.analytics.event.LabelMapper.getEventLabel; +import static org.hisp.dhis.analytics.event.LabelMapper.getIncidentDateLabel; +import static org.hisp.dhis.analytics.event.LabelMapper.getOrgUnitLabel; +import static org.hisp.dhis.analytics.event.LabelMapper.getProgramStageLabel; +import static org.hisp.dhis.analytics.event.LabelMapper.getScheduledDateLabel; +import static org.hisp.dhis.analytics.tracker.HeaderHelper.addCommonHeaders; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.addPaging; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.applyHeaders; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.getDimensionsKeywords; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.setRowContextColumns; +import static org.hisp.dhis.analytics.util.AnalyticsUtils.throwIllegalQueryEx; +import static org.hisp.dhis.common.ValueType.DATETIME; +import static org.hisp.dhis.common.ValueType.NUMBER; +import static org.hisp.dhis.common.ValueType.TEXT; +import static org.hisp.dhis.feedback.ErrorCode.E7218; + +import java.util.List; +import javax.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import org.hisp.dhis.analytics.AnalyticsSecurityManager; +import org.hisp.dhis.analytics.Rectangle; +import org.hisp.dhis.analytics.event.EventAnalyticsManager; +import org.hisp.dhis.analytics.event.EventQueryParams; +import org.hisp.dhis.analytics.event.EventQueryPlanner; +import org.hisp.dhis.analytics.event.EventQueryValidator; +import org.hisp.dhis.analytics.tracker.MetadataItemsHandler; +import org.hisp.dhis.analytics.tracker.SchemeIdHandler; +import org.hisp.dhis.common.DimensionItemKeywords.Keyword; +import org.hisp.dhis.common.Grid; +import org.hisp.dhis.common.GridHeader; +import org.hisp.dhis.system.database.DatabaseInfoProvider; +import org.hisp.dhis.system.grid.ListGrid; +import org.hisp.dhis.util.Timer; +import org.springframework.stereotype.Service; + +/** This service is responsible for querying events. */ +@Service +@RequiredArgsConstructor +public class EventQueryService { + + private final AnalyticsSecurityManager securityManager; + + private final EventQueryValidator queryValidator; + + private final EventAnalyticsManager eventAnalyticsManager; + + private final EventQueryPlanner queryPlanner; + + private final DatabaseInfoProvider databaseInfoProvider; + + private final MetadataItemsHandler metadataHandler; + + private final SchemeIdHandler schemeIdHandler; + + private boolean spatialSupport; + + @PostConstruct + void init() { + this.spatialSupport = databaseInfoProvider.getDatabaseInfo().isSpatialSupport(); + } + + /** + * Returns a list of events matching the given query. + * + * @param params the event query parameters. + * @return events as a {@Grid} object. + */ + public Grid getEvents(EventQueryParams params) { + // Check access/constraints. + securityManager.decideAccessEventQuery(params); + params = securityManager.withUserConstraints(params); + + // Validate request. + queryValidator.validate(params); + + List keywords = getDimensionsKeywords(params); + + // Set periods. + params = new EventQueryParams.Builder(params).withStartEndDatesForPeriods().build(); + + // Populate headers. + Grid grid = createGridWithHeaders(params); + addCommonHeaders(grid, params, List.of()); + + // Add data. + long count = 0; + + if (!params.isSkipData() || params.analyzeOnly()) { + count = addData(grid, params); + } + + // Set response info. + metadataHandler.addMetadata(grid, params, keywords); + schemeIdHandler.applyScheme(grid, params); + + addPaging(params, count, grid); + applyHeaders(grid, params); + setRowContextColumns(grid); + + return grid; + } + + /** + * Returns a list of event clusters matching the given query. + * + * @param params the event query parameters. + * @return event clusters as a {@link Grid} object. + */ + public Grid getEventClusters(EventQueryParams params) { + if (!spatialSupport) { + throwIllegalQueryEx(E7218); + } + + params = + new EventQueryParams.Builder(params) + .withGeometryOnly(true) + .withStartEndDatesForPeriods() + .build(); + + securityManager.decideAccessEventQuery(params); + + queryValidator.validate(params); + + Grid grid = new ListGrid(); + grid.addHeader(new GridHeader(COUNT.getItem(), COUNT.getName(), NUMBER, false, false)) + .addHeader(new GridHeader(CENTER.getItem(), CENTER.getName(), TEXT, false, false)) + .addHeader(new GridHeader(EXTENT.getItem(), EXTENT.getName(), TEXT, false, false)) + .addHeader(new GridHeader(POINTS.getItem(), POINTS.getName(), TEXT, false, false)); + + params = queryPlanner.planEventQuery(params); + + eventAnalyticsManager.getEventClusters(params, grid, queryValidator.getMaxLimit()); + + return grid; + } + + /** + * Returns a Rectangle with information about event count and extent of the spatial rectangle for + * the given query. + * + * @param params the event query parameters. + * @return event clusters as a {@link Grid} object. + */ + public Rectangle getRectangle(EventQueryParams params) { + if (!spatialSupport) { + throwIllegalQueryEx(E7218); + } + + params = + new EventQueryParams.Builder(params) + .withGeometryOnly(true) + .withStartEndDatesForPeriods() + .build(); + + securityManager.decideAccessEventQuery(params); + + queryValidator.validate(params); + + params = queryPlanner.planEventQuery(params); + + return eventAnalyticsManager.getRectangle(params); + } + + /** + * Creates a grid with headers. + * + * @param params the {@link EventQueryParams}. + */ + private Grid createGridWithHeaders(EventQueryParams params) { + Grid grid = new ListGrid(); + + grid.addHeader( + new GridHeader( + EVENT.getItem(), + getEventLabel(params.getProgramStage(), EVENT.getName()), + TEXT, + false, + true)) + .addHeader( + new GridHeader( + PROGRAM_STAGE.getItem(), + getProgramStageLabel(params.getProgramStage(), PROGRAM_STAGE.getName()), + TEXT, + false, + true)) + .addHeader( + new GridHeader( + EVENT_DATE.getItem(), + getEventDateLabel(params.getProgramStage(), EVENT_DATE.getName()), + DATETIME, + false, + true)) + .addHeader(new GridHeader(STORED_BY.getItem(), STORED_BY.getName(), TEXT, false, true)) + .addHeader( + new GridHeader( + CREATED_BY_DISPLAY_NAME.getItem(), + CREATED_BY_DISPLAY_NAME.getName(), + TEXT, + false, + true)) + .addHeader( + new GridHeader( + LAST_UPDATED_BY_DISPLAY_NAME.getItem(), + LAST_UPDATED_BY_DISPLAY_NAME.getName(), + TEXT, + false, + true)) + .addHeader( + new GridHeader(LAST_UPDATED.getItem(), LAST_UPDATED.getName(), DATETIME, false, true)) + .addHeader( + new GridHeader( + SCHEDULED_DATE.getItem(), + getScheduledDateLabel(params.getProgramStage(), SCHEDULED_DATE.getName()), + DATETIME, + false, + true)); + + if (params.getProgram().isRegistration()) { + grid.addHeader( + new GridHeader( + ENROLLMENT_DATE.getItem(), + getEnrollmentDateLabel(params.getProgram(), ENROLLMENT_DATE.getName()), + DATETIME, + false, + true)) + .addHeader( + new GridHeader( + INCIDENT_DATE.getItem(), + getIncidentDateLabel(params.getProgram(), INCIDENT_DATE.getName()), + DATETIME, + false, + true)) + .addHeader( + new GridHeader(TRACKED_ENTITY.getItem(), TRACKED_ENTITY.getName(), TEXT, false, true)) + .addHeader( + new GridHeader( + PROGRAM_INSTANCE.getItem(), PROGRAM_INSTANCE.getName(), TEXT, false, true)); + } + + grid.addHeader(new GridHeader(GEOMETRY.getItem(), GEOMETRY.getName(), TEXT, false, true)) + .addHeader(new GridHeader(LONGITUDE.getItem(), LONGITUDE.getName(), NUMBER, false, true)) + .addHeader(new GridHeader(LATITUDE.getItem(), LATITUDE.getName(), NUMBER, false, true)) + .addHeader( + new GridHeader( + ORG_UNIT_NAME.getItem(), + getOrgUnitLabel(params.getProgram(), ORG_UNIT_NAME.getName()), + TEXT, + false, + true)) + .addHeader( + new GridHeader( + ORG_UNIT_NAME_HIERARCHY.getItem(), + ORG_UNIT_NAME_HIERARCHY.getName(), + TEXT, + false, + true)) + .addHeader( + new GridHeader(ORG_UNIT_CODE.getItem(), ORG_UNIT_CODE.getName(), TEXT, false, true)) + .addHeader( + new GridHeader(PROGRAM_STATUS.getItem(), PROGRAM_STATUS.getName(), TEXT, false, true)) + .addHeader( + new GridHeader(EVENT_STATUS.getItem(), EVENT_STATUS.getName(), TEXT, false, true)); + + return grid; + } + + /** + * Adds event data to the given grid. Returns the number of events matching the given event query. + * + * @param grid the {@link Grid}. + * @param params the {@link EventQueryParams}. + * @return the count of events. + */ + private long addData(Grid grid, EventQueryParams params) { + Timer timer = new Timer().start().disablePrint(); + + params = queryPlanner.planEventQuery(params); + + timer.getSplitTime("Planned event query, got partitions: " + params.getPartitions()); + + long count = 0; + EventQueryParams immutableParams = new EventQueryParams.Builder(params).build(); + + if (params.getPartitions().hasAny() || params.isSkipPartitioning()) { + eventAnalyticsManager.getEvents(immutableParams, grid, queryValidator.getMaxLimit()); + + if (params.isPaging() && params.isTotalPages()) { + count = eventAnalyticsManager.getEventCount(immutableParams); + } + + timer.getTime("Got events " + grid.getHeight()); + } + + return count; + } +} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/tracker/HeaderHelper.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/tracker/HeaderHelper.java new file mode 100644 index 000000000000..50e304ead549 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/tracker/HeaderHelper.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.analytics.tracker; + +import static java.util.stream.Collectors.counting; +import static java.util.stream.Collectors.groupingBy; +import static lombok.AccessLevel.PRIVATE; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.getItemUid; +import static org.hisp.dhis.common.ValueType.COORDINATE; +import static org.hisp.dhis.common.ValueType.ORGANISATION_UNIT; +import static org.hisp.dhis.common.ValueType.REFERENCE; +import static org.hisp.dhis.common.ValueType.TEXT; + +import java.util.List; +import java.util.Map; +import lombok.NoArgsConstructor; +import org.hisp.dhis.analytics.event.EventQueryParams; +import org.hisp.dhis.common.DimensionalObject; +import org.hisp.dhis.common.DisplayProperty; +import org.hisp.dhis.common.Grid; +import org.hisp.dhis.common.GridHeader; +import org.hisp.dhis.common.QueryItem; +import org.hisp.dhis.common.RepeatableStageParams; + +@NoArgsConstructor(access = PRIVATE) +public class HeaderHelper { + public static void addCommonHeaders( + Grid grid, EventQueryParams params, List periods) { + + for (DimensionalObject dimension : params.getDimensions()) { + grid.addHeader( + new GridHeader( + dimension.getDimension(), dimension.getDimensionDisplayName(), TEXT, false, true)); + } + + for (DimensionalObject dimension : periods) { + grid.addHeader( + new GridHeader( + dimension.getDimension(), dimension.getDimensionDisplayName(), TEXT, false, true)); + } + + DisplayProperty displayProperty = params.getDisplayProperty(); + Map repeatedNames = + params.getItems().stream() + .collect(groupingBy(s -> s.getItem().getDisplayProperty(displayProperty), counting())); + + for (QueryItem item : params.getItems()) { + /** + * If the request contains an item of value type ORGANISATION_UNIT and the item UID is linked + * to coordinates (coordinateField), then create header of value type COORDINATE and type + * Point. + */ + if (item.getValueType() == ORGANISATION_UNIT + && params.getCoordinateFields().stream() + .anyMatch(f -> f.equals(item.getItem().getUid()))) { + grid.addHeader( + new GridHeader( + item.getItem().getUid(), + item.getItem().getDisplayProperty(displayProperty), + COORDINATE, + false, + true, + item.getOptionSet(), + item.getLegendSet())); + } else if (item.hasNonDefaultRepeatableProgramStageOffset()) { + String column = item.getItem().getDisplayProperty(displayProperty); + String displayColumn = item.getColumnName(displayProperty, repeatedNames.get(column) > 1); + + RepeatableStageParams repeatableStageParams = item.getRepeatableStageParams(); + + String name = repeatableStageParams.getDimension(); + + grid.addHeader( + new GridHeader( + name, + column, + displayColumn, + repeatableStageParams.simpleStageValueExpected() ? item.getValueType() : REFERENCE, + false, + true, + item.getOptionSet(), + item.getLegendSet(), + item.getProgramStage().getUid(), + item.getRepeatableStageParams())); + } else { + String uid = getItemUid(item); + String column = item.getItem().getDisplayProperty(displayProperty); + String displayColumn = item.getColumnName(displayProperty, repeatedNames.get(column) > 1); + + grid.addHeader( + new GridHeader( + uid, + column, + displayColumn, + item.getValueType(), + false, + true, + item.getOptionSet(), + item.getLegendSet())); + } + } + } +} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/AbstractAnalyticsService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/tracker/MetadataItemsHandler.java similarity index 54% rename from dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/AbstractAnalyticsService.java rename to dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/tracker/MetadataItemsHandler.java index 230fe44bcef6..86a3a2e78c5d 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/AbstractAnalyticsService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/tracker/MetadataItemsHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022, University of Oslo + * Copyright (c) 2004-2024, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -25,31 +25,25 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.hisp.dhis.analytics.event.data; +package org.hisp.dhis.analytics.tracker; import static java.util.Collections.emptyList; import static java.util.Optional.empty; -import static java.util.stream.Collectors.counting; -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toList; import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; -import static org.apache.commons.lang3.StringUtils.EMPTY; -import static org.apache.commons.lang3.StringUtils.joinWith; import static org.hisp.dhis.analytics.AnalyticsMetaDataKey.DIMENSIONS; import static org.hisp.dhis.analytics.AnalyticsMetaDataKey.ITEMS; import static org.hisp.dhis.analytics.AnalyticsMetaDataKey.ORG_UNIT_HIERARCHY; import static org.hisp.dhis.analytics.AnalyticsMetaDataKey.ORG_UNIT_NAME_HIERARCHY; -import static org.hisp.dhis.analytics.AnalyticsMetaDataKey.PAGER; import static org.hisp.dhis.analytics.event.data.QueryItemHelper.getItemOptions; import static org.hisp.dhis.analytics.event.data.QueryItemHelper.getItemOptionsAsFilter; +import static org.hisp.dhis.analytics.tracker.ResponseHelper.getItemUid; +import static org.hisp.dhis.analytics.util.AnalyticsOrganisationUnitUtils.getUserOrganisationUnitItems; import static org.hisp.dhis.common.DimensionalObject.ORGUNIT_DIM_ID; import static org.hisp.dhis.common.DimensionalObject.PERIOD_DIM_ID; import static org.hisp.dhis.common.DimensionalObjectUtils.asTypedList; import static org.hisp.dhis.common.DimensionalObjectUtils.getDimensionalItemIds; -import static org.hisp.dhis.common.IdScheme.NAME; import static org.hisp.dhis.common.IdentifiableObjectUtils.getLocalPeriodIdentifiers; import static org.hisp.dhis.common.IdentifiableObjectUtils.getUids; -import static org.hisp.dhis.common.ValueType.COORDINATE; import static org.hisp.dhis.organisationunit.OrganisationUnit.getParentGraphMap; import static org.hisp.dhis.organisationunit.OrganisationUnit.getParentNameGraphMap; @@ -62,395 +56,231 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.TreeMap; import lombok.RequiredArgsConstructor; import org.hisp.dhis.analytics.AnalyticsSecurityManager; -import org.hisp.dhis.analytics.common.processing.MetadataItemsHandler; -import org.hisp.dhis.analytics.common.scheme.SchemeInfo; -import org.hisp.dhis.analytics.common.scheme.SchemeInfo.Data; -import org.hisp.dhis.analytics.common.scheme.SchemeInfo.Settings; -import org.hisp.dhis.analytics.data.handler.SchemeIdResponseMapper; import org.hisp.dhis.analytics.event.EventQueryParams; -import org.hisp.dhis.analytics.event.EventQueryValidator; import org.hisp.dhis.analytics.orgunit.OrgUnitHelper; -import org.hisp.dhis.analytics.util.AnalyticsOrganisationUnitUtils; import org.hisp.dhis.analytics.util.AnalyticsUtils; import org.hisp.dhis.calendar.Calendar; -import org.hisp.dhis.common.DimensionItemKeywords; import org.hisp.dhis.common.DimensionItemKeywords.Keyword; import org.hisp.dhis.common.DimensionalItemObject; import org.hisp.dhis.common.DimensionalObject; import org.hisp.dhis.common.DisplayProperty; import org.hisp.dhis.common.Grid; -import org.hisp.dhis.common.GridHeader; import org.hisp.dhis.common.IdentifiableObjectUtils; import org.hisp.dhis.common.MetadataItem; -import org.hisp.dhis.common.Pager; import org.hisp.dhis.common.QueryItem; -import org.hisp.dhis.common.RepeatableStageParams; -import org.hisp.dhis.common.SlimPager; -import org.hisp.dhis.common.ValueStatus; -import org.hisp.dhis.common.ValueType; import org.hisp.dhis.option.Option; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.period.PeriodType; import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.User; import org.hisp.dhis.user.UserService; +import org.springframework.stereotype.Component; -/** - * @author Luciano Fiandesio - */ +@Component @RequiredArgsConstructor -public abstract class AbstractAnalyticsService { +public class MetadataItemsHandler { protected final AnalyticsSecurityManager securityManager; - protected final EventQueryValidator queryValidator; - - protected final SchemeIdResponseMapper schemeIdResponseMapper; - protected final UserService userService; /** - * Returns a grid based on the given query. + * Adds meta data values to the given grid based on the given data query parameters. * + * @param grid the {@link Grid}. * @param params the {@link EventQueryParams}. - * @return a {@link Grid}. + * @param keywords the list of {@link Keyword}. */ - protected Grid getGrid(EventQueryParams params) { - // --------------------------------------------------------------------- - // Decide access, add constraints and validate - // --------------------------------------------------------------------- - - securityManager.decideAccessEventQuery(params); - - params = securityManager.withUserConstraints(params); - - queryValidator.validate(params); - - // Keywords and periods are removed in the next step - - List periodKeywords = - params.getDimensions().stream() - .map(DimensionalObject::getDimensionItemKeywords) - .filter( - dimensionItemKeywords -> - dimensionItemKeywords != null && !dimensionItemKeywords.isEmpty()) - .flatMap(dk -> dk.getKeywords().stream()) - .collect(toList()); - - List periods = getPeriods(params); - - params = new EventQueryParams.Builder(params).withStartEndDatesForPeriods().build(); - - // --------------------------------------------------------------------- - // Headers - // --------------------------------------------------------------------- - - Grid grid = createGridWithHeaders(params); - - for (DimensionalObject dimension : params.getDimensions()) { - grid.addHeader( - new GridHeader( - dimension.getDimension(), - dimension.getDimensionDisplayName(), - ValueType.TEXT, - false, - true)); - } - - for (DimensionalObject dimension : periods) { - grid.addHeader( - new GridHeader( - dimension.getDimension(), - dimension.getDimensionDisplayName(), - ValueType.TEXT, - false, - true)); - } - - final DisplayProperty displayProperty = params.getDisplayProperty(); - Map repeatedNames = - params.getItems().stream() - .collect(groupingBy(s -> s.getItem().getDisplayProperty(displayProperty), counting())); + public void addMetadata(Grid grid, EventQueryParams params, List keywords) { + if (!params.isSkipMeta()) { + Map metadata = new HashMap<>(); + Map> optionsPresentInGrid = getItemOptions(grid, params.getItems()); + Set