Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Analytics API: Add Options aggregation and disaggregation[2.42-DHIS2-18368_3] #19558

Draft
wants to merge 37 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5aa2582
cherrypicking, resolve conflicts, part1
d-bernat Dec 18, 2024
8bbb20f
cherrypicking, resolve conflicts, part2
d-bernat Dec 18, 2024
3a85626
cherrypicking, resolve conflicts, part3
d-bernat Dec 20, 2024
123bc05
cherrypicking, resolve conflicts, part3.1
d-bernat Dec 20, 2024
10f0c8d
Revert "cherrypicking, resolve conflicts, part3.1"
d-bernat Dec 20, 2024
b2a5cdc
cherrypicking, resolve conflicts, part3.2
d-bernat Dec 20, 2024
9641e6b
cherrypicking, resolve conflicts, part3.2
d-bernat Dec 23, 2024
36520de
cherrypicking, resolve conflicts, part3.2
d-bernat Dec 23, 2024
30ab376
code cleaning
d-bernat Dec 23, 2024
a6cce87
Merge branch 'master' into 2.42-DHIS2-18368_3
d-bernat Dec 23, 2024
5fd5343
code cleaning
d-bernat Dec 23, 2024
4c48f68
QA issues
d-bernat Dec 23, 2024
55ed40d
QA issues
d-bernat Dec 23, 2024
f5c2da5
QA issues
d-bernat Dec 23, 2024
5bcf118
bugfix unit test
d-bernat Dec 23, 2024
092ab3e
bugfix unit test
d-bernat Dec 23, 2024
ad4b4a8
bugfix unit test
d-bernat Dec 23, 2024
9614438
Merge branch 'master' into 2.42-DHIS2-18368_3
d-bernat Dec 23, 2024
e898458
Merge branch 'master' into 2.42-DHIS2-18368_3
d-bernat Dec 28, 2024
49f4af5
code cleaning
d-bernat Dec 30, 2024
86d704e
request caching
d-bernat Dec 31, 2024
a15edae
Merge branch 'master' into 2.42-DHIS2-18368_3
d-bernat Dec 31, 2024
7e1282f
Merge branch 'master' into 2.42-DHIS2-18368_3
maikelarabori Jan 3, 2025
5fdd521
fix: Small bugs and clean-up [DHIS2-18368]
maikelarabori Jan 6, 2025
cc51c89
fix: Conflict with master [DHIS2-18368]
maikelarabori Jan 6, 2025
6c00bf2
fix: prevent NPE [DHIS2-18368]
maikelarabori Jan 6, 2025
049a9c4
fix: Group by issue [DHIS2-18368]
maikelarabori Jan 6, 2025
542d0f4
fix: Regex/separator for options [DHIS2-18368]
maikelarabori Jan 6, 2025
6aa4be6
refactor: Extract common code [DHIS2-18368]
maikelarabori Jan 7, 2025
4cf8eee
chore: Replace dot by constat [DHIS2-18368]
maikelarabori Jan 7, 2025
baee323
chore: Comment typo [DHIS2-18368]
maikelarabori Jan 7, 2025
3eae096
chore: Comment typo [DHIS2-18368]
maikelarabori Jan 7, 2025
774a948
chore: Replace dot by constat [DHIS2-18368]
maikelarabori Jan 7, 2025
bb64ae8
chore: Code clean-up [DHIS2-18368]
maikelarabori Jan 7, 2025
656c5c2
fix: Conflicts with master [DHIS2-18368]
maikelarabori Jan 7, 2025
3282d23
fix: Merge issue [DHIS2-18368]
maikelarabori Jan 7, 2025
466f8d8
fix: Sonar issue [DHIS2-18368]
maikelarabori Jan 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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;

import java.util.Set;
import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
public class OptionSetSelection {
private String optionSetUid;
private Set<String> options;
private OptionSetSelectionMode optionSetSelectionMode;

@Override
public String toString() {
return "OptionSetSelection{"
+ "optionSetUid='"
+ optionSetUid
+ '\''
+ ", options="
+ options
+ ", optionSetSelectionMode="
+ optionSetSelectionMode
+ '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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;

import static org.apache.commons.lang3.StringUtils.EMPTY;

import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class OptionSetSelectionCriteria {
private Map<String, OptionSetSelection> optionSetSelections;

@Override
public String toString() {
if (optionSetSelections == null || optionSetSelections.isEmpty()) {
return EMPTY;
}

return "OptionSetSelectionCriteria{" + "optionSetSelections=" + optionSetSelections + '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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;

import java.util.Arrays;
import java.util.List;

/** The selection modes for dimension items with {@link OptionSet}. */
public enum OptionSetSelectionMode {
/**
* All options in an option set are chosen and aggregated into a single column. This selection is
* relative, so any new options added to the option set are included. This is the default mode.
*/
AGGREGATED,
/**
* All options in an option set are chosen and displayed as data items. This selection is
* relative, so any new options added to the option set are included.
*/
DISAGGREGATED;

public static List<String> getOptionSetSelectionModes() {
return Arrays.stream(OptionSetSelectionMode.values()).map(Enum::toString).toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.substringAfterLast;
import static org.hisp.dhis.analytics.OptionSetSelectionMode.AGGREGATED;
import static org.hisp.dhis.common.DimensionalObject.DIMENSION_NAME_SEP;
import static org.hisp.dhis.common.DimensionalObject.DIMENSION_SEP;
import static org.hisp.dhis.common.DimensionalObject.ITEM_SEP;
Expand All @@ -57,6 +58,7 @@
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Triple;
import org.hisp.dhis.analytics.OptionSetSelectionMode;
import org.hisp.dhis.common.comparator.ObjectStringValueComparator;
import org.hisp.dhis.dataelement.DataElementOperand;
import org.hisp.dhis.eventvisualization.Attribute;
Expand All @@ -69,8 +71,6 @@
*/
@NoArgsConstructor(access = PRIVATE)
public class DimensionalObjectUtils {
public static final String COMPOSITE_DIM_OBJECT_ESCAPED_SEP = "\\.";

public static final String COMPOSITE_DIM_OBJECT_PLAIN_SEP = ".";

public static final String TITLE_ITEM_SEP = ", ";
Expand All @@ -81,12 +81,20 @@ public class DimensionalObjectUtils {

public static final String COL_SEP = " ";

/**
* Regex to ignore splitting by ";" inside square brackets []. ie:
* dx:FTRrcoaog83;WSGAb5XwJ3Y.QFX1FLWBwtq.R3ShQczKnI9[l8S7SjnQ58G;rexqxNDqUKg], splits into
* FTRrcoaog83 and WSGAb5XwJ3Y.QFX1FLWBwtq.R3ShQczKnI9[l8S7SjnQ58G;rexqxNDqUKg]
*/
private static final Pattern DX_REGEX_PATTERN = Pattern.compile(";(?![^\\(\\[]*[\\]\\)])");

/**
* Matching data element operand, program data element, program attribute, data set reporting rate
* metric.
* metric. ie: Luqe6ps5KZ9.uTLkjHWtSL8.R0jROOT3zni-AGGREGATED
*/
private static final Pattern COMPOSITE_DIM_OBJECT_PATTERN =
Pattern.compile("(?<id1>\\w+)\\.(?<id2>\\w+|\\*)(\\.(?<id3>\\w+|\\*))?");
Pattern.compile(
"(?<id1>\\w+)\\.(?<id2>\\w+|\\*)(\\.(?<id3>\\w+|\\*))?(\\[(?<list>[^\\]]*?)\\])?(-(?<suffix>AGGREGATED|DISAGGREGATED)?)?");

private static final Set<QueryOperator> IGNORED_OPERATORS =
Set.of(QueryOperator.LIKE, QueryOperator.IN, QueryOperator.SW, QueryOperator.EW);
Expand Down Expand Up @@ -359,6 +367,21 @@ public static String getDimensionFromParam(String param) {
return param.split(DIMENSION_NAME_SEP).length > 0 ? param.split(DIMENSION_NAME_SEP)[0] : param;
}

/**
* Retrieves the value from the given dimension param. Returns the part of the string after the
* dimension name separator, or the whole string if the separator is not present. ie:
* dx:WSGAb5XwJ3Y.QFX1FLWBwtq, becomes WSGAb5XwJ3Y.QFX1FLWBwtq
*
* @param param the parameter.
*/
public static String getValueFromDimensionParam(String param) {
if (param == null) {
return null;
}

return param.split(DIMENSION_NAME_SEP).length > 1 ? param.split(DIMENSION_NAME_SEP)[1] : param;
}

/**
* Retrieves the dimension options from the given string. Looks for the part succeeding the
* dimension name separator, if exists, splits the string part on the option separator and returns
Expand All @@ -374,8 +397,7 @@ public static List<String> getDimensionItemsFromParam(String param) {
// Extracts dimension items by removing dimension name and separator
String dimensionItems = param.substring(param.indexOf(DIMENSION_NAME_SEP) + 1);

// Returns them as List<String>
return Arrays.asList(dimensionItems.split(OPTION_SEP));
return Arrays.asList(DX_REGEX_PATTERN.split(dimensionItems));
}

return new ArrayList<>();
Expand Down Expand Up @@ -521,20 +543,78 @@ public static boolean isCompositeDimensionalObject(String expression) {
* @param compositeItem the composite dimension object identifier.
* @return the first identifier, or null if not a valid composite identifier or no match.
*/
public static String getFirstIdentifer(String compositeItem) {
public static String getFirstIdentifier(String compositeItem) {
if (compositeItem == null) {
return null;
}

Matcher matcher = COMPOSITE_DIM_OBJECT_PATTERN.matcher(compositeItem);
return matcher.matches() ? matcher.group(1) : null;
return matcher.matches() ? matcher.group("id1") : null;
}

/**
* Returns the second identifier in a composite dimension object identifier.
*
* @param compositeItem the composite dimension object identifier.
* @return the second identifier, or null if not a valid composite identifier or no match.
* @return the second identifier, or null if thr composite identifier is not valid or do not
* match.
*/
public static String getSecondIdentifer(String compositeItem) {
public static String getSecondIdentifier(String compositeItem) {
if (compositeItem == null) {
return null;
}

Matcher matcher = COMPOSITE_DIM_OBJECT_PATTERN.matcher(compositeItem);
return matcher.matches() ? matcher.group(2) : null;
return matcher.matches() ? matcher.group("id2") : null;
}

/**
* Returns the third identifier in a composite dimension object identifier.
*
* @param compositeItem the composite dimension object identifier.
* @return the third identifier, or null if thr composite identifier is not valid or do not match.
*/
public static String getThirdIdentifier(String compositeItem) {
if (compositeItem == null) {
return null;
}

Matcher matcher = COMPOSITE_DIM_OBJECT_PATTERN.matcher(compositeItem);

return matcher.matches() ? matcher.group("id3") : null;
}

/**
* Based on the given argument, it will return the associated {@link OptionSetSelectionMode}. If
* the argument is null, or no association is found, it returns the default mode "AGGREGATED".
*
* @param composedOptionSetId the full option set dimension, ie:
* Luqe6ps5KZ9.uTLkjHWtSL8.R0jROOT3zni-AGGREGATED.
* @return the respective {@link OptionSetSelectionMode}, or the default mode.
*/
public static OptionSetSelectionMode getOptionSetSelectionMode(String composedOptionSetId) {
if (composedOptionSetId == null) {
return null;
}

Matcher matcher = COMPOSITE_DIM_OBJECT_PATTERN.matcher(composedOptionSetId);
if (matcher.matches()) {
String suffix = matcher.group("suffix");
return suffix != null ? OptionSetSelectionMode.valueOf(suffix) : AGGREGATED;
}

return AGGREGATED;
}

/**
* Luqe6ps5KZ9.uTLkjHWtSL8.R0jROOT3zni-AGGREGATED
*
* @param optionSetParam
* @return the options specified for the option set param, or null.
*/
public static String getOptionsParam(String optionSetParam) {
Matcher matcher = COMPOSITE_DIM_OBJECT_PATTERN.matcher(optionSetParam);
return matcher.matches() ? matcher.group("list") : null;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public enum ErrorCode {
E1126("Category combo {0} cannot combine more than {1} categories, but had: {2}"),
E1127("Category {0} cannot have more than {1} options, but had: {2} "),
E1128("Category combo {0} cannot have more than {1} combinations, but requires: {2}"),
E1129("OptionSet selection mode is not valid: `{0}`"),

/* Org unit merge */
E1500("At least two source orgs unit must be specified"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,15 @@ void testGetDataElementOperandIdSchemeCodeMap() {
@Test
void testGetFirstSecondIdentifier() {
assertEquals(
"A123456789A", DimensionalObjectUtils.getFirstIdentifer("A123456789A.P123456789A"));
assertNull(DimensionalObjectUtils.getFirstIdentifer("A123456789A"));
"A123456789A", DimensionalObjectUtils.getFirstIdentifier("A123456789A.P123456789A"));
assertNull(DimensionalObjectUtils.getFirstIdentifier("A123456789A"));
}

@Test
void testGetSecondIdentifier() {
assertEquals(
"P123456789A", DimensionalObjectUtils.getSecondIdentifer("A123456789A.P123456789A"));
assertNull(DimensionalObjectUtils.getSecondIdentifer("A123456789A"));
"P123456789A", DimensionalObjectUtils.getSecondIdentifier("A123456789A.P123456789A"));
assertNull(DimensionalObjectUtils.getSecondIdentifier("A123456789A"));
}

@Test
Expand Down
Loading
Loading