Skip to content

Commit

Permalink
feat: Handle Expressions wth source COC refs [DHIS2-18321]
Browse files Browse the repository at this point in the history
  • Loading branch information
david-mackessy committed Jan 17, 2025
1 parent 067c226 commit 0d7c320
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2004-2025, 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.expression;

import org.hisp.dhis.common.GenericStore;

/**
* @author david mackessy
*/
public interface ExpressionStore extends GenericStore<Expression> {

int updateExpressionContaining(String find, String replace);
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ private void initMergeHandlers() {
metadataMergeHandler::handleDataElementOperands,
metadataMergeHandler::handleMinMaxDataElements,
metadataMergeHandler::handleSmsCodes,
metadataMergeHandler::handleIndicators);
metadataMergeHandler::handleIndicators,
metadataMergeHandler::handleExpressions);

dataMergeHandlers =
List.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.hisp.dhis.datadimensionitem.DataDimensionItemStore;
import org.hisp.dhis.dataelement.DataElementOperand;
import org.hisp.dhis.dataelement.DataElementOperandStore;
import org.hisp.dhis.expression.ExpressionStore;
import org.hisp.dhis.indicator.IndicatorStore;
import org.hisp.dhis.minmax.MinMaxDataElement;
import org.hisp.dhis.minmax.MinMaxDataElementStore;
Expand Down Expand Up @@ -68,6 +69,7 @@ public class MetadataCategoryOptionComboMergeHandler {
private final PredictorStore predictorStore;
private final SMSCommandStore smsCommandStore;
private final IndicatorStore indicatorStore;
private final ExpressionStore expressionStore;

/**
* Remove sources from {@link CategoryOption} and add target to {@link CategoryOption}
Expand Down Expand Up @@ -181,10 +183,11 @@ public void handleSmsCodes(List<CategoryOptionCombo> sources, CategoryOptionComb
}

/**
* Set target to {@link SMSCode}
* Update each Indicator numerator and denominator values, replacing any source ref with the
* target ref.
*
* @param sources to be removed
* @param target to add
* @param sources to be replaced
* @param target to replace source refs
*/
public void handleIndicators(List<CategoryOptionCombo> sources, CategoryOptionCombo target) {
log.info("Merging source indicators");
Expand All @@ -196,4 +199,20 @@ public void handleIndicators(List<CategoryOptionCombo> sources, CategoryOptionCo

log.info("{} indicators updated", totalUpdates);
}

/**
* Update each Expression expression value, replacing any source ref with the target ref.
*
* @param sources to be replaced
* @param target to replace source refs
*/
public void handleExpressions(List<CategoryOptionCombo> sources, CategoryOptionCombo target) {
log.info("Merging source expressions");
int totalUpdates = 0;
for (CategoryOptionCombo source : sources) {
totalUpdates += expressionStore.updateExpressionContaining(source.getUid(), target.getUid());
}

log.info("{} expressions updated", totalUpdates);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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.expression;

import jakarta.persistence.EntityManager;
import org.hisp.dhis.hibernate.HibernateGenericStore;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

/**
* @author david mackessy
*/
@Repository
public class HibernateExpressionStore extends HibernateGenericStore<Expression>
implements org.hisp.dhis.expression.ExpressionStore {

public HibernateExpressionStore(
EntityManager entityManager, JdbcTemplate jdbcTemplate, ApplicationEventPublisher publisher) {
super(entityManager, jdbcTemplate, publisher, Expression.class, false);
}

@Override
public int updateExpressionContaining(String find, String replace) {
String sql =
"""
update expression
set expression = replace(expression, '%s', '%s')
where expression like '%s';
"""
.formatted(find, replace, "%" + find + "%");
return jdbcTemplate.update(sql);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import org.hisp.dhis.indicator.IndicatorStore;
import org.hisp.dhis.indicator.IndicatorType;
import org.hisp.dhis.security.acl.AclService;
import org.hisp.dhis.system.util.SqlUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class CategoryOptionComboMergeTest extends ApiTest {
private RestApiActions dataValueSetActions;
private RestApiActions indicatorActions;
private RestApiActions indicatorTypeActions;
private RestApiActions validationRuleActions;
private UserActions userActions;
private LoginActions loginActions;
private String sourceUid1;
Expand All @@ -96,6 +97,7 @@ public void before() {
visualizationActions = new RestApiActions("visualizations");
indicatorActions = new RestApiActions("indicators");
indicatorTypeActions = new RestApiActions("indicatorTypes");
validationRuleActions = new RestApiActions("validationRules");
loginActions.loginAsSuperUser();

// add user with required merge auth
Expand Down Expand Up @@ -609,6 +611,53 @@ void indicatorsNumeratorDenominatorTest() {
checkIndicatorValues(5, indicator5, "randomUID1", "randomUID2", "randomUID3", "randomUID4");
}

@Test
@DisplayName(
"Expressions with COC source refs in their expression are updated with target COC ref")
void expressionTest() {
// given
maintenanceApiActions
.post("categoryOptionComboUpdate", new QueryParamsBuilder().build())
.validateStatus(204);

// get cat opt combo uids for sources and target, after generating
sourceUid1 = getCocWithOptions("1A", "2A");
sourceUid2 = getCocWithOptions("1B", "2B");
targetUid = getCocWithOptions("3A", "4B");

// indicators with mix of source COC in numerator, denominator
String validationRule1 =
setupExpressionInValidationRule("1", sourceUid1, "leftSide2", "rightSide1", "rightSide2");
String validationRule2 =
setupExpressionInValidationRule("2", "leftSide1", "leftSide2", "rightSide1", sourceUid2);
String validationRule3 =
setupExpressionInValidationRule("3", sourceUid1, sourceUid1, sourceUid2, "rightSide2");
String validationRule4 =
setupExpressionInValidationRule("4", targetUid, "leftSide2", "rightSide1", "rightSide2");
String validationRule5 =
setupExpressionInValidationRule("5", "leftSide1", "leftSide2", "rightSide1", "rightSide2");

// when
ValidatableResponse response =
categoryOptionComboApiActions.post("merge", getMergeBody("DISCARD")).validate();

// then
response
.statusCode(200)
.body("httpStatus", equalTo("OK"))
.body("response.mergeReport.message", equalTo("CategoryOptionCombo merge complete"))
.body("response.mergeReport.mergeErrors", empty())
.body("response.mergeReport.mergeType", equalTo("CategoryOptionCombo"))
.body("response.mergeReport.sourcesDeleted", hasItems(sourceUid1, sourceUid2));

// and source COC refs have been replaced with target COC refs
checkExpressionValues(1, validationRule1, targetUid, "leftSide2", "rightSide1", "rightSide2");
checkExpressionValues(2, validationRule2, "leftSide1", "leftSide2", "rightSide1", targetUid);
checkExpressionValues(3, validationRule3, targetUid, targetUid, targetUid, "rightSide2");
checkExpressionValues(4, validationRule4, targetUid, "leftSide2", "rightSide1", "rightSide2");
checkExpressionValues(5, validationRule5, "leftSide1", "leftSide2", "rightSide1", "rightSide2");
}

private void checkIndicatorValues(
int name, String indicator, String num1, String num2, String denom1, String denom2) {
indicatorActions
Expand All @@ -620,6 +669,26 @@ private void checkIndicatorValues(
.body("name", equalTo("test indicator %d".formatted(name)));
}

private void checkExpressionValues(
int name,
String rule,
String leftSide1,
String leftSide2,
String rightSide1,
String rightSide2) {
validationRuleActions
.get("/" + rule)
.validate()
.statusCode(200)
.body(
"leftSide.expression",
equalTo("#{%s.RandomUid01}+#{RandomUid02.%s}".formatted(leftSide1, leftSide2)))
.body(
"rightSide.expression",
equalTo("#{%s.RandomUid03}+#{RandomUid04.%s}".formatted(rightSide1, rightSide2)))
.body("name", equalTo("test val rule %d".formatted(name)));
}

private void setupMetadata() {
metadataActions.importMetadata(metadata()).validateStatus(200);
}
Expand Down Expand Up @@ -689,6 +758,30 @@ private String setupIndicator(
.extractUid();
}

private String setupExpressionInValidationRule(
String name, String leftSide1, String leftSide2, String rightSide1, String rightSide2) {
return validationRuleActions
.post(
"""
{
"name": "test val rule %s",
"leftSide": {
"expression": "#{%s.RandomUid01}+#{RandomUid02.%s}",
"description": "expression 1"
},
"rightSide": {
"expression": "#{%s.RandomUid03}+#{RandomUid04.%s}",
"description": "expression 2"
},
"operator": "less_than_or_equal_to",
"periodType": "Monthly"
}
"""
.formatted(name, leftSide1, leftSide2, rightSide1, rightSide2))
.validateStatus(201)
.extractUid();
}

private String setupIndicatorType(String name) {
return indicatorTypeActions
.post(
Expand Down

0 comments on commit 0d7c320

Please sign in to comment.