From 97f5a70bc174d8fcc16b85bd1a2da1cb152fc479 Mon Sep 17 00:00:00 2001 From: David Mackessy <131455290+david-mackessy@users.noreply.github.com> Date: Mon, 11 Dec 2023 07:51:05 +0000 Subject: [PATCH 1/2] feat: Add feature that allows merging Indicator Types [DHIS2-14458] (#15824) * feat: Initial indicator type merge work [DHIS2-14458] * feat: small cleanup [DHIS2-14458] * feat: Refactor to use merge report [DHIS2-14458] * feat: Start to generify approach [DHIS2-14458] * feat: Use generic processor to standardise merges [DHIS2-14458] * feat: Add validator tests [DHIS2-14458] * feat: Refactor and clean up [DHIS2-14458] * feat: Remove commented code [DHIS2-14458] * feat: Add more tests and use UID [DHIS2-14458] * feat: Add more e2e tests [DHIS2-14458] * feat: format [DHIS2-14458] * feat: remove metadata from setup [DHIS2-14458] * feat: Rename existing MergeService to avoid confusion [DHIS2-14458] * feat: Initial addressing of comments [DHIS2-14458] * feat: fix e2e and int tests [DHIS2-14458] * feat: fix setters in tests [DHIS2-14458] * feat: fix tests [DHIS2-14458] * feat: remove logging of params [DHIS2-14458] * feat: Clean up [DHIS2-14458] * feat: format [DHIS2-14458] * feat: Add global locale text mapping. Revert previous name change [DHIS2-14458] * feat: Rejigged wiring of components [DHIS2-14458] --- .../main/java/org/hisp/dhis/common/UID.java | 2 + .../hisp/dhis/feedback/ConflictException.java | 2 + .../org/hisp/dhis/feedback/ErrorCode.java | 6 + .../dhis/feedback/ErrorMessageContainer.java | 40 ++ .../org/hisp/dhis/feedback/MergeReport.java | 74 +++ .../hisp/dhis/indicator/IndicatorService.java | 4 + .../hisp/dhis/indicator/IndicatorStore.java | 2 + .../hisp/dhis/merge}/DataMergeStrategy.java | 2 +- .../java/org/hisp/dhis/merge/MergeParams.java | 51 ++ .../org/hisp/dhis/merge/MergeProcessor.java | 74 +++ .../org/hisp/dhis/merge/MergeRequest.java | 63 +++ .../org/hisp/dhis/merge/MergeService.java | 62 +++ .../java/org/hisp/dhis/merge/MergeType.java | 39 ++ .../org/hisp/dhis/security/Authorities.java | 1 + .../IndicatorTypeMergeProcessor.java | 49 ++ .../indicator/IndicatorTypeMergeService.java | 161 ++++++ .../dhis/merge/orgunit/OrgUnitMergeQuery.java | 1 + .../merge/orgunit/OrgUnitMergeRequest.java | 1 + .../handler/DataOrgUnitMergeHandler.java | 2 +- .../dimension/DefaultDimensionService.java | 6 +- .../indicator/DefaultIndicatorService.java | 12 + .../hibernate/HibernateIndicatorStore.java | 10 + .../dhis/preheat/DefaultPreheatService.java | 12 +- .../src/main/resources/i18n_global.properties | 1 + .../dxf2/gml/DefaultGmlImportService.java | 9 +- .../DefaultObjectBundleService.java | 10 +- .../hooks/AbstractObjectBundleHook.java | 4 +- .../dhis/dxf2/webmessage/WebMessageUtils.java | 11 + .../responses/MergeWebResponse.java | 50 ++ ....java => DefaultMetadataMergeService.java} | 23 +- ...geParams.java => MetadataMergeParams.java} | 10 +- ...Service.java => MetadataMergeService.java} | 6 +- .../commons/collection/CollectionUtils.java | 10 + .../commons/util/CollectionUtilsTest.java | 8 + .../org/hisp/dhis/DhisConvenienceTest.java | 2 + .../dhis/merge/IndicatorTypeMergeTest.java | 469 ++++++++++++++++++ ...est.java => MetadataMergeServiceTest.java} | 27 +- .../IndicatorTypeMergeServiceTest.java | 388 +++++++++++++++ .../orgunit/OrgUnitMergeServiceTest.java | 1 + .../handler/DataOrgUnitMergeHandlerTest.java | 2 +- .../controller/AbstractCrudController.java | 4 +- .../controller/CrudControllerAdvice.java | 32 ++ .../indicator/IndicatorTypeController.java | 37 +- .../controller/mapping/MapController.java | 6 +- .../controller/user/UserController.java | 5 +- 45 files changed, 1730 insertions(+), 61 deletions(-) create mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorMessageContainer.java create mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/MergeReport.java rename dhis-2/{dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit => dhis-api/src/main/java/org/hisp/dhis/merge}/DataMergeStrategy.java (97%) create mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeParams.java create mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeProcessor.java create mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeRequest.java create mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeService.java create mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeType.java create mode 100644 dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/indicator/IndicatorTypeMergeProcessor.java create mode 100644 dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/indicator/IndicatorTypeMergeService.java create mode 100644 dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/webmessage/responses/MergeWebResponse.java rename dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/{DefaultMergeService.java => DefaultMetadataMergeService.java} (86%) rename dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/{MergeParams.java => MetadataMergeParams.java} (88%) rename dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/{MergeService.java => MetadataMergeService.java} (90%) create mode 100644 dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/IndicatorTypeMergeTest.java rename dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dxf2/metadata/{MergeServiceTest.java => MetadataMergeServiceTest.java} (89%) create mode 100644 dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/indicator/IndicatorTypeMergeServiceTest.java diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UID.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UID.java index bc68e8d35b93..e14144b5a372 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UID.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UID.java @@ -29,6 +29,7 @@ import static java.util.stream.Collectors.toUnmodifiableSet; +import com.fasterxml.jackson.annotation.JsonCreator; import java.io.Serializable; import java.util.Collection; import java.util.List; @@ -69,6 +70,7 @@ public String toString() { return value; } + @JsonCreator public static UID of(@Nonnull String value) { return new UID(value); } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ConflictException.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ConflictException.java index ec1a79b5a4a0..608ce45821c6 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ConflictException.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ConflictException.java @@ -60,6 +60,8 @@ public static V on( @Setter private ObjectReport objectReport; + @Setter private MergeReport mergeReport; + public ConflictException(String message) { super(message); this.code = ErrorCode.E1004; diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorCode.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorCode.java index b0f9e992a6f1..83a3151df04a 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorCode.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorCode.java @@ -88,6 +88,12 @@ public enum ErrorCode { E1522("User `{0}` is not allowed to move organisation `{1}` unit from parent `{2}`"), E1523("User `{0}` is not allowed to move organisation `{1}` unit to parent `{2}`"), + /* Indicator Type merge */ + E1530("At least one source indicator type must be specified"), + E1531("Target indicator type must be specified"), + E1532("Target indicator type cannot be a source indicator type"), + E1533("{0} indicator type does not exist: `{1}`"), + /* Data */ E2000("Query parameters cannot be null"), E2001("At least one data element, data set or data element group must be specified"), diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorMessageContainer.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorMessageContainer.java new file mode 100644 index 000000000000..ceded8271d74 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorMessageContainer.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2004-2023, 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.feedback; + +/** + * An ADT interface for a collection of {@link ErrorMessage}s. + * + * @author david mackessy + */ +public interface ErrorMessageContainer { + + boolean hasErrorMessages(); + + void addErrorMessage(ErrorMessage errorMessage); +} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/MergeReport.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/MergeReport.java new file mode 100644 index 000000000000..9d7c6dab1244 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/MergeReport.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2004-2023, 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.feedback; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.Data; +import org.hisp.dhis.merge.MergeType; + +/** + * Class representing the state of a merge
+ * + * + * + * @author david mackessy + */ +@Data +public class MergeReport implements ErrorMessageContainer { + + @JsonProperty private final List mergeErrors = new ArrayList<>(); + @JsonProperty private MergeType mergeType; + @JsonProperty private Set sourcesDeleted = new HashSet<>(); + @JsonProperty private String message; + + public MergeReport(MergeType mergeType) { + this.mergeType = mergeType; + } + + @Override + public boolean hasErrorMessages() { + return !mergeErrors.isEmpty(); + } + + @Override + public void addErrorMessage(ErrorMessage errorMessage) { + mergeErrors.add(errorMessage); + } + + public void addDeletedSource(String uid) { + sourcesDeleted.add(uid); + } +} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorService.java index a733b14d4b75..18e95f692d2b 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorService.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorService.java @@ -55,6 +55,8 @@ public interface IndicatorService { List getIndicatorsWithDataSets(); + List getAssociatedIndicators(List indicatorTypes); + // ------------------------------------------------------------------------- // IndicatorType // ------------------------------------------------------------------------- @@ -71,6 +73,8 @@ public interface IndicatorService { List getAllIndicatorTypes(); + List getIndicatorTypesByUid(List uids); + // ------------------------------------------------------------------------- // IndicatorGroup // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorStore.java index 132ef7f0835f..74fe65136652 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/indicator/IndicatorStore.java @@ -41,4 +41,6 @@ public interface IndicatorStore extends IdentifiableObjectStore { List getIndicatorsWithoutGroups(); List getIndicatorsWithDataSets(); + + List getAssociatedIndicators(List indicatorTypes); } diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/DataMergeStrategy.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/DataMergeStrategy.java similarity index 97% rename from dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/DataMergeStrategy.java rename to dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/DataMergeStrategy.java index 31a2b5d06dcd..f979f4925738 100644 --- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/DataMergeStrategy.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/DataMergeStrategy.java @@ -25,7 +25,7 @@ * (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.merge.orgunit; +package org.hisp.dhis.merge; /** * Enum for merge strategies. diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeParams.java new file mode 100644 index 000000000000..3a41fe66a4e9 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeParams.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2004-2023, 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.merge; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Set; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hisp.dhis.common.UID; + +/** + * Encapsulation of web API merge params. Contains source {@link UID}s to be merged and a target + * {@link UID} to be merged in to. Also indicates whether sources should be deleted or not.
+ * All {@link UID}s should be verified. + * + * @author david mackessy + */ +@Data +@NoArgsConstructor +public class MergeParams { + @JsonProperty private Set sources; + + @JsonProperty private UID target; + + @JsonProperty private boolean deleteSources; +} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeProcessor.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeProcessor.java new file mode 100644 index 000000000000..277a5d7d3613 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeProcessor.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2004-2023, 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.merge; + +import javax.annotation.Nonnull; +import org.hisp.dhis.common.IdentifiableObject; +import org.hisp.dhis.feedback.ConflictException; +import org.hisp.dhis.feedback.MergeReport; + +/** + * Interface with default method used to process merging of {@link IdentifiableObject}s uniformly, + * in a standardised fashion. It requires an implementation of {@link MergeService}, which will + * process the merging for its required use case.
+ * It essentially calls each method of the {@link MergeService} in the following order:
+ * + *
    + *
  1. validate + *
  2. merge + *
+ */ +public interface MergeProcessor { + + /** + * @return implemented {@link MergeService} to process merge + */ + MergeService getMergeService(); + + /** + * Processes a merge in full, using the implemented {@link MergeService} retrieved. + * + * @param mergeParams {@link MergeParams} to process + * @param mergeType {@link MergeType} + * @return updated {@link MergeReport} with any errors + */ + default MergeReport processMerge(@Nonnull MergeParams mergeParams, @Nonnull MergeType mergeType) + throws ConflictException { + MergeReport mergeReport = new MergeReport(mergeType); + + MergeRequest mergeRequest = getMergeService().validate(mergeParams, mergeReport); + if (mergeReport.hasErrorMessages()) + throw new ConflictException("Merge validation error").setMergeReport(mergeReport); + + MergeReport report = getMergeService().merge(mergeRequest, mergeReport); + if (report.hasErrorMessages()) + throw new ConflictException("Merge error").setMergeReport(mergeReport); + + return report; + } +} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeRequest.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeRequest.java new file mode 100644 index 000000000000..f23d3d9fe700 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeRequest.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2004-2023, 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.merge; + +import java.util.HashSet; +import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; +import org.hisp.dhis.common.IdentifiableObject; +import org.hisp.dhis.common.UID; + +/** + * Encapsulation of a merge request. Contains source {@link UID}s that extend {@link + * IdentifiableObject} to be merged into a target of the same type. Also indicates whether sources + * should be deleted. + * + * @author david mackessy + */ +@Builder +@ToString +@AllArgsConstructor +public class MergeRequest { + @Builder.Default private final Set sources = new HashSet<>(); + + @Getter private final UID target; + + @Getter private final boolean deleteSources; + + public Set getSources() { + return Set.copyOf(sources); + } + + public static MergeRequest empty() { + return MergeRequest.builder().build(); + } +} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeService.java new file mode 100644 index 000000000000..913503f4dea1 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeService.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2004-2023, 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.merge; + +import javax.annotation.Nonnull; +import org.hisp.dhis.common.IdentifiableObject; +import org.hisp.dhis.feedback.MergeReport; +import org.springframework.transaction.annotation.Transactional; + +/** + * Interface for merging {@link IdentifiableObject}s + * + * @author david mackessy + */ +public interface MergeService { + + /** + * This method transforms a {@link MergeParams} to a {@link MergeRequest}. If there are any + * errors/issues with the params then the {@link MergeReport} should be updated. + * + * @param params params to transform to {@link MergeRequest} + * @param mergeReport report to be updated if any issues/errors with the {@link MergeParams} + * @return {@link MergeRequest} + */ + MergeRequest validate(@Nonnull MergeParams params, @Nonnull MergeReport mergeReport); + + /** + * This method merges a {@link MergeRequest}. If there are any errors/issues with the {@link + * MergeRequest} then the {@link MergeReport} should be updated. + * + * @param request request to be merged + * @param mergeReport report to be updated if any issues/errors with the {@link MergeRequest} + * @return {@link MergeReport} + */ + @Transactional + MergeReport merge(@Nonnull MergeRequest request, @Nonnull MergeReport mergeReport); +} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeType.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeType.java new file mode 100644 index 000000000000..aaf13584a47f --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeType.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2004-2023, 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.merge; + +/** + * Enum for merge type. + * + * @author david mackessy + */ +public enum MergeType { + ORG_UNIT, + + INDICATOR_TYPE +} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/security/Authorities.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/security/Authorities.java index b6af1340e487..c893b9a77e01 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/security/Authorities.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/security/Authorities.java @@ -54,6 +54,7 @@ public enum Authorities { F_ORGANISATIONUNIT_MOVE, F_ORGANISATION_UNIT_SPLIT, F_ORGANISATION_UNIT_MERGE, + F_INDICATOR_TYPE_MERGE, F_INSERT_CUSTOM_JS_CSS, F_VIEW_UNAPPROVED_DATA, F_USER_VIEW, diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/indicator/IndicatorTypeMergeProcessor.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/indicator/IndicatorTypeMergeProcessor.java new file mode 100644 index 000000000000..4f64b6acc4a9 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/indicator/IndicatorTypeMergeProcessor.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2004-2023, 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.merge.indicator; + +import lombok.RequiredArgsConstructor; +import org.hisp.dhis.merge.MergeProcessor; +import org.hisp.dhis.merge.MergeService; +import org.springframework.stereotype.Component; + +/** + * Implementation of {@link MergeProcessor} that currently only uses its default method. + * + * @author david mackessy + */ +@Component +@RequiredArgsConstructor +public class IndicatorTypeMergeProcessor implements MergeProcessor { + private final MergeService indicatorTypeMergeService; + + @Override + public MergeService getMergeService() { + return indicatorTypeMergeService; + } +} diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/indicator/IndicatorTypeMergeService.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/indicator/IndicatorTypeMergeService.java new file mode 100644 index 000000000000..4950a8d9176b --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/indicator/IndicatorTypeMergeService.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2004-2023, 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.merge.indicator; + +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import javax.annotation.Nonnull; +import lombok.RequiredArgsConstructor; +import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.commons.collection.CollectionUtils; +import org.hisp.dhis.feedback.ErrorCode; +import org.hisp.dhis.feedback.ErrorMessage; +import org.hisp.dhis.feedback.MergeReport; +import org.hisp.dhis.indicator.Indicator; +import org.hisp.dhis.indicator.IndicatorService; +import org.hisp.dhis.indicator.IndicatorType; +import org.hisp.dhis.merge.MergeParams; +import org.hisp.dhis.merge.MergeRequest; +import org.hisp.dhis.merge.MergeService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Main class for indicator type merge. + * + * @author david mackessy + */ +@Service +@RequiredArgsConstructor +public class IndicatorTypeMergeService implements MergeService { + + private final IndicatorService indicatorService; + private final IdentifiableObjectManager idObjectManager; + + @Override + public MergeRequest validate(@Nonnull MergeParams params, @Nonnull MergeReport mergeReport) { + // sources + Set sources = new HashSet<>(); + Optional.ofNullable(params.getSources()) + .filter(CollectionUtils::isNotEmpty) + .ifPresentOrElse( + ids -> getSourcesAndVerify(ids, mergeReport, sources), + () -> mergeReport.addErrorMessage(new ErrorMessage(ErrorCode.E1530))); + + // target + Optional target = Optional.ofNullable(params.getTarget()); + checkIsTargetInSources(sources, target, mergeReport); + + return target + .map(t -> getTargetAndVerify(t, mergeReport, sources, params)) + .orElseGet( + () -> { + mergeReport.addErrorMessage(new ErrorMessage(ErrorCode.E1531)); + return MergeRequest.empty(); + }); + } + + @Override + @Transactional + public MergeReport merge(@Nonnull MergeRequest request, @Nonnull MergeReport mergeReport) { + List sources = + indicatorService.getIndicatorTypesByUid(UID.toValueList(request.getSources())); + IndicatorType target = indicatorService.getIndicatorType(request.getTarget().getValue()); + reassignIndicatorAssociations(target, sources); + + if (request.isDeleteSources()) handleDeleteSources(sources, mergeReport); + + return mergeReport; + } + + private void checkIsTargetInSources( + Set sources, Optional target, MergeReport mergeReport) { + target.ifPresent( + t -> { + if (sources.contains(t)) mergeReport.addErrorMessage(new ErrorMessage(ErrorCode.E1532)); + }); + } + + private void handleDeleteSources(List sources, MergeReport mergeReport) { + for (IndicatorType source : sources) { + mergeReport.addDeletedSource(source.getUid()); + idObjectManager.delete(source); + } + } + + /** + * Retrieves the {@link IndicatorType} with the given identifier. If a valid {@link IndicatorType} + * is not found then an empty {@link Optional} is returned and an error is added to the report. + * + * @param uid the indicator type identifier + * @return {@link Optional} + */ + private Optional getAndVerifyIndicatorType( + UID uid, MergeReport mergeReport, String indType) { + return Optional.ofNullable(idObjectManager.get(IndicatorType.class, uid.getValue())) + .map(it -> UID.of(it.getUid())) + .or( + () -> { + mergeReport.addErrorMessage(new ErrorMessage(ErrorCode.E1533, indType, uid)); + return Optional.empty(); + }); + } + + /** + * All {@link Indicator} associations with source {@link IndicatorType}s are reassigned to the + * target {@link IndicatorType} + * + * @param target {@link IndicatorType} for reassignments + * @param sources List of {@link IndicatorType} used to retrieve related {@link Indicator}s + */ + private void reassignIndicatorAssociations(IndicatorType target, List sources) { + List associatedIndicators = indicatorService.getAssociatedIndicators(sources); + associatedIndicators.forEach(ind -> ind.setIndicatorType(target)); + } + + private void getSourcesAndVerify(Set uids, MergeReport report, Set indicatorTypes) { + uids.forEach( + uid -> getAndVerifyIndicatorType(uid, report, "Source").ifPresent(indicatorTypes::add)); + } + + private MergeRequest getTargetAndVerify( + UID target, MergeReport report, Set sources, MergeParams params) { + return getAndVerifyIndicatorType(target, report, "Target") + .map( + t -> + MergeRequest.builder() + .sources(sources) + .target(t) + .deleteSources(params.isDeleteSources()) + .build()) + .orElse(MergeRequest.empty()); + } +} diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/OrgUnitMergeQuery.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/OrgUnitMergeQuery.java index 7346aba2bcac..f87288c46a85 100644 --- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/OrgUnitMergeQuery.java +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/OrgUnitMergeQuery.java @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.List; import lombok.Data; +import org.hisp.dhis.merge.DataMergeStrategy; /** * Encapsulation of a web API request for org unit merge. diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/OrgUnitMergeRequest.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/OrgUnitMergeRequest.java index d994fcca680d..321c572d1c30 100644 --- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/OrgUnitMergeRequest.java +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/OrgUnitMergeRequest.java @@ -35,6 +35,7 @@ import java.util.Set; import lombok.Getter; import org.hisp.dhis.common.IdentifiableObjectUtils; +import org.hisp.dhis.merge.DataMergeStrategy; import org.hisp.dhis.organisationunit.OrganisationUnit; /** diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/handler/DataOrgUnitMergeHandler.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/handler/DataOrgUnitMergeHandler.java index dd5a36169d38..d0d0b6cd4f9b 100644 --- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/handler/DataOrgUnitMergeHandler.java +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/orgunit/handler/DataOrgUnitMergeHandler.java @@ -35,7 +35,7 @@ import org.hisp.dhis.dataapproval.DataApprovalAuditService; import org.hisp.dhis.dataset.DataSetService; import org.hisp.dhis.datavalue.DataValueAuditService; -import org.hisp.dhis.merge.orgunit.DataMergeStrategy; +import org.hisp.dhis.merge.DataMergeStrategy; import org.hisp.dhis.merge.orgunit.OrgUnitMergeRequest; import org.hisp.dhis.minmax.MinMaxDataElementService; import org.hisp.dhis.validation.ValidationResultService; diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dimension/DefaultDimensionService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dimension/DefaultDimensionService.java index 9b6332826c3b..1ffc22377c5b 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dimension/DefaultDimensionService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dimension/DefaultDimensionService.java @@ -113,7 +113,7 @@ import org.hisp.dhis.program.ProgramIndicator; import org.hisp.dhis.program.ProgramStage; import org.hisp.dhis.program.ProgramTrackedEntityAttributeDimensionItem; -import org.hisp.dhis.schema.MergeService; +import org.hisp.dhis.schema.MetadataMergeService; import org.hisp.dhis.security.acl.AclService; import org.hisp.dhis.trackedentity.TrackedEntityAttribute; import org.hisp.dhis.trackedentity.TrackedEntityAttributeDimension; @@ -142,7 +142,7 @@ public class DefaultDimensionService implements DimensionService { private final CurrentUserService currentUserService; - private final MergeService mergeService; + private final MetadataMergeService metadataMergeService; private final DataDimensionExtractor dataDimensionExtractor; @@ -328,7 +328,7 @@ public DimensionalObject getDimensionalObjectCopy(String uid, boolean filterCanR if (dimension == null) { throw new NotFoundException("Dimension does not exist: " + uid); } - BaseDimensionalObject copy = mergeService.clone(dimension); + BaseDimensionalObject copy = metadataMergeService.clone(dimension); if (filterCanRead) { User user = currentUserService.getCurrentUser(); diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/indicator/DefaultIndicatorService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/indicator/DefaultIndicatorService.java index 259738329df4..04bd1eeff0f9 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/indicator/DefaultIndicatorService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/indicator/DefaultIndicatorService.java @@ -111,6 +111,12 @@ public List getIndicatorsWithDataSets() { return indicatorStore.getIndicatorsWithDataSets(); } + @Override + @Transactional(readOnly = true) + public List getAssociatedIndicators(List indicatorTypes) { + return indicatorStore.getAssociatedIndicators(indicatorTypes); + } + // ------------------------------------------------------------------------- // IndicatorType // ------------------------------------------------------------------------- @@ -153,6 +159,12 @@ public List getAllIndicatorTypes() { return indicatorTypeStore.getAll(); } + @Override + @Transactional(readOnly = true) + public List getIndicatorTypesByUid(List uids) { + return indicatorTypeStore.getByUid(uids); + } + // ------------------------------------------------------------------------- // IndicatorGroup // ------------------------------------------------------------------------- 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 606b72c4d247..8dc20e0f7644 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 @@ -29,9 +29,11 @@ import java.util.List; import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; import org.hisp.dhis.common.hibernate.HibernateIdentifiableObjectStore; import org.hisp.dhis.indicator.Indicator; import org.hisp.dhis.indicator.IndicatorStore; +import org.hisp.dhis.indicator.IndicatorType; import org.hisp.dhis.security.acl.AclService; import org.hisp.dhis.user.CurrentUserService; import org.springframework.context.ApplicationEventPublisher; @@ -84,4 +86,12 @@ public List getIndicatorsWithDataSets() { return getQuery(hql).setCacheable(true).list(); } + + @Override + public List getAssociatedIndicators(List indicatorTypes) { + TypedQuery query = + entityManager.createQuery( + "FROM Indicator i where i.indicatorType in :indicatorTypes", Indicator.class); + return query.setParameter("indicatorTypes", indicatorTypes).getResultList(); + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/preheat/DefaultPreheatService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/preheat/DefaultPreheatService.java index 1e4fe2b3ba5c..9232ed13e7a3 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/preheat/DefaultPreheatService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/preheat/DefaultPreheatService.java @@ -66,8 +66,8 @@ import org.hisp.dhis.query.Query; import org.hisp.dhis.query.QueryService; import org.hisp.dhis.query.Restrictions; -import org.hisp.dhis.schema.MergeParams; -import org.hisp.dhis.schema.MergeService; +import org.hisp.dhis.schema.MetadataMergeParams; +import org.hisp.dhis.schema.MetadataMergeService; import org.hisp.dhis.schema.Property; import org.hisp.dhis.schema.PropertyType; import org.hisp.dhis.schema.Schema; @@ -107,7 +107,7 @@ public class DefaultPreheatService implements PreheatService { private final AttributeService attributeService; - private final MergeService mergeService; + private final MetadataMergeService metadataMergeService; private final SchemaToDataFetcher schemaToDataFetcher; @@ -711,7 +711,8 @@ private Map, Map>> collectObjectReferences( try { IdentifiableObject identifiableObject = (IdentifiableObject) p.getKlass().newInstance(); - mergeService.merge(new MergeParams<>(reference, identifiableObject)); + metadataMergeService.merge( + new MetadataMergeParams<>(reference, identifiableObject)); refMap.get(object.getUid()).put(p.getName(), identifiableObject); } catch (InstantiationException | IllegalAccessException ignored) { } @@ -731,7 +732,8 @@ private Map, Map>> collectObjectReferences( try { IdentifiableObject identifiableObject = (IdentifiableObject) p.getItemKlass().newInstance(); - mergeService.merge(new MergeParams<>(reference, identifiableObject)); + metadataMergeService.merge( + new MetadataMergeParams<>(reference, identifiableObject)); refObjects.add(identifiableObject); } catch (InstantiationException | IllegalAccessException ignored) { } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/resources/i18n_global.properties b/dhis-2/dhis-services/dhis-service-core/src/main/resources/i18n_global.properties index 23d04b460c00..45fe5fd0c554 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/resources/i18n_global.properties +++ b/dhis-2/dhis-services/dhis-service-core/src/main/resources/i18n_global.properties @@ -192,6 +192,7 @@ F_INDICATOR_PRIVATE_ADD=Add/Update Private Indicator F_INDICATOR_DELETE=Delete Indicator F_INDICATORTYPE_ADD=Add/Update Indicator Type F_INDICATORTYPE_DELETE=Delete Indicator Type +F_INDICATOR_TYPE_MERGE=Merge Indicator Type F_INDICATORGROUP_PUBLIC_ADD=Add/Update Public Indicator Group F_INDICATORGROUP_PRIVATE_ADD=Add/Update Private Indicator Group F_INDICATORGROUP_DELETE=Delete Indicator Group diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/gml/DefaultGmlImportService.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/gml/DefaultGmlImportService.java index 445315ff39c8..a1d2ce572615 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/gml/DefaultGmlImportService.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/gml/DefaultGmlImportService.java @@ -65,8 +65,8 @@ import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.render.RenderService; import org.hisp.dhis.scheduling.JobProgress; -import org.hisp.dhis.schema.MergeParams; -import org.hisp.dhis.schema.MergeService; +import org.hisp.dhis.schema.MetadataMergeParams; +import org.hisp.dhis.schema.MetadataMergeService; import org.hisp.dhis.schema.SchemaService; import org.hisp.dhis.system.notification.Notifier; import org.locationtech.jts.geom.Geometry; @@ -110,7 +110,7 @@ public class DefaultGmlImportService implements GmlImportService { private final IdentifiableObjectManager idObjectManager; private final SchemaService schemaService; private final MetadataImportService importService; - private final MergeService mergeService; + private final MetadataMergeService metadataMergeService; @Transactional @Override @@ -285,7 +285,8 @@ private Map getMatchingPersistedOrgUnits( private void mergeNonGeoData(OrganisationUnit source, OrganisationUnit target) { Geometry geometry = target.getGeometry(); - mergeService.merge(new MergeParams<>(source, target).setMergeMode(MergeMode.MERGE)); + metadataMergeService.merge( + new MetadataMergeParams<>(source, target).setMergeMode(MergeMode.MERGE)); target.setGeometry(geometry); diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/DefaultObjectBundleService.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/DefaultObjectBundleService.java index c6cba14e4596..f45a4e435167 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/DefaultObjectBundleService.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/DefaultObjectBundleService.java @@ -59,8 +59,8 @@ import org.hisp.dhis.preheat.PreheatService; import org.hisp.dhis.scheduling.JobProgress; import org.hisp.dhis.scheduling.NoopJobProgress; -import org.hisp.dhis.schema.MergeParams; -import org.hisp.dhis.schema.MergeService; +import org.hisp.dhis.schema.MetadataMergeParams; +import org.hisp.dhis.schema.MetadataMergeService; import org.hisp.dhis.schema.SchemaService; import org.hisp.dhis.user.CurrentUserService; import org.hisp.dhis.user.User; @@ -82,7 +82,7 @@ public class DefaultObjectBundleService implements ObjectBundleService { private final IdentifiableObjectManager manager; private final DbmsManager dbmsManager; private final HibernateCacheManager cacheManager; - private final MergeService mergeService; + private final MetadataMergeService metadataMergeService; private final ObjectBundleHooks objectBundleHooks; private final EventHookPublisher eventHookPublisher; @@ -299,8 +299,8 @@ private TypeReport handleUpdates( object, bundle.getPreheat(), bundle.getPreheatIdentifier()); if (bundle.getMergeMode() != MergeMode.NONE) { - mergeService.merge( - new MergeParams<>(object, persistedObject) + metadataMergeService.merge( + new MetadataMergeParams<>(object, persistedObject) .setMergeMode(bundle.getMergeMode()) .setSkipSharing(bundle.isSkipSharing()) .setSkipTranslation(bundle.isSkipTranslation())); diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/AbstractObjectBundleHook.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/AbstractObjectBundleHook.java index 22dd325df56f..d723a0145ad8 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/AbstractObjectBundleHook.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/AbstractObjectBundleHook.java @@ -37,7 +37,7 @@ import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundleHook; import org.hisp.dhis.feedback.ErrorReport; import org.hisp.dhis.preheat.PreheatService; -import org.hisp.dhis.schema.MergeService; +import org.hisp.dhis.schema.MetadataMergeService; import org.hisp.dhis.schema.SchemaService; import org.springframework.beans.factory.annotation.Autowired; @@ -53,7 +53,7 @@ public class AbstractObjectBundleHook implements ObjectBundleHook { @Autowired protected SchemaService schemaService; - @Autowired protected MergeService mergeService; + @Autowired protected MetadataMergeService metadataMergeService; @Override public void validate(T object, ObjectBundle bundle, Consumer addReports) { diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/webmessage/WebMessageUtils.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/webmessage/WebMessageUtils.java index 912296a1de26..2a4dff8dbdb1 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/webmessage/WebMessageUtils.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/webmessage/WebMessageUtils.java @@ -40,12 +40,14 @@ import org.hisp.dhis.dxf2.scheduling.JobConfigurationWebMessageResponse; import org.hisp.dhis.dxf2.webmessage.responses.ErrorReportsWebMessageResponse; import org.hisp.dhis.dxf2.webmessage.responses.ImportReportWebMessageResponse; +import org.hisp.dhis.dxf2.webmessage.responses.MergeWebResponse; import org.hisp.dhis.dxf2.webmessage.responses.ObjectReportWebMessageResponse; import org.hisp.dhis.dxf2.webmessage.responses.TypeReportWebMessageResponse; import org.hisp.dhis.feedback.BadRequestException; import org.hisp.dhis.feedback.ErrorCode; import org.hisp.dhis.feedback.ErrorMessage; import org.hisp.dhis.feedback.ErrorReport; +import org.hisp.dhis.feedback.MergeReport; import org.hisp.dhis.feedback.ObjectReport; import org.hisp.dhis.feedback.Status; import org.hisp.dhis.feedback.TypeReport; @@ -215,6 +217,15 @@ public static WebMessage objectReport(ObjectReport objectReport) { .setResponse(new ObjectReportWebMessageResponse(objectReport)); } + public static WebMessage mergeReport(MergeReport mergeReport) { + if (!mergeReport.hasErrorMessages()) { + return ok().setResponse(new MergeWebResponse(mergeReport)); + } + return new WebMessage(Status.WARNING, HttpStatus.CONFLICT) + .setMessage("One or more errors occurred, please see full details in merge report.") + .setResponse(new MergeWebResponse(mergeReport)); + } + public static WebMessage jobConfigurationReport(JobConfiguration config) { return ok("Initiated " + config.getName()) .setResponse(new JobConfigurationWebMessageResponse(config)) diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/webmessage/responses/MergeWebResponse.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/webmessage/responses/MergeWebResponse.java new file mode 100644 index 000000000000..702af14c8e07 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/webmessage/responses/MergeWebResponse.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2004-2023, 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.dxf2.webmessage.responses; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.Nonnull; +import org.hisp.dhis.feedback.MergeReport; +import org.hisp.dhis.merge.MergeType; +import org.hisp.dhis.webmessage.WebMessageResponse; + +/** + * @author david mackessy + */ +public class MergeWebResponse implements WebMessageResponse { + @JsonProperty private MergeReport mergeReport; + + public MergeWebResponse(@Nonnull MergeReport mergeReport) { + this.mergeReport = mergeReport; + MergeType mergeType = mergeReport.getMergeType(); + this.mergeReport.setMessage( + mergeReport.hasErrorMessages() + ? "%s merge has errors".formatted(mergeType) + : "%s merge complete".formatted(mergeType)); + } +} diff --git a/dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/DefaultMergeService.java b/dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/DefaultMetadataMergeService.java similarity index 86% rename from dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/DefaultMergeService.java rename to dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/DefaultMetadataMergeService.java index 6b1d69828f37..453713481800 100644 --- a/dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/DefaultMergeService.java +++ b/dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/DefaultMetadataMergeService.java @@ -41,25 +41,26 @@ * @author Morten Olav Hansen */ @RequiredArgsConstructor -@Service("org.hisp.dhis.schema.MergeService") +@Service @Slf4j -public class DefaultMergeService implements MergeService { +public class DefaultMetadataMergeService implements MetadataMergeService { private final SchemaService schemaService; @Override - public T merge(MergeParams mergeParams) { - T source = mergeParams.getSource(); - T target = mergeParams.getTarget(); + public T merge(MetadataMergeParams metadataMergeParams) { + T source = metadataMergeParams.getSource(); + T target = metadataMergeParams.getTarget(); Schema schema = schemaService.getDynamicSchema(HibernateProxyUtils.getRealClass(source)); for (Property property : schema.getProperties()) { if (schema.isIdentifiableObject()) { - if (mergeParams.isSkipSharing() && ReflectionUtils.isSharingProperty(property)) { + if (metadataMergeParams.isSkipSharing() && ReflectionUtils.isSharingProperty(property)) { continue; } - if (mergeParams.isSkipTranslation() && ReflectionUtils.isTranslationProperty(property)) { + if (metadataMergeParams.isSkipTranslation() + && ReflectionUtils.isTranslationProperty(property)) { continue; } } @@ -83,7 +84,7 @@ public T merge(MergeParams mergeParams) { targetObject = ReflectionUtils.newCollectionInstance(property.getKlass()); } - if (mergeParams.getMergeMode().isMerge()) { + if (metadataMergeParams.getMergeMode().isMerge()) { Collection merged = ReflectionUtils.newCollectionInstance(property.getKlass()); merged.addAll(targetObject); merged.addAll( @@ -100,8 +101,8 @@ public T merge(MergeParams mergeParams) { } else { Object sourceObject = ReflectionUtils.invokeMethod(source, property.getGetterMethod()); - if (mergeParams.getMergeMode().isReplace() - || (mergeParams.getMergeMode().isMerge() && sourceObject != null)) { + if (metadataMergeParams.getMergeMode().isReplace() + || (metadataMergeParams.getMergeMode().isMerge() && sourceObject != null)) { ReflectionUtils.invokeMethod(target, property.getSetterMethod(), sourceObject); } } @@ -117,7 +118,7 @@ public T clone(T source) { try { return merge( - new MergeParams<>( + new MetadataMergeParams<>( source, (T) HibernateProxyUtils.getRealClass(source) diff --git a/dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/MergeParams.java b/dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/MetadataMergeParams.java similarity index 88% rename from dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/MergeParams.java rename to dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/MetadataMergeParams.java index 71340565c461..a66ec90c6f16 100644 --- a/dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/MergeParams.java +++ b/dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/MetadataMergeParams.java @@ -33,7 +33,7 @@ /** * @author Morten Olav Hansen */ -public final class MergeParams { +public final class MetadataMergeParams { private final T source; private final T target; @@ -44,7 +44,7 @@ public final class MergeParams { private boolean skipTranslation; - public MergeParams(T source, T target) { + public MetadataMergeParams(T source, T target) { this.source = Objects.requireNonNull(source); this.target = Objects.requireNonNull(target); } @@ -61,7 +61,7 @@ public MergeMode getMergeMode() { return mergeMode; } - public MergeParams setMergeMode(MergeMode mergeMode) { + public MetadataMergeParams setMergeMode(MergeMode mergeMode) { this.mergeMode = mergeMode; return this; } @@ -70,7 +70,7 @@ public boolean isSkipSharing() { return skipSharing; } - public MergeParams setSkipSharing(boolean skipSharing) { + public MetadataMergeParams setSkipSharing(boolean skipSharing) { this.skipSharing = skipSharing; return this; } @@ -79,7 +79,7 @@ public boolean isSkipTranslation() { return skipTranslation; } - public MergeParams setSkipTranslation(boolean skipTranslation) { + public MetadataMergeParams setSkipTranslation(boolean skipTranslation) { this.skipTranslation = skipTranslation; return this; } diff --git a/dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/MergeService.java b/dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/MetadataMergeService.java similarity index 90% rename from dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/MergeService.java rename to dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/MetadataMergeService.java index 3bf1727cd489..4adbf837d7f9 100644 --- a/dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/MergeService.java +++ b/dhis-2/dhis-services/dhis-service-schema/src/main/java/org/hisp/dhis/schema/MetadataMergeService.java @@ -30,13 +30,13 @@ /** * @author Morten Olav Hansen */ -public interface MergeService { +public interface MetadataMergeService { /** * Merges source object into target object, requires a "schema friendly" class. * - * @param mergeParams MergeParams instance containing source and target object + * @param metadataMergeParams MergeParams instance containing source and target object */ - T merge(MergeParams mergeParams); + T merge(MetadataMergeParams metadataMergeParams); /** * Creates a clone of given object and returns it. diff --git a/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/collection/CollectionUtils.java b/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/collection/CollectionUtils.java index 7f2420f298e3..15d0fa9f1c34 100644 --- a/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/collection/CollectionUtils.java +++ b/dhis-2/dhis-support/dhis-support-commons/src/main/java/org/hisp/dhis/commons/collection/CollectionUtils.java @@ -255,6 +255,16 @@ public static boolean isEmpty(Collection collection) { return collection == null || collection.isEmpty(); } + /** + * Indicates whether the given collection is not empty. + * + * @param collection the collection, may be null. + * @return true if the given collection is not empty, false otherwise. + */ + public static boolean isNotEmpty(Collection collection) { + return !isEmpty(collection); + } + /** * Adds the given object to the given collection if the object is not null. * diff --git a/dhis-2/dhis-support/dhis-support-commons/src/test/java/org/hisp/dhis/commons/util/CollectionUtilsTest.java b/dhis-2/dhis-support/dhis-support-commons/src/test/java/org/hisp/dhis/commons/util/CollectionUtilsTest.java index a98534022f42..2a63512affee 100644 --- a/dhis-2/dhis-support/dhis-support-commons/src/test/java/org/hisp/dhis/commons/util/CollectionUtilsTest.java +++ b/dhis-2/dhis-support/dhis-support-commons/src/test/java/org/hisp/dhis/commons/util/CollectionUtilsTest.java @@ -32,6 +32,7 @@ import static org.hisp.dhis.commons.collection.CollectionUtils.firstMatch; import static org.hisp.dhis.commons.collection.CollectionUtils.flatMapToSet; import static org.hisp.dhis.commons.collection.CollectionUtils.isEmpty; +import static org.hisp.dhis.commons.collection.CollectionUtils.isNotEmpty; import static org.hisp.dhis.commons.collection.CollectionUtils.mapToList; import static org.hisp.dhis.commons.collection.CollectionUtils.mapToSet; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -172,4 +173,11 @@ void testIsEmpty() { assertTrue(isEmpty(null)); assertFalse(isEmpty(List.of("One", "Two"))); } + + @Test + void testIsNotEmpty() { + assertFalse(isNotEmpty(List.of())); + assertFalse(isNotEmpty(null)); + assertTrue(isNotEmpty(List.of("One", "Two"))); + } } diff --git a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/DhisConvenienceTest.java b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/DhisConvenienceTest.java index a0d4e56ccb83..81099ff3a3ac 100644 --- a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/DhisConvenienceTest.java +++ b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/DhisConvenienceTest.java @@ -225,6 +225,7 @@ public abstract class DhisConvenienceTest { protected static final String BASE_UID = "abcdefghij"; protected static final String BASE_IN_UID = "inabcdefgh"; + protected static final String BASE_IN_TYPE_UID = "IntY123abg"; protected static final String BASE_DE_UID = "deabcdefgh"; @@ -825,6 +826,7 @@ public static IndicatorType createIndicatorType(char uniqueCharacter) { IndicatorType type = new IndicatorType(); type.setAutoFields(); + type.setUid(BASE_IN_TYPE_UID + uniqueCharacter); type.setName("IndicatorType" + uniqueCharacter); type.setFactor(100); diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/IndicatorTypeMergeTest.java b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/IndicatorTypeMergeTest.java new file mode 100644 index 000000000000..d6a84882c0db --- /dev/null +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/IndicatorTypeMergeTest.java @@ -0,0 +1,469 @@ +/* + * Copyright (c) 2004-2023, 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.merge; + +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItems; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import org.hisp.dhis.ApiTest; +import org.hisp.dhis.actions.LoginActions; +import org.hisp.dhis.actions.RestApiActions; +import org.hisp.dhis.actions.UserActions; +import org.hisp.dhis.dto.ApiResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class IndicatorTypeMergeTest extends ApiTest { + + private RestApiActions indicatorTypeApiActions; + private RestApiActions indicatorApiActions; + private LoginActions loginActions; + private UserActions userActions; + + @BeforeEach + public void before() { + loginActions = new LoginActions(); + indicatorTypeApiActions = new RestApiActions("indicatorTypes"); + indicatorApiActions = new RestApiActions("indicators"); + userActions = new UserActions(); + loginActions.loginAsSuperUser(); + } + + @Test + @DisplayName("Invalid source UID format results in error message") + void testInvalidSourceUid() { + // when a merge request with an invalid source UID format is submitted + ApiResponse response = + indicatorTypeApiActions + .post("merge", getMergeBody("invalid", "ValidUid001", "ValidUid002", false)) + .validateStatus(400); + + // then an error message is received advising of the UID constraints + response + .validate() + .statusCode(400) + .body("httpStatus", equalTo("Bad Request")) + .body("status", equalTo("ERROR")) + .body( + "message", + equalTo( + "JSON parse error: Cannot construct instance of `org.hisp.dhis.common.UID`, problem: UID must be an alphanumeric string of 11 characters starting with a letter.")); + } + + @Test + @DisplayName("Invalid target UID format results in error message") + void testInvalidTargetUid() { + // when a merge request with an invalid UID format is submitted + ApiResponse response = + indicatorTypeApiActions + .post("merge", getMergeBody("ValidUid002", "ValidUid001", "invalid", false)) + .validateStatus(400); + + // then an error message is received advising of the UID constraints + response + .validate() + .statusCode(400) + .body("httpStatus", equalTo("Bad Request")) + .body("status", equalTo("ERROR")) + .body( + "message", + equalTo( + "JSON parse error: Cannot construct instance of `org.hisp.dhis.common.UID`, problem: UID must be an alphanumeric string of 11 characters starting with a letter.")); + } + + @Test + @DisplayName( + "Valid merge request is processed, successful response received and sources not deleted") + void testValidMergeKeepSources() { + // given 3 indicators and 3 indicator types exist + // indicator types + String indTypeUid1 = + indicatorTypeApiActions + .post(createIndicatorType("A", 98, true)) + .validateStatus(201) + .extractUid(); + String indTypeUid2 = + indicatorTypeApiActions + .post(createIndicatorType("B", 99, false)) + .validateStatus(201) + .extractUid(); + String indTypeUid3 = + indicatorTypeApiActions + .post(createIndicatorType("C", 100, true)) + .validateStatus(201) + .extractUid(); + + // indicators referencing the indicator types + String i1 = + indicatorApiActions + .post(createIndicator("Ind1", indTypeUid1)) + .validateStatus(201) + .extractUid(); + String i2 = + indicatorApiActions + .post(createIndicator("Ind2", indTypeUid2)) + .validateStatus(201) + .extractUid(); + String i3 = + indicatorApiActions + .post(createIndicator("Ind3", indTypeUid3)) + .validateStatus(201) + .extractUid(); + + // when an indicator type merge request is submitted, keeping sources + ApiResponse response = + indicatorTypeApiActions + .post("merge", getMergeBody(indTypeUid1, indTypeUid2, indTypeUid3, false)) + .validateStatus(200); + + // then a successful response is received and no sources are deleted + response + .validate() + .statusCode(200) + .body("httpStatus", equalTo("OK")) + .body("response.mergeReport.message", equalTo("INDICATOR_TYPE merge complete")) + .body("response.mergeReport.mergeErrors", empty()) + .body("response.mergeReport.mergeType", equalTo("INDICATOR_TYPE")) + .body("response.mergeReport.sourcesDeleted", empty()); + + // and sources & target exist + indicatorTypeApiActions.get(indTypeUid1).validateStatus(200); + indicatorTypeApiActions.get(indTypeUid2).validateStatus(200); + indicatorTypeApiActions.get(indTypeUid3).validateStatus(200); + + // and all indicators now have target indicator type reference + indicatorApiActions + .get(i1) + .validate() + .statusCode(200) + .body("indicatorType.id", equalTo(indTypeUid3)); + + indicatorApiActions + .get(i2) + .validate() + .statusCode(200) + .body("indicatorType.id", equalTo(indTypeUid3)); + + indicatorApiActions + .get(i3) + .validate() + .statusCode(200) + .body("indicatorType.id", equalTo(indTypeUid3)); + } + + @Test + @DisplayName( + "Valid merge request is processed, successful response received and sources are deleted") + void testValidMergeDeleteSources() { + // given 3 indicators and 3 indicator types exist + // indicator types + String indTypeUid1 = + indicatorTypeApiActions + .post(createIndicatorType("D", 98, true)) + .validateStatus(201) + .extractUid(); + String indTypeUid2 = + indicatorTypeApiActions + .post(createIndicatorType("E", 99, false)) + .validateStatus(201) + .extractUid(); + String indTypeUid3 = + indicatorTypeApiActions + .post(createIndicatorType("F", 100, true)) + .validateStatus(201) + .extractUid(); + + // indicators + String i1 = + indicatorApiActions + .post(createIndicator("Ind4", indTypeUid1)) + .validateStatus(201) + .extractUid(); + String i2 = + indicatorApiActions + .post(createIndicator("Ind5", indTypeUid2)) + .validateStatus(201) + .extractUid(); + String i3 = + indicatorApiActions + .post(createIndicator("Ind6", indTypeUid3)) + .validateStatus(201) + .extractUid(); + + // when an indicator type merge request is submitted, deleting sources + ApiResponse response = + indicatorTypeApiActions + .post("merge", getMergeBody(indTypeUid1, indTypeUid2, indTypeUid3, true)) + .validateStatus(200); + + // then a successful response is received and sources are deleted + response + .validate() + .statusCode(200) + .body("httpStatus", equalTo("OK")) + .body("response.mergeReport.message", equalTo("INDICATOR_TYPE merge complete")) + .body("response.mergeReport.mergeErrors", empty()) + .body("response.mergeReport.mergeType", equalTo("INDICATOR_TYPE")) + .body("response.mergeReport.sourcesDeleted", hasItems(indTypeUid1, indTypeUid2)); + + // and sources are deleted & target exists + indicatorTypeApiActions.get(indTypeUid1).validateStatus(404); + indicatorTypeApiActions.get(indTypeUid2).validateStatus(404); + indicatorTypeApiActions.get(indTypeUid3).validateStatus(200); + + // and all indicators now reference target indicator type + indicatorApiActions + .get(i1) + .validate() + .statusCode(200) + .body("indicatorType.id", equalTo(indTypeUid3)); + + indicatorApiActions + .get(i2) + .validate() + .statusCode(200) + .body("indicatorType.id", equalTo(indTypeUid3)); + + indicatorApiActions + .get(i3) + .validate() + .statusCode(200) + .body("indicatorType.id", equalTo(indTypeUid3)); + } + + @Test + @DisplayName("Invalid merge request with no sources results in failure response") + void testInvalidMergeNoSources() { + // given a target indicator type exists + String indTypeUid1 = + indicatorTypeApiActions + .post(createIndicatorType("G", 98, true)) + .validateStatus(201) + .extractUid(); + + // when a merge request with no sources is sent + ApiResponse response = + indicatorTypeApiActions + .post("merge", getMergeBodyNoSources(indTypeUid1, true)) + .validateStatus(409); + + // then a response with an error is received + response + .validate() + .statusCode(409) + .body("httpStatus", equalTo("Conflict")) + .body("status", equalTo("WARNING")) + .body("response.mergeReport.message", equalTo("INDICATOR_TYPE merge has errors")) + .body( + "response.mergeReport.mergeErrors[0].message", + equalTo("At least one source indicator type must be specified")) + .body("response.mergeReport.mergeErrors[0].errorCode", equalTo("E1530")) + .body("response.mergeReport.mergeType", equalTo("INDICATOR_TYPE")) + .body("response.mergeReport.sourcesDeleted", empty()); + } + + @Test + @DisplayName("Invalid merge request with no target results in failure response") + void testInvalidMergeNoTarget() { + // given a valid source indicator type exists + String indTypeUid1 = + indicatorTypeApiActions + .post(createIndicatorType("H", 98, true)) + .validateStatus(201) + .extractUid(); + + // when a merge request with no target is sent + ApiResponse response = + indicatorTypeApiActions + .post("merge", getMergeBodyNoTarget(indTypeUid1, true)) + .validateStatus(409); + + // then a response with an error is received + response + .validate() + .statusCode(409) + .body("httpStatus", equalTo("Conflict")) + .body("status", equalTo("WARNING")) + .body("response.mergeReport.message", equalTo("INDICATOR_TYPE merge has errors")) + .body( + "response.mergeReport.mergeErrors[0].message", + equalTo("Target indicator type must be specified")) + .body("response.mergeReport.mergeErrors[0].errorCode", equalTo("E1531")) + .body("response.mergeReport.mergeType", equalTo("INDICATOR_TYPE")) + .body("response.mergeReport.sourcesDeleted", empty()); + } + + @Test + @DisplayName("Invalid merge request has errors when target is in sources") + void testInvalidMergeTargetInSources() { + // given indicator types exist + String indTypeUid1 = + indicatorTypeApiActions + .post(createIndicatorType("J", 98, true)) + .validateStatus(201) + .extractUid(); + String indTypeUid2 = + indicatorTypeApiActions + .post(createIndicatorType("K", 99, false)) + .validateStatus(201) + .extractUid(); + String indTypeUid3 = + indicatorTypeApiActions + .post(createIndicatorType("L", 100, true)) + .validateStatus(201) + .extractUid(); + + // when a merge request has the target contained in the sources + ApiResponse response = + indicatorTypeApiActions + .post("merge", getMergeBody(indTypeUid1, indTypeUid3, indTypeUid3, true)) + .validateStatus(409); + + // then a response with an error is received + response + .validate() + .statusCode(409) + .body("httpStatus", equalTo("Conflict")) + .body("status", equalTo("WARNING")) + .body("response.mergeReport.message", equalTo("INDICATOR_TYPE merge has errors")) + .body( + "response.mergeReport.mergeErrors[0].message", + equalTo("Target indicator type cannot be a source indicator type")) + .body("response.mergeReport.mergeErrors[0].errorCode", equalTo("E1532")) + .body("response.mergeReport.mergeType", equalTo("INDICATOR_TYPE")) + .body("response.mergeReport.sourcesDeleted", empty()); + } + + @Test + @DisplayName("Invalid merge request has multiple errors when sources and target don't exist") + void testInvalidMergeTargetAndSourcesDontExist() { + // when a merge request is sent with invalid source and target UIDs + ApiResponse response = + indicatorTypeApiActions + .post("merge", getMergeBody("invalidUid1", "invalidUid2", "invalidUid3", false)) + .validateStatus(409); + + // then a response with 3 errors is received (2 x sources, 1 x target) + response + .validate() + .statusCode(409) + .body("httpStatus", equalTo("Conflict")) + .body("status", equalTo("WARNING")) + .body("response.mergeReport.message", equalTo("INDICATOR_TYPE merge has errors")) + .body("response.mergeReport.mergeErrors.size()", equalTo(3)) + .body("response.mergeReport.mergeType", equalTo("INDICATOR_TYPE")) + .body("response.mergeReport.sourcesDeleted", empty()); + } + + @Test + @DisplayName("Invalid merge request no auth") + void testInvalidMergeNoAuth() { + // given a user with no indicator type merge auth + userActions.addUser("basicUser", "passTest1!"); + loginActions.loginAsUser("basicUser", "passTest1!"); + + // when they submit a merge request + ApiResponse response = + indicatorTypeApiActions + .post("merge", getMergeBody("invalidUid1", "invalidUid2", "invalidUid3", false)) + .validateStatus(403); + + // then they get an auth error + response + .validate() + .statusCode(403) + .body("httpStatus", equalTo("Forbidden")) + .body("status", equalTo("ERROR")) + .body("message", equalTo("Access is denied")); + } + + private JsonObject getMergeBody( + String source1, String source2, String target, boolean deleteSources) { + JsonObject json = new JsonObject(); + JsonArray array = new JsonArray(); + array.add(source1); + array.add(source2); + json.add("sources", array); + json.addProperty("target", target); + json.addProperty("deleteSources", deleteSources); + return json; + } + + private String getMergeBodyNoSources(String target, boolean deleteSources) { + return """ + { + "sources": [], + "target": "%s", + "number": "%b" + } + """ + .formatted(target, deleteSources); + } + + private String getMergeBodyNoTarget(String target, boolean deleteSources) { + return """ + { + "sources": ["%s"], + "target": null, + "number": "%b" + } + """ + .formatted(target, deleteSources); + } + + private String createIndicatorType(String name, int factor, boolean isNumber) { + return """ + { + "name": "test indicator type %s", + "factor": "%d", + "number": "%b" + } + """ + .formatted(name, factor, isNumber); + } + + private String createIndicator(String name, String indicatorType) { + return """ + { + "name": "test indicator %s", + "shortName": "test short %s", + "dimensionItemType": "INDICATOR", + "numerator": "#{fbfJHSPpUQD}", + "denominator": "#{h0xKKjijTdI}", + "indicatorType": { + "id": "%s" + } + } + """ + .formatted(name, name, indicatorType); + } +} diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dxf2/metadata/MergeServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dxf2/metadata/MetadataMergeServiceTest.java similarity index 89% rename from dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dxf2/metadata/MergeServiceTest.java rename to dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dxf2/metadata/MetadataMergeServiceTest.java index 221b800c5baa..ebf1a7d09ab4 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dxf2/metadata/MergeServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dxf2/metadata/MetadataMergeServiceTest.java @@ -40,8 +40,8 @@ import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.organisationunit.OrganisationUnitGroup; import org.hisp.dhis.organisationunit.OrganisationUnitGroupSet; -import org.hisp.dhis.schema.MergeParams; -import org.hisp.dhis.schema.MergeService; +import org.hisp.dhis.schema.MetadataMergeParams; +import org.hisp.dhis.schema.MetadataMergeService; import org.hisp.dhis.test.integration.SingleSetupIntegrationTestBase; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -50,9 +50,9 @@ /** * @author Morten Olav Hansen */ -class MergeServiceTest extends SingleSetupIntegrationTestBase { +class MetadataMergeServiceTest extends SingleSetupIntegrationTestBase { - @Autowired private MergeService mergeService; + @Autowired private MetadataMergeService metadataMergeService; @Override public void setUpTest() {} @@ -62,7 +62,8 @@ void simpleReplace() { Date date = new Date(); Simple source = new Simple("string", 10, date, false, 123, 2.5f); Simple target = new Simple(); - mergeService.merge(new MergeParams<>(source, target).setMergeMode(MergeMode.REPLACE)); + metadataMergeService.merge( + new MetadataMergeParams<>(source, target).setMergeMode(MergeMode.REPLACE)); Assertions.assertEquals("string", target.getString()); assertEquals(10, (int) target.getInteger()); Assertions.assertEquals(date, target.getDate()); @@ -75,7 +76,8 @@ void simpleMerge() { Date date = new Date(); Simple source = new Simple(null, 10, date, null, 123, 2.5f); Simple target = new Simple("hello", 20, date, true, 123, 2.5f); - mergeService.merge(new MergeParams<>(source, target).setMergeMode(MergeMode.MERGE)); + metadataMergeService.merge( + new MetadataMergeParams<>(source, target).setMergeMode(MergeMode.MERGE)); Assertions.assertEquals("hello", target.getString()); assertEquals(10, (int) target.getInteger()); Assertions.assertEquals(date, target.getDate()); @@ -90,7 +92,8 @@ void simpleCollection() { source.getSimples().add(new Simple("simple", 20, date, false, 123, 2.5f)); source.getSimples().add(new Simple("simple", 30, date, false, 123, 2.5f)); SimpleCollection target = new SimpleCollection("target"); - mergeService.merge(new MergeParams<>(source, target).setMergeMode(MergeMode.MERGE)); + metadataMergeService.merge( + new MetadataMergeParams<>(source, target).setMergeMode(MergeMode.MERGE)); Assertions.assertEquals("name", target.getName()); Assertions.assertEquals(3, target.getSimples().size()); Assertions.assertTrue(target.getSimples().contains(source.getSimples().get(0))); @@ -112,8 +115,8 @@ void mergeOrgUnitGroup() { organisationUnitGroupA.getMembers().add(organisationUnitD); OrganisationUnitGroupSet organisationUnitGroupSetA = createOrganisationUnitGroupSet('A'); organisationUnitGroupSetA.addOrganisationUnitGroup(organisationUnitGroupA); - mergeService.merge( - new MergeParams<>(organisationUnitGroupA, organisationUnitGroupB) + metadataMergeService.merge( + new MetadataMergeParams<>(organisationUnitGroupA, organisationUnitGroupB) .setMergeMode(MergeMode.REPLACE)); assertFalse(organisationUnitGroupB.getMembers().isEmpty()); assertEquals(4, organisationUnitGroupB.getMembers().size()); @@ -135,8 +138,8 @@ void mergeOrgUnitGroupSet() { OrganisationUnitGroupSet organisationUnitGroupSetA = createOrganisationUnitGroupSet('A'); OrganisationUnitGroupSet organisationUnitGroupSetB = createOrganisationUnitGroupSet('B'); organisationUnitGroupSetA.addOrganisationUnitGroup(organisationUnitGroupA); - mergeService.merge( - new MergeParams<>(organisationUnitGroupSetA, organisationUnitGroupSetB) + metadataMergeService.merge( + new MetadataMergeParams<>(organisationUnitGroupSetA, organisationUnitGroupSetB) .setMergeMode(MergeMode.REPLACE)); assertFalse(organisationUnitGroupSetB.getOrganisationUnitGroups().isEmpty()); assertEquals(organisationUnitGroupSetA.getName(), organisationUnitGroupSetB.getName()); @@ -154,7 +157,7 @@ void mergeOrgUnitGroupSet() { void testIndicatorClone() { IndicatorType indicatorType = createIndicatorType('A'); Indicator indicator = createIndicator('A', indicatorType); - Indicator clone = mergeService.clone(indicator); + Indicator clone = metadataMergeService.clone(indicator); assertEquals(indicator.getName(), clone.getName()); assertEquals(indicator.getUid(), clone.getUid()); assertEquals(indicator.getCode(), clone.getCode()); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/indicator/IndicatorTypeMergeServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/indicator/IndicatorTypeMergeServiceTest.java new file mode 100644 index 000000000000..7c1b651d367a --- /dev/null +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/indicator/IndicatorTypeMergeServiceTest.java @@ -0,0 +1,388 @@ +/* + * Copyright (c) 2004-2023, 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.merge.indicator; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.feedback.ErrorCode; +import org.hisp.dhis.feedback.ErrorMessage; +import org.hisp.dhis.feedback.MergeReport; +import org.hisp.dhis.indicator.Indicator; +import org.hisp.dhis.indicator.IndicatorType; +import org.hisp.dhis.merge.MergeParams; +import org.hisp.dhis.merge.MergeRequest; +import org.hisp.dhis.merge.MergeService; +import org.hisp.dhis.merge.MergeType; +import org.hisp.dhis.test.integration.TransactionalIntegrationTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @author david mackessy + */ +class IndicatorTypeMergeServiceTest extends TransactionalIntegrationTest { + + @Autowired private MergeService service; + + @Autowired private IdentifiableObjectManager idObjectManager; + + private IndicatorType itA; + private IndicatorType itB; + private IndicatorType itC; + + private final UID uidA = UID.of(BASE_IN_TYPE_UID + 'A'); + private final UID uidB = UID.of(BASE_IN_TYPE_UID + 'B'); + private final UID uidC = UID.of(BASE_IN_TYPE_UID + 'C'); + private final UID uidX = UID.of(BASE_IN_TYPE_UID + 'X'); + + @Override + public void setUpTest() { + itA = createIndicatorType('A'); + itA.setFactor(99); + itA.setNumber(true); + itB = createIndicatorType('B'); + itA.setFactor(98); + itA.setNumber(false); + itC = createIndicatorType('C'); + idObjectManager.save(itA); + idObjectManager.save(itB); + idObjectManager.save(itC); + } + + @Test + @DisplayName( + "Transform valid merge params, producing a valid merge request and an error free merge report") + void testGetFromParams() { + // given + MergeParams params = new MergeParams(); + params.setSources(Set.of(uidA, uidB)); + params.setTarget(uidC); + params.setDeleteSources(true); + MergeReport mergeReport = new MergeReport(MergeType.INDICATOR_TYPE); + + // when + MergeRequest request = service.validate(params, mergeReport); + + // then + assertEquals(2, request.getSources().size()); + assertTrue(request.getSources().containsAll(List.of(uidA, uidB))); + assertEquals(uidC, request.getTarget()); + assertTrue(request.isDeleteSources()); + assertFalse(mergeReport.hasErrorMessages()); + } + + @Test + @DisplayName( + "Transform merge params with missing sources and target, producing an empty merge request and a merge report with errors") + void testGetFromParamsWithErrors() { + // given + MergeParams params = new MergeParams(); + MergeReport mergeReport = new MergeReport(MergeType.INDICATOR_TYPE); + + // when + MergeRequest request = service.validate(params, mergeReport); + + // then + assertRequestIsEmpty(request); + assertTrue(mergeReport.hasErrorMessages()); + assertMatchesErrorCodes(mergeReport, Set.of(ErrorCode.E1530, ErrorCode.E1531)); + assertMatchesErrorMessages( + mergeReport, + Set.of( + "At least one source indicator type must be specified", + "Target indicator type must be specified")); + } + + @Test + @DisplayName( + "Transform merge params with invalid source uid, producing an empty merge request and a merge report with an error") + void testSourceNotFound() { + // given + MergeParams params = new MergeParams(); + params.setSources(Set.of(uidA, uidX)); + params.setTarget(uidC); + MergeReport mergeReport = new MergeReport(MergeType.INDICATOR_TYPE); + + // when + MergeRequest request = service.validate(params, mergeReport); + + // then + assertEquals(1, request.getSources().size()); + assertEquals(BASE_IN_TYPE_UID + 'C', request.getTarget().getValue()); + assertTrue(mergeReport.hasErrorMessages()); + assertMatchesErrorCodes(mergeReport, Set.of(ErrorCode.E1533)); + assertMatchesErrorMessages( + mergeReport, Set.of("Source indicator type does not exist: `IntY123abgX`")); + } + + @Test + @DisplayName( + "Transform merge params with invalid target uid, producing an empty merge request and a merge report with error") + void testTargetNotFound() { + // given + MergeParams params = new MergeParams(); + params.setSources(Set.of(uidA, uidB)); + params.setTarget(uidX); + MergeReport mergeReport = new MergeReport(MergeType.INDICATOR_TYPE); + + // when + MergeRequest request = service.validate(params, mergeReport); + + // then + assertRequestIsEmpty(request); + assertTrue(mergeReport.hasErrorMessages()); + assertMatchesErrorCodes(mergeReport, Set.of(ErrorCode.E1533)); + assertMatchesErrorMessages( + mergeReport, Set.of("Target indicator type does not exist: `IntY123abgX`")); + } + + @Test + @DisplayName("Validate a valid merge request") + void testValidate() { + // given indicators exist and are associated with indicator types + Indicator iA = createIndicator('A', itA); + Indicator iB = createIndicator('B', itB); + Indicator iC = createIndicator('C', itC); + idObjectManager.save(iA); + idObjectManager.save(iB); + idObjectManager.save(iC); + + assertNotNull(idObjectManager.get(IndicatorType.class, itA.getUid())); + assertNotNull(idObjectManager.get(IndicatorType.class, itB.getUid())); + assertNotNull(idObjectManager.get(IndicatorType.class, itC.getUid())); + assertEquals(itA, iA.getIndicatorType()); + assertEquals(itB, iB.getIndicatorType()); + assertEquals(itC, iC.getIndicatorType()); + MergeParams params = new MergeParams(); + params.setSources(Set.of(uidA, uidB)); + params.setTarget(uidC); + params.setDeleteSources(true); + + // when an indicator merge request is validated + MergeReport mergeReport = new MergeReport(MergeType.INDICATOR_TYPE); + service.validate(params, mergeReport); + + // then + // source indicator types exist + assertNotNull(idObjectManager.get(IndicatorType.class, itA.getUid())); + assertNotNull(idObjectManager.get(IndicatorType.class, itB.getUid())); + // and the target indicator type exists + assertNotNull(idObjectManager.get(IndicatorType.class, itC.getUid())); + + // and the merge report has no errors + assertFalse(mergeReport.hasErrorMessages()); + } + + @Test + @DisplayName("Validate an invalid merge request missing sources") + void testValidateWithErrorNoSources() { + // given + Indicator iC = createIndicator('C', itC); + idObjectManager.save(iC); + + assertNotNull(idObjectManager.get(IndicatorType.class, itC.getUid())); + assertEquals(itC, iC.getIndicatorType()); + MergeParams params = new MergeParams(); + params.setSources(Set.of()); + params.setTarget(uidC); + params.setDeleteSources(true); + + // when an indicator merge request is validated + MergeReport mergeReport = new MergeReport(MergeType.INDICATOR_TYPE); + MergeRequest validatedRequest = service.validate(params, mergeReport); + + // then + // and the target indicator exists + assertNotNull(idObjectManager.get(IndicatorType.class, itC.getUid())); + + // and the merge report has errors and the merge request is empty + assertTrue(mergeReport.hasErrorMessages()); + assertEquals(Set.of(), validatedRequest.getSources()); + assertEquals(uidC, validatedRequest.getTarget()); + assertTrue(validatedRequest.isDeleteSources()); + assertMatchesErrorCodes(mergeReport, Set.of(ErrorCode.E1530)); + assertMatchesErrorMessages( + mergeReport, Set.of("At least one source indicator type must be specified")); + } + + @Test + @DisplayName("Validate an invalid merge request missing a target") + void testValidateWithErrorNoTarget() { + // given indicators exist and are associated with indicator types + Indicator iA = createIndicator('A', itA); + Indicator iB = createIndicator('B', itB); + idObjectManager.save(iA); + idObjectManager.save(iB); + + assertNotNull(idObjectManager.get(IndicatorType.class, itA.getUid())); + assertNotNull(idObjectManager.get(IndicatorType.class, itB.getUid())); + assertEquals(itA, iA.getIndicatorType()); + assertEquals(itB, iB.getIndicatorType()); + MergeParams params = new MergeParams(); + params.setSources(Set.of(uidA, uidB)); + params.setTarget(null); + params.setDeleteSources(true); + + // when an indicator merge request is validated + MergeReport mergeReport = new MergeReport(MergeType.INDICATOR_TYPE); + MergeRequest validatedRequest = service.validate(params, mergeReport); + + // then + // and the source indicators exists + assertNotNull(idObjectManager.get(IndicatorType.class, itA.getUid())); + assertNotNull(idObjectManager.get(IndicatorType.class, itB.getUid())); + + // and the merge report has errors and the merge request is empty + assertTrue(mergeReport.hasErrorMessages()); + assertRequestIsEmpty(validatedRequest); + assertMatchesErrorCodes(mergeReport, Set.of(ErrorCode.E1531)); + assertMatchesErrorMessages(mergeReport, Set.of("Target indicator type must be specified")); + } + + @Test + @DisplayName("Merge indicator types and delete sources") + void testValidMergeDeleteSources() { + // given indicators exist and are associated with indicator types + Indicator iA = createIndicator('A', itA); + Indicator iB = createIndicator('B', itB); + Indicator iC = createIndicator('C', itC); + idObjectManager.save(iA); + idObjectManager.save(iB); + idObjectManager.save(iC); + + assertNotNull(idObjectManager.get(IndicatorType.class, itA.getUid())); + assertNotNull(idObjectManager.get(IndicatorType.class, itB.getUid())); + assertNotNull(idObjectManager.get(IndicatorType.class, itC.getUid())); + assertEquals(itA, iA.getIndicatorType()); + assertEquals(itB, iB.getIndicatorType()); + assertEquals(itC, iC.getIndicatorType()); + MergeRequest request = + MergeRequest.builder().sources(Set.of(uidA, uidB)).target(uidC).deleteSources(true).build(); + + // when an indicator merge request is merged + MergeReport mergeReport = new MergeReport(MergeType.INDICATOR_TYPE); + MergeReport completeReport = service.merge(request, mergeReport); + + // then + // source indicator types are deleted + assertNull(idObjectManager.get(IndicatorType.class, itA.getUid())); + assertNull(idObjectManager.get(IndicatorType.class, itB.getUid())); + // and the target indicator type exists + assertNotNull(idObjectManager.get(IndicatorType.class, itC.getUid())); + + // and associated source indicators are now associated to the target indicator type + assertEquals(itC, iA.getIndicatorType()); + assertEquals(itC, iB.getIndicatorType()); + assertEquals(itC, iC.getIndicatorType()); + assertEquals(100, itC.getFactor()); + assertFalse(itC.isNumber()); + + // and the merge report has no errors and contains deleted sources + assertFalse(completeReport.hasErrorMessages()); + assertEquals(Set.of("IntY123abgA", "IntY123abgB"), completeReport.getSourcesDeleted()); + } + + @Test + @DisplayName("Merge indicator types and do not delete sources") + void testValidMergeKeepSources() { + // given indicators exist and are associated with indicator types + Indicator iA = createIndicator('A', itA); + Indicator iB = createIndicator('B', itB); + Indicator iC = createIndicator('C', itC); + idObjectManager.save(iA); + idObjectManager.save(iB); + idObjectManager.save(iC); + + assertNotNull(idObjectManager.get(IndicatorType.class, itA.getUid())); + assertNotNull(idObjectManager.get(IndicatorType.class, itB.getUid())); + assertNotNull(idObjectManager.get(IndicatorType.class, itC.getUid())); + assertEquals(itA, iA.getIndicatorType()); + assertEquals(itB, iB.getIndicatorType()); + assertEquals(itC, iC.getIndicatorType()); + MergeRequest request = + MergeRequest.builder() + .sources(Set.of(uidA, uidB)) + .target(uidC) + .deleteSources(false) + .build(); + + // when an indicator merge request is merged + MergeReport mergeReport = new MergeReport(MergeType.INDICATOR_TYPE); + MergeReport completeReport = service.merge(request, mergeReport); + + // then + // source indicator types are deleted + assertNotNull(idObjectManager.get(IndicatorType.class, itA.getUid())); + assertNotNull(idObjectManager.get(IndicatorType.class, itB.getUid())); + // and the target indicator type exists + assertNotNull(idObjectManager.get(IndicatorType.class, itC.getUid())); + + // and associated source indicators are now associated to the target indicator type + assertEquals(itC, iA.getIndicatorType()); + assertEquals(itC, iB.getIndicatorType()); + assertEquals(itC, iC.getIndicatorType()); + assertEquals(100, itC.getFactor()); + assertFalse(itC.isNumber()); + + // and the merge report has no errors and shows 2 deleted sources + assertFalse(completeReport.hasErrorMessages()); + assertEquals(Set.of(), completeReport.getSourcesDeleted()); + } + + public static void assertRequestIsEmpty(MergeRequest request) { + assertEquals(0, request.getSources().size()); + assertNull(request.getTarget()); + assertFalse(request.isDeleteSources()); + } + + private void assertMatchesErrorMessages(MergeReport mergeReport, Set expected) { + Set actual = + mergeReport.getMergeErrors().stream() + .map(ErrorMessage::getMessage) + .collect(Collectors.toSet()); + assertEquals(expected, actual); + } + + private void assertMatchesErrorCodes(MergeReport mergeReport, Set expected) { + Set actual = + mergeReport.getMergeErrors().stream() + .map(ErrorMessage::getErrorCode) + .collect(Collectors.toSet()); + assertEquals(expected, actual); + } +} diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/orgunit/OrgUnitMergeServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/orgunit/OrgUnitMergeServiceTest.java index f6c1ea77aa23..f2e0a8892cc7 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/orgunit/OrgUnitMergeServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/orgunit/OrgUnitMergeServiceTest.java @@ -38,6 +38,7 @@ import org.hisp.dhis.common.IllegalQueryException; import org.hisp.dhis.dataset.DataSet; import org.hisp.dhis.feedback.ErrorCode; +import org.hisp.dhis.merge.DataMergeStrategy; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.organisationunit.OrganisationUnitGroup; import org.hisp.dhis.period.MonthlyPeriodType; diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/orgunit/handler/DataOrgUnitMergeHandlerTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/orgunit/handler/DataOrgUnitMergeHandlerTest.java index 3a984cabe12d..1dcbd9ebc3bb 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/orgunit/handler/DataOrgUnitMergeHandlerTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/orgunit/handler/DataOrgUnitMergeHandlerTest.java @@ -41,7 +41,7 @@ import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.datavalue.DataValue; import org.hisp.dhis.datavalue.DataValueService; -import org.hisp.dhis.merge.orgunit.DataMergeStrategy; +import org.hisp.dhis.merge.DataMergeStrategy; import org.hisp.dhis.merge.orgunit.OrgUnitMergeRequest; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.period.MonthlyPeriodType; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractCrudController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractCrudController.java index 42fd73324d7d..127c0d46c286 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractCrudController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractCrudController.java @@ -86,7 +86,7 @@ import org.hisp.dhis.patch.PatchParams; import org.hisp.dhis.patch.PatchService; import org.hisp.dhis.render.RenderService; -import org.hisp.dhis.schema.MergeService; +import org.hisp.dhis.schema.MetadataMergeService; import org.hisp.dhis.schema.validation.SchemaValidator; import org.hisp.dhis.sharing.SharingService; import org.hisp.dhis.translation.Translation; @@ -129,7 +129,7 @@ public abstract class AbstractCrudController @Autowired protected CollectionService collectionService; - @Autowired protected MergeService mergeService; + @Autowired protected MetadataMergeService metadataMergeService; @Autowired protected JsonPatchManager jsonPatchManager; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/CrudControllerAdvice.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/CrudControllerAdvice.java index 39a15de9fdce..40ad98475c23 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/CrudControllerAdvice.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/CrudControllerAdvice.java @@ -32,6 +32,7 @@ import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.createWebMessage; import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.error; import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.forbidden; +import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.mergeReport; import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.objectReport; import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.unauthorized; @@ -90,6 +91,7 @@ import org.springframework.beans.TypeMismatchException; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.jdbc.BadSqlGrammarException; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; @@ -176,6 +178,10 @@ public WebMessage conflictException(org.hisp.dhis.feedback.ConflictException ex) if (ex.getObjectReport() != null) { return objectReport(ex.getObjectReport()); } + + if (ex.getMergeReport() != null) { + return mergeReport(ex.getMergeReport()); + } return conflict(ex.getMessage(), ex.getCode()).setDevMessage(ex.getDevMessage()); } @@ -528,6 +534,32 @@ public WebMessage defaultExceptionHandler(Exception ex) { return error(getExceptionMessage(ex)); } + /** + * Exception handler handling {@link UID} instantiation errors (from {@link String} to {@link + * UID}) received in web requests. The error message is checked to see if it contains 'UID' & ';' + * so it can be formatted more nicely for client consumption, otherwise too much extraneous + * exception info is included. See e2e {@link IndicatorTypeMergeTest#testInvalidSourceUid} for + * example response expected. + * + * @param ex exception + * @return web message + */ + @ResponseBody + @ExceptionHandler(HttpMessageNotReadableException.class) + public WebMessage handleHttpMessageNotReadableExceptionHandler( + HttpMessageNotReadableException ex) { + String message = "HttpMessageNotReadableException exception has no message"; + String exMessage = ex.getMessage(); + if (exMessage != null) { + message = exMessage; + } + + if (message.contains("UID") && message.contains(";")) { + message = message.substring(0, message.indexOf(';')); + } + return badRequest(message); + } + private String getExceptionMessage(Exception ex) { boolean isMessageSensitive = false; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorTypeController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorTypeController.java index 109f4039ddd7..f9deec1755c8 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorTypeController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorTypeController.java @@ -27,12 +27,29 @@ */ package org.hisp.dhis.webapi.controller.indicator; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.dxf2.webmessage.WebMessage; +import org.hisp.dhis.dxf2.webmessage.WebMessageUtils; +import org.hisp.dhis.feedback.ConflictException; +import org.hisp.dhis.feedback.MergeReport; import org.hisp.dhis.indicator.IndicatorType; +import org.hisp.dhis.merge.MergeParams; +import org.hisp.dhis.merge.MergeProcessor; +import org.hisp.dhis.merge.MergeType; import org.hisp.dhis.schema.descriptors.IndicatorTypeSchemaDescriptor; import org.hisp.dhis.webapi.controller.AbstractCrudController; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; /** * @author Morten Olav Hansen @@ -40,4 +57,22 @@ @OpenApi.Tags("metadata") @Controller @RequestMapping(value = IndicatorTypeSchemaDescriptor.API_ENDPOINT) -public class IndicatorTypeController extends AbstractCrudController {} +@RequiredArgsConstructor +@Slf4j +public class IndicatorTypeController extends AbstractCrudController { + + private final MergeProcessor indicatorTypeMergeProcessor; + + @ResponseStatus(HttpStatus.OK) + @PreAuthorize("hasRole('ALL') or hasRole('F_INDICATOR_TYPE_MERGE')") + @PostMapping(value = "/merge", produces = APPLICATION_JSON_VALUE) + public @ResponseBody WebMessage mergeIndicatorTypes(@RequestBody MergeParams params) + throws ConflictException { + log.info("Indicator type merge received"); + + MergeReport report = indicatorTypeMergeProcessor.processMerge(params, MergeType.INDICATOR_TYPE); + + log.info("Indicator type merge processed with report: {}", report); + return WebMessageUtils.mergeReport(report); + } +} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/MapController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/MapController.java index 2d87cd233542..7d5440bb2e43 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/MapController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/MapController.java @@ -62,7 +62,7 @@ import org.hisp.dhis.period.Period; import org.hisp.dhis.program.ProgramService; import org.hisp.dhis.program.ProgramStageService; -import org.hisp.dhis.schema.MergeParams; +import org.hisp.dhis.schema.MetadataMergeParams; import org.hisp.dhis.schema.descriptors.MapSchemaDescriptor; import org.hisp.dhis.trackedentity.TrackedEntityType; import org.hisp.dhis.user.CurrentUser; @@ -139,8 +139,8 @@ public WebMessage putJsonObject( Map newMap = deserializeJsonEntity(request); newMap.setUid(uid); - mergeService.merge( - new MergeParams<>(newMap, map) + metadataMergeService.merge( + new MetadataMergeParams<>(newMap, map) .setMergeMode(params.getMergeMode()) .setSkipSharing(params.isSkipSharing()) .setSkipTranslation(params.isSkipTranslation())); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/user/UserController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/user/UserController.java index 8e872cfd0d76..c4070678db09 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/user/UserController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/user/UserController.java @@ -93,7 +93,7 @@ import org.hisp.dhis.query.Pagination; import org.hisp.dhis.query.Query; import org.hisp.dhis.query.QueryParserException; -import org.hisp.dhis.schema.MergeParams; +import org.hisp.dhis.schema.MetadataMergeParams; import org.hisp.dhis.schema.descriptors.UserSchemaDescriptor; import org.hisp.dhis.security.RestoreOptions; import org.hisp.dhis.security.SecurityService; @@ -478,7 +478,8 @@ public WebMessage replicateUser( } User userReplica = new User(); - mergeService.merge(new MergeParams<>(existingUser, userReplica).setMergeMode(MergeMode.MERGE)); + metadataMergeService.merge( + new MetadataMergeParams<>(existingUser, userReplica).setMergeMode(MergeMode.MERGE)); copyAttributeValues(userReplica); userReplica.setId(0); userReplica.setUuid(UUID.randomUUID()); From a01b627db1fbffd07870642abbbb0fac46e56a4d Mon Sep 17 00:00:00 2001 From: Enrico Colasante Date: Mon, 11 Dec 2023 10:04:54 +0100 Subject: [PATCH 2/2] chore: Add update snapshots to all github actions (#15868) --- .github/codeql/codeql-config.yml | 2 +- .github/workflows/analyse-pr.yml | 4 ++-- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/run-api-analytics-tests.yml | 6 +++--- .github/workflows/run-api-tests.yml | 4 ++-- .github/workflows/run-tests.yml | 5 ++--- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index b8f195c65dd4..478a871d2570 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -8,4 +8,4 @@ queries: - name: Extended Security uses: security-extended - name: Security and Quality - uses: security-and-quality \ No newline at end of file + uses: security-and-quality diff --git a/.github/workflows/analyse-pr.yml b/.github/workflows/analyse-pr.yml index aed1654d2e6e..1099d8790185 100644 --- a/.github/workflows/analyse-pr.yml +++ b/.github/workflows/analyse-pr.yml @@ -40,11 +40,11 @@ jobs: PR: ${{ github.event.number }} if: github.event_name == 'pull_request' run: | - mvn -f dhis-2/pom.xml clean install --threads 2C --batch-mode --no-transfer-progress -DskipTests + mvn -f dhis-2/pom.xml clean install --threads 2C --batch-mode --update-snapshots --no-transfer-progress -DskipTests mvn -f dhis-2/pom.xml sonar:sonar -Dsonar.internal.analysis.dbd=false --batch-mode --no-transfer-progress -Dsonar.scm.revision=${{ github.event.pull_request.head.sha }} -Dsonar.projectKey=dhis2_dhis2-core - name: Analyse long-living branch if: github.event_name != 'pull_request' run: | - mvn -f dhis-2/pom.xml clean install --threads 2C --batch-mode --no-transfer-progress -DskipTests + mvn -f dhis-2/pom.xml clean install --threads 2C --batch-mode --update-snapshots --no-transfer-progress -DskipTests mvn -f dhis-2/pom.xml sonar:sonar -Dsonar.internal.analysis.dbd=false --batch-mode --no-transfer-progress -Dsonar.branch.name=${GITHUB_REF#refs/heads/} -Dsonar.projectKey=dhis2_dhis2-core diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1613f5306039..04d876b2c428 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -58,9 +58,9 @@ jobs: config-file: ./.github/codeql/codeql-config.yml - name: Build core - run: mvn clean install -f ./dhis-2/pom.xml --batch-mode --no-transfer-progress -Pdev -DskipTests=true -Dmaven.javadoc.skip=true -V + run: mvn clean install -f ./dhis-2/pom.xml --batch-mode --no-transfer-progress --update-snapshots -Pdev -DskipTests=true -Dmaven.javadoc.skip=true -V - name: Build web - run: mvn clean install -f ./dhis-2/dhis-web/pom.xml --batch-mode --no-transfer-progress -Pdev -DskipTests=true -Dmaven.javadoc.skip=true -V + run: mvn clean install -f ./dhis-2/dhis-web/pom.xml --batch-mode --no-transfer-progress --update-snapshots -Pdev -DskipTests=true -Dmaven.javadoc.skip=true -V - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/run-api-analytics-tests.yml b/.github/workflows/run-api-analytics-tests.yml index 56e29fbdf642..5ee31ec503c4 100644 --- a/.github/workflows/run-api-analytics-tests.yml +++ b/.github/workflows/run-api-analytics-tests.yml @@ -31,7 +31,7 @@ jobs: # Remove Android and .NET libs (approx. 14GB) sudo rm -rf /usr/local/lib/android sudo rm -rf /usr/share/dotnet - + - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -41,8 +41,8 @@ jobs: - name: Build core image run: | - mvn clean install --threads 2C --batch-mode --no-transfer-progress -DskipTests -Dmaven.test.skip=true -f dhis-2/pom.xml -pl -dhis-web-embedded-jetty,-dhis-test-integration,-dhis-test-coverage - mvn clean install --threads 2C --batch-mode --no-transfer-progress -DskipTests -Dmaven.test.skip=true -f dhis-2/dhis-web/pom.xml + mvn clean install --threads 2C --batch-mode --no-transfer-progress -DskipTests -Dmaven.test.skip=true --update-snapshots -f dhis-2/pom.xml -pl -dhis-web-embedded-jetty,-dhis-test-integration,-dhis-test-coverage + mvn clean install --threads 2C --batch-mode --no-transfer-progress -DskipTests -Dmaven.test.skip=true --update-snapshots -f dhis-2/dhis-web/pom.xml mvn --batch-mode --no-transfer-progress -DskipTests -Dmaven.test.skip=true -f ./dhis-2/dhis-web/dhis-web-portal/pom.xml jib:dockerBuild -Djib.to.image=$CORE_IMAGE_NAME - name: Get Sierra Leone DB from cache diff --git a/.github/workflows/run-api-tests.yml b/.github/workflows/run-api-tests.yml index 3f254b9226aa..c4e9a13cdb05 100644 --- a/.github/workflows/run-api-tests.yml +++ b/.github/workflows/run-api-tests.yml @@ -37,8 +37,8 @@ jobs: - name: Build war run: | - mvn clean install --threads 2C --batch-mode --no-transfer-progress -DskipTests -Dmaven.test.skip=true -f dhis-2/pom.xml -pl -dhis-web-embedded-jetty,-dhis-test-integration,-dhis-test-coverage - mvn clean install --threads 2C --batch-mode --no-transfer-progress -DskipTests -Dmaven.test.skip=true -f dhis-2/dhis-web/pom.xml + mvn clean install --threads 2C --batch-mode --no-transfer-progress -DskipTests -Dmaven.test.skip=true --update-snapshots -f dhis-2/pom.xml -pl -dhis-web-embedded-jetty,-dhis-test-integration,-dhis-test-coverage + mvn clean install --threads 2C --batch-mode --no-transfer-progress -DskipTests -Dmaven.test.skip=true --update-snapshots -f dhis-2/dhis-web/pom.xml - name: Build container image run: | diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 8358ee95ff0e..7a899bc5d4c8 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -69,7 +69,7 @@ jobs: distribution: temurin cache: maven - name: Run integration tests - run: mvn clean verify --threads 2C --batch-mode --no-transfer-progress -Pintegration -f ./dhis-2/pom.xml -pl -dhis-web-embedded-jetty + run: mvn clean verify --threads 2C --batch-mode --no-transfer-progress -Pintegration --update-snapshots -f ./dhis-2/pom.xml -pl -dhis-web-embedded-jetty timeout-minutes: 30 - uses: actions/upload-artifact@v3 name: Upload test logs on failure @@ -120,7 +120,7 @@ jobs: distribution: temurin cache: maven - name: Run integration h2 tests - run: mvn clean verify --threads 2C --batch-mode --no-transfer-progress -PintegrationH2 -f ./dhis-2/pom.xml -pl -dhis-web-embedded-jetty + run: mvn clean verify --threads 2C --batch-mode --no-transfer-progress -PintegrationH2 --update-snapshots -f ./dhis-2/pom.xml -pl -dhis-web-embedded-jetty timeout-minutes: 30 - uses: actions/upload-artifact@v3 name: Upload test logs on failure @@ -175,4 +175,3 @@ jobs: SLACK_CHANNEL: 'team-backend' SLACK_MESSAGE: "Latest test run on master failed and needs investigation :detective-duck:. \n Commit message: ${{ github.event.head_commit.message }}" SLACK_COLOR: '#ff0000' -