From 0d7c320609f32cac5019678af0cccb81b3ca0d52 Mon Sep 17 00:00:00 2001 From: David Mackessy Date: Fri, 17 Jan 2025 15:26:28 +0000 Subject: [PATCH] feat: Handle Expressions wth source COC refs [DHIS2-18321] --- .../hisp/dhis/expression/ExpressionStore.java | 38 ++++++++ .../CategoryOptionComboMergeService.java | 3 +- ...tadataCategoryOptionComboMergeHandler.java | 25 ++++- .../expression/HibernateExpressionStore.java | 59 ++++++++++++ .../hibernate/HibernateIndicatorStore.java | 1 - .../merge/CategoryOptionComboMergeTest.java | 93 +++++++++++++++++++ 6 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/expression/ExpressionStore.java create mode 100644 dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/HibernateExpressionStore.java diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/expression/ExpressionStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/expression/ExpressionStore.java new file mode 100644 index 000000000000..05ba4709ab08 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/expression/ExpressionStore.java @@ -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 { + + int updateExpressionContaining(String find, String replace); +} diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/CategoryOptionComboMergeService.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/CategoryOptionComboMergeService.java index fb039e26b6e2..c99ad4f42131 100644 --- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/CategoryOptionComboMergeService.java +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/CategoryOptionComboMergeService.java @@ -120,7 +120,8 @@ private void initMergeHandlers() { metadataMergeHandler::handleDataElementOperands, metadataMergeHandler::handleMinMaxDataElements, metadataMergeHandler::handleSmsCodes, - metadataMergeHandler::handleIndicators); + metadataMergeHandler::handleIndicators, + metadataMergeHandler::handleExpressions); dataMergeHandlers = List.of( diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/MetadataCategoryOptionComboMergeHandler.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/MetadataCategoryOptionComboMergeHandler.java index f5649188de95..b32b36aa5fde 100644 --- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/MetadataCategoryOptionComboMergeHandler.java +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/MetadataCategoryOptionComboMergeHandler.java @@ -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; @@ -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} @@ -181,10 +183,11 @@ public void handleSmsCodes(List 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 sources, CategoryOptionCombo target) { log.info("Merging source indicators"); @@ -196,4 +199,20 @@ public void handleIndicators(List 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 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); + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/HibernateExpressionStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/HibernateExpressionStore.java new file mode 100644 index 000000000000..3ee7d3e67eee --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/HibernateExpressionStore.java @@ -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 + 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); + } +} diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/indicator/hibernate/HibernateIndicatorStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/indicator/hibernate/HibernateIndicatorStore.java index 86d87b930d85..6382a9403944 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/indicator/hibernate/HibernateIndicatorStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/indicator/hibernate/HibernateIndicatorStore.java @@ -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; diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionComboMergeTest.java b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionComboMergeTest.java index ce522a6abf6f..2d6b53290a39 100644 --- a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionComboMergeTest.java +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionComboMergeTest.java @@ -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; @@ -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 @@ -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 @@ -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); } @@ -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(